Practice brief

Building Custom Images Exercises

Until now you have been running the API by mounting your code into a borrowed Node image. In Module 5 you write the Dockerfile that turns TaskFlow's API into a real image you can build, tag, and run without any bind mount. This is the most important step in the incremental project arc.

Build from scratch

5.1 Build from scratch
~25-35 min

Write the TaskFlow API Dockerfile

Modules 2 through 4 ran the API by mounting your local code into a node:20-alpine container. That works for development but it is not a real image — there is no Dockerfile, nothing to build, nothing you can share with another developer or deploy to a server. In this exercise you will write the Dockerfile that packages the TaskFlow API into a proper image. After this, running the API is just docker run taskflow-api — no bind mount, no manual path, no node command specified.

Try it yourself first. Open the guided path if you get blocked.

Step 1 — Create the Dockerfile

touch Dockerfile

Create a file named exactly Dockerfile with no extension at the root of the taskflow-lab directory. Docker looks for this filename by default when you run docker build.

Step 2 — Write the FROM instruction

Open Dockerfile in your editor and add the first line. FROM sets the base image — everything you add builds on top of it. FROM node:20-alpine

Step 3 — Set the working directory

Add WORKDIR below FROM. This creates the directory if it does not exist and sets it as the default location for all following instructions. WORKDIR /app

Step 4 — Copy dependencies and install them

Add these two lines next. The order matters — copy package files first, install, then copy the rest of the code. This way Docker caches the install step and skips it on rebuilds unless package.json actually changed. COPY package*.json ./ RUN npm install

Step 5 — Copy the application code

After npm install, copy the rest of the project into the image. At this point your Dockerfile should have FROM, WORKDIR, COPY package*.json, RUN npm install, and now this. COPY . .

Step 6 — Set the start command

The final instruction tells Docker what to run when the container starts. Use the array form — this is exec form and it passes signals directly to the Node process. You will see why this matters in the break-fix exercise. CMD ["node", "api/server.mjs"]

Step 7 — Build the image

docker build -t taskflow-api .

-t taskflow-api tags the image with a name. The dot is the build context — Docker sends the current directory to the builder. Watch the output: each step in your Dockerfile becomes a layer. If a layer is cached from a previous build you will see CACHED next to it.

Step 8 — Run the container from your new image

docker run -d --name taskflow-api -p 8000:8000 taskflow-api

No -v flag. No -w flag. No node command at the end. The Dockerfile encoded all of that into the image. The only flags you need now are -d for background and -p to expose the port.

Step 9 — Verify the API responds

curl http://localhost:8000/health

If you see a JSON response, the Dockerfile is correct. If the container exits, run docker logs taskflow-api to see the error. The most common mistake at this stage is a wrong path in CMD — check that the file path matches your project structure.

Break-fix

5.2 Break-fix
~15-25 min

The Dockerfile That Will Not Stop Cleanly

A teammate wrote a Dockerfile for the TaskFlow API. It builds, the container starts, and the API responds. Everything looks fine. But when you run docker stop taskflow-api, the container takes exactly 10 seconds to stop instead of stopping immediately. Multiply that by a rolling deployment across ten containers and you have a 100-second deployment window where traffic is still hitting dying containers. Reproduce the slow stop, find what in the Dockerfile causes it, and fix it so docker stop returns in under two seconds.

printf 'FROM node:20-alpine\nWORKDIR /app\nCOPY package*.json ./\nRUN npm install\nCOPY . .\nCMD node api/server.mjs\n' > Dockerfile.broken
docker build -t taskflow-api-broken -f Dockerfile.broken .
docker run -d --name taskflow-api-broken -p 8000:8000 taskflow-api-broken
time docker stop taskflow-api-broken
Show investigative hints
  • The 10-second delay is not a bug in the API — it is Docker's default timeout before it gives up waiting for a graceful shutdown and sends SIGKILL instead.
  • When the CMD uses shell form (CMD node api/server.mjs), the node process is not PID 1 inside the container. Something else is PID 1. Run docker exec taskflow-api-broken ps aux and look at which process has PID 1.
  • The process that is PID 1 receives the shutdown signal. If that process does not forward the signal to node, node never gets the shutdown request and just keeps running until the timeout.
Show diagnosis and fix rationale

CMD node api/server.mjs uses shell form, so Docker starts a shell as PID 1 and the Node process becomes its child. docker stop sends the shutdown signal to PID 1. The shell does not forward that signal cleanly to Node, so Docker waits for its timeout and then kills the container. Use exec form instead: CMD ["node", "api/server.mjs"]. Then Node is PID 1 and receives the shutdown signal directly.