Lesson 06/16 minutes/1 graded
Writing a Dockerfile that stays fast
Instruction order, multi-stage builds, and the layer that invalidates everything.
Everything so far, applied to one file. The target is an image that rebuilds quickly after a source edit and starts correctly on a machine that is not yours.
The sample project
Every command in this lesson runs against the course sample,
examples/docker-for-developers-api: a small HTTP API with one runtime dependency
(nanoid) and one build-only dependency (typescript). That split is what the
Dockerfile below is built around. Put it at ~/projects/api and the Dockerfile it
ships with is the one shown here.
A complete example
# syntax=docker/dockerfile:1
ARG NODE_VERSION=24-slim
FROM node:${NODE_VERSION} AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts --no-audit --no-fund
FROM node:${NODE_VERSION} AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:${NODE_VERSION} AS prod-deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev --ignore-scripts --no-audit --no-fund
FROM node:${NODE_VERSION} AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=prod-deps /app/node_modules ./node_modules
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
The deps stage exists so a source edit does not reinstall anything, and
prod-deps exists so the image ships runtime dependencies only. The build
stage needs devDependencies to compile; the runtime stage must not inherit them,
which is why they come from different stages.
The remaining lines that are not self-explanatory:
.dockerignore is not optional
Before the daemon runs a single instruction, the CLI sends it the build context,
the directory you pointed at. Without a .dockerignore, that includes
node_modules, .git, and .next. On a normal project that is hundreds of
megabytes uploaded before the build even starts.
node_modules
.git
.next
.env*
*.log
USER node: do not run as root
Containers run as root by default. A process that is compromised inside the container is then root inside the container, which is a much better starting position for an attacker than it needs to be. Official images usually ship a non-root user; switch to it after the last step that needs to write.
CMD in exec form
CMD ["node", "dist/server.js"] # exec form — the process is PID 1
CMD node dist/server.js # shell form — wrapped in /bin/sh -c
Shell form means your process is a child of sh, and sh does not forward
SIGTERM. docker stop then always takes the full ten-second timeout and kills
you ungracefully. Use the exec form.
ENV NODE_ENV=production
Express and many other Node frameworks read NODE_ENV and change how much they
log and how much error detail they return. Setting it in the image means nobody
has to remember it at deploy time.
Pin your base image
node:24-slim is better than node:latest. A digest is more specific still:
FROM node:24-slim@sha256:...
A tag can be repointed at a new image; a digest always refers to the same bytes. Pinning by digest means you never get a surprise patch, and also that you never get a security fix without editing the file. Which surprise you prefer is the actual decision.
Prove it runs
Build it, then run it with the port published, and check that the process actually answers. An image that has never been started tells you nothing about whether it works.
docker ps tells you whether it is still up, and curl localhost:3000 tells you
whether it is serving. If docker ps comes back empty the container exited
already, and docker logs api will say why.
Where this stops being enough
The image you just ran is a single container started by hand. The next problems are the ones that hand-started containers cannot solve: restarting a crashed process, rolling out a new version without downtime, and running more than one copy behind something that balances load. Those belong to Compose in the small case and to an orchestrator in the large one.
Read the Dockerfile reference
when you need an instruction this course did not cover. HEALTHCHECK and
ARG-versus-ENV are the two most people reach for next.