How do you write a good Dockerfile? What are multi-stage builds?
- Technology:
- Docker
Quick answer
Use a small, pinned base image, order instructions so dependencies are cached before source code, use multi-stage builds to leave build tools out of the final image, run as a non-root user and keep secrets out of the image.
Detailed explanation
Layer caching drives build speed. Copy the dependency manifest and install dependencies first, then copy the source code. Changing a source file then reuses the cached dependency layer instead of reinstalling everything.
A multi-stage build uses one stage with compilers and dev dependencies to build the app, then copies only the output into a slim runtime stage. The final image is smaller, starts faster and has a smaller attack surface.
Other habits: pin base image versions, add a .dockerignore (exclude node_modules, .git, local env files), run as a non-root user, use CMD in exec form so signals reach your process, and pass secrets at runtime or with build secrets rather than ENV or COPY.
Example
# build stage
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# runtime stage
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]Key points
- Copy dependency files before source code for caching
- Multi-stage builds keep build tools out of production images
- Pin versions, use .dockerignore, run as non-root
- Never bake secrets into image layers
Common mistakes
- COPY . . before installing dependencies, which breaks caching.
- Using the latest tag, so builds change without warning.
- Deleting a secret in a later layer and assuming it is gone. It still exists in the earlier layer.
Follow-up questions
- What is the difference between CMD and ENTRYPOINT?