Chapter 06 • Image Engineering

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

DirectiveBehavior & Layer ImpactSyntax Example
FROMInitializes a new build stage and sets the parent base image. Must be the first non-comment line.FROM node:20-alpine AS builder
WORKDIRSets working directory for all subsequent directives (RUN, CMD, COPY). Creates directory if missing.WORKDIR /app
COPYCopies files/directories from the local host build context into the image filesystem. Preserves permissions.COPY --chown=node:node package*.json ./
ADDLike COPY, but also automatically extracts local tarballs and fetches remote URLs. Prefer COPY unless auto-extract is needed.ADD assets.tar.gz /app/assets/
RUNExecutes 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/*
ENVSets persistent environment variables accessible both during image build and at container runtime.ENV NODE_ENV=production PORT=8080
ARGDefines ephemeral build-time variables passed via docker build --build-arg. Not persisted in running containers.ARG APP_VERSION=1.0.0
EXPOSEDocuments which network ports the application listens on. Does not publish ports (metadata only).EXPOSE 8080/tcp
USERSwitches execution user and group ID for security, ensuring the container does not run as root.USER nonroot:nonroot
HEALTHCHECKConfigures 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 Linux SIGTERM signal 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 forwardSIGTERM signals to child processes. Your app cannot shut down cleanly and gets forcefully killed via SIGKILL after 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 →

Explore Related Tools

Hand-picked utilities and calculators related to this tool.

Related Tools