sehaj/learn
Docker/02 Images

02.08 · build · 25 min

Project: containerise the API

Put Module 02 together: take the notes API from source code to a small, non-root, cache-friendly image in a registry.

The brief

The notes API is going to production eventually. This step produces its image. Done means:

Try it yourself first. Each step below has the answer folded away under "Stuck?".

Get the code

❯terminal
$ git clone https://github.com/sehajmakkar/learn.git
$ cd learn
$ git checkout docker-step-01
$ cd project/docker/api
$ ls
Dockerfile README.md package-lock.json package.json src tsconfig.json

docker-step-01 is the finished state of this step, so delete Dockerfile and .dockerignore now for a clean start. Later steps change these files, which is why you check out the git tag instead of using main.

Step 1: the build context

CheckpointWrite .dockerignore

Create a .dockerignore. When you build later, the "transferring context" line should be a few kilobytes, not megabytes.

STUCK? ▸

Ignore at least node_modules, dist, .git, .env and *.log. Adding Dockerfile and .dockerignore keeps edits to them from busting the COPY cache.

Step 2: the Dockerfile

CheckpointWrite a multi-stage Dockerfile

Two stages on node:22-slim. The build stage installs everything and runs npm run build. The final stage sets NODE_ENV=production, installs production dependencies only, copies dist from the build stage, switches to the node user and starts node dist/server.js.

STUCK? ▸

Compare with the finished Dockerfile in project/docker/api (tag docker-step-01). The key moves: name the first stage AS build; copy package.json and the lockfile before npm ci; in the final stage run npm ci --omit=dev, COPY --from=build /app/dist ./dist, then USER node and an exec-form CMD.

Step 3: build and inspect

❯terminal
$ docker build -t notes-api .
...
#7 transferring context: 3.82kB done
...
$ docker images notes-api
IMAGE ID DISK USAGE CONTENT SIZE
notes-api:latest cf94b518b7ef 349MB 80.4MB
$ docker run --rm notes-api ls
dist
node_modules
package-lock.json
package.json

Output trimmed; your ID will differ. No src, no tsconfig.json, no TypeScript compiler.

Now prove the cache works: change a message in src/server.ts and rebuild. Both npm ci steps should say CACHED; only the source copy, the compile and the final COPY --from should run.

Step 4: run it like production would

❯terminal
$ docker run -d --name notes -p 8080:3000 notes-api
$ curl localhost:8080/healthz
{"status":"ok"}
$ docker exec notes whoami
node
$ docker stop notes
notes
$ docker logs notes
notes-api listening on :3000
SIGTERM received, closing

docker stop returns almost immediately, because the app handles SIGTERM and runs as PID 1 with the exec-form CMD. If yours waits for the whole stop timeout and the logs don't show SIGTERM received, check you used CMD ["node", "dist/server.js"] and not the shell form.

Step 5: tag and push

Tag with the version from package.json and the short commit hash, so any running container can be traced back to its code:

❯terminal
$ VERSION=$(node -p "require('./package.json').version")
$ SHA=$(git rev-parse --short HEAD)
$ docker run -d --name registry -p 5050:5000 registry:2
$ docker tag notes-api localhost:5050/notes-api:$VERSION
$ docker tag notes-api localhost:5050/notes-api:sha-$SHA
$ docker push --all-tags localhost:5050/notes-api

Both tags point at the same image, so each layer uploads once and the second tag only adds a name. To publish for real, swap localhost:5050 for ghcr.io/YOUR_USERNAME after docker login ghcr.io (02.06 Tags, digests and registries).

Predict first

You edit src/server.ts and rebuild your finished multi-stage image. Which steps run again?

Commit to an answer before you read on.

Compare with the reference

❯terminal
$ git diff docker-step-01 -- project/docker/api

No output means your files match the reference exactly. Differences aren't necessarily wrong. If yours builds, the image contains only what it needs, runs as node and stops cleanly, it meets the brief.

What's next

The image works, but it has no idea whether the app inside is healthy, takes its config from code, and keeps notes in memory. Module 03 adds configuration, limits and a healthcheck (03.07 Healthchecks (project step 2)), and Module 04 moves the notes into Postgres (04.05 Project: persist Postgres).

Interview questions

MidfreqMust-knowDesign

Walk me through how you'd write a production Dockerfile for a Node.js service.

Report a mistakeEdit this page
☰ Images02.08 · 14/44next →