Writing Production Dockerfiles
A Dockerfile is a declarative script containing step-by-step instructions that the Docker engine translates into stacked, cached read-only image layers. Writing efficient Dockerfiles requires understanding layer cache invalidation, signal propagation, and least-privilege security.
1. Complete Dockerfile Directives Reference
| Directive | Behavior & Layer Impact | Syntax Example |
|---|---|---|
FROM | Initializes a new build stage and sets the parent base image. Must be the first non-comment line. | FROM node:20-alpine AS builder |
WORKDIR | Sets working directory for all subsequent directives (RUN, CMD, COPY). Creates directory if missing. | WORKDIR /app |
COPY | Copies files/directories from the local host build context into the image filesystem. Preserves permissions. | COPY --chown=node:node package*.json ./ |
ADD | Like COPY, but also automatically extracts local tarballs and fetches remote URLs. Prefer COPY unless auto-extract is needed. | ADD assets.tar.gz /app/assets/ |
RUN | Executes shell commands during the build phase and commits the resulting filesystem delta as a new image layer. | RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* |
ENV | Sets persistent environment variables accessible both during image build and at container runtime. | ENV NODE_ENV=production PORT=8080 |
ARG | Defines ephemeral build-time variables passed via docker build --build-arg. Not persisted in running containers. | ARG APP_VERSION=1.0.0 |
EXPOSE | Documents which network ports the application listens on. Does not publish ports (metadata only). | EXPOSE 8080/tcp |
USER | Switches execution user and group ID for security, ensuring the container does not run as root. | USER nonroot:nonroot |
HEALTHCHECK | Configures an automated health probe executed by the Docker daemon to monitor container viability. | HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:8080/health || exit 1 |
2. The Deep Dive: CMD vs ENTRYPOINT
Understanding the distinction between ENTRYPOINT and CMD is critical for designing reusable, CLI-friendly containers:
ENTRYPOINT (The Fixed Binary)
Specifies the immutable executable that the container always starts with. It turns the container into an executable CLI command.
ENTRYPOINT ["python", "manage.py"]
CMD (The Default Arguments)
Supplies default arguments to the ENTRYPOINT. Any arguments passed on the terminal viadocker run <image> [args...] will override CMD entirely!
CMD ["runserver", "0.0.0.0:8000"]
Exec Form vs Shell Form (The Signal Trap)
Both directives can be written in two forms, but only one is safe for production:
- Exec Form (Recommended):
CMD ["node", "server.js"]
Runs the binary directly as PID 1. When you stop the container, the LinuxSIGTERMsignal is delivered straight to your application process, enabling graceful connection shutdown! - Shell Form (Danger):
CMD node server.js
Spawns/bin/sh -c "node server.js". The shell becomes PID 1 and frequently refuses to forwardSIGTERMsignals to child processes. Your app cannot shut down cleanly and gets forcefully killed viaSIGKILLafter 10 seconds!
3. Optimizing the Docker Layer Cache
Docker evaluates instructions from top to bottom. If a layer hasn't changed and all previous layers were cache hits, Docker reuses the cached layer instantly (0 seconds build time). The moment a layer changes, every subsequent layer's cache is invalidated!
❌ Slow Cache Invalidation (Bad):
FROM node:20-alpine WORKDIR /app COPY . . RUN npm install CMD ["npm", "start"]
Changing even one line of CSS or README invalidates COPY . ., forcing a complete npm install (downloading hundreds of MBs) on every build!
✅ Optimized Layer Order (Production):
FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . CMD ["node", "dist/main.js"]
Dependencies are only re-downloaded when package.json changes. Normal source code changes build in less than 2 seconds!
4. Enterprise Production Dockerfile Template
A hardened, secure Node.js application Dockerfile implementing non-root user execution, layer caching, and health probes:
# 1. Official Minimal Base Image
FROM node:20-alpine
# 2. Add curl for health checks
RUN apk add --no-cache curl
# 3. Create isolated application directory with appropriate ownership
WORKDIR /app
# 4. Copy dependency manifests first for maximum layer cache reuse
COPY --chown=node:node package*.json ./
# 5. Clean, deterministic production install (avoids updating package-lock.json)
RUN npm ci --omit=dev && npm cache clean --force
# 6. Copy application source code
COPY --chown=node:node . .
# 7. Switch to non-root user built into the node image
USER node
# 8. Document exposed application port
EXPOSE 3000
# 9. Configure automated daemon healthcheck
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:3000/api/health || exit 1
# 10. Start the server using Exec form for clean SIGTERM signal handling
CMD ["node", "server.js"]Next Up: Building & Publishing Container Images
In Chapter 7, explore BuildKit features, multi-platform builds for ARM64 and AMD64, and publishing to Docker Hub and cloud registries.
Chapter 07: Building & Publishing Images →