Writing Production-Grade Dockerfiles
BeginnerProduction Dockerfiles use multi-stage builds, non-root users, .dockerignore, explicit version pinning, and health checks to produce small, secure, reproducible images.
Overview
A naive Dockerfile that "works" can produce 1 GB images containing build tools, dev dependencies, and running as root — a security and performance nightmare. Multi-stage builds are the single most impactful technique: use a fat build stage to compile your app, then copy only the final artifact into a minimal runtime image. Combined with a .dockerignore file, non-root user, pinned base image versions, and a HEALTHCHECK, your production Dockerfile becomes small, secure, and deterministic.
Multi-Stage Builds
Multi-stage builds use multiple FROM instructions in one Dockerfile. Only the final stage becomes the image. Earlier stages are used as build environments and are discarded, along with their tools and intermediate files.
# ──── Stage 1: build ────────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci # includes devDependencies
COPY . .
RUN npm run build # compile TypeScript, bundle, etc.
# ──── Stage 2: production runtime ────────────────────
FROM node:20-alpine AS runtime # fresh image — no build tools
WORKDIR /app
# Only copy what the app needs to run
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json .
# Security: run as non-root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
# Result:
# Without multi-stage: ~700 MB (includes TypeScript compiler, source maps, devDeps)
# With multi-stage: ~180 MB (only runtime + dist output).dockerignore — Exclude What You Do Not Need
Without .dockerignore, the Docker build context sends your entire project directory to the daemon — including node_modules (300 MB+), .git, secrets, and local env files. This slows every build and can leak sensitive data into the image.
# .dockerignore (place next to Dockerfile)
node_modules/ # re-installed inside container; don't copy host's
.git/ # git history not needed in image
.env # never bake secrets into images!
.env.local
dist/ # rebuilt inside container
coverage/
*.log
.DS_Store
README.md
docker-compose*.yml # runtime config, not build artifact
.github/
# Verify your build context size:
# docker build --no-cache . 2>&1 | grep "Sending build context"
# Before .dockerignore: Sending build context to Docker daemon 480.1MB
# After .dockerignore: Sending build context to Docker daemon 12.5MBSecurity Hardening
Containers run as root by default. A vulnerability in your app can give attackers root access to your container and potentially the host. Always create and switch to a non-root user. Also pin base image versions for reproducibility.
FROM node:20.15.1-alpine3.20 # ↠pin exact version, not 'node:latest'
# Create non-root user and group
RUN addgroup -S appgroup && \
adduser -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup . . # set ownership at copy time
RUN npm ci --only=production
USER appuser # switch before CMD
# Verify non-root at runtime:
# docker run my-app whoami → appuser
# Also: use 'distroless' for even smaller, no-shell images
# FROM gcr.io/distroless/nodejs20-debian12 AS runtime
# No bash, no apt, no package manager — minimal attack surfaceKey Points to Remember
- 1Multi-stage builds separate build environment from runtime — dramatically smaller images.
- 2.dockerignore prevents leaking node_modules, .git, and .env files into the build context.
- 3Never run containers as root in production — always add a non-root user.
- 4Pin exact base image versions (node:20.15.1-alpine3.20) for reproducibility.
- 5HEALTHCHECK lets Docker and orchestrators know if the container is actually healthy.
- 6Never bake secrets (API keys, passwords) into Docker images — use env vars or secrets managers.
Interview Questions
Sign in to ask AriaWhat is a multi-stage Docker build and why do you use it?
Why should you not run containers as root?
What goes in .dockerignore and why?
Ask Aria about Writing Production-Grade Dockerfiles
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.