Docker in Production: Multi-Stage Builds and Container Security

Docker is ubiquitous in modern software development, but using it correctly in production requires understanding several patterns that are not obvious from the introductory tutorials. Multi-stage builds, image security, and runtime configuration are where most teams that use Docker casually go wrong.

Multi-Stage Builds

The problem with single-stage Docker builds: every tool you need to build your application (compilers, build dependencies, test frameworks, development utilities) ends up in the final image — often adding hundreds of megabytes to an image that only needs the compiled output. Multi-stage builds solve this by separating the build environment from the runtime environment:
“`dockerfile
# Stage 1: Builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci –only=production=false
COPY . .
RUN npm run build

# Stage 2: Runtime
FROM node:20-alpine AS runtime
WORKDIR /app
COPY –from=builder /app/dist ./dist
COPY –from=builder /app/node_modules ./node_modules
EXPOSE 3000
USER node
CMD [“node”, “dist/index.js”]
“`
The final image only contains what the runtime stage copies — build tools, test dependencies, and source files are not included. A typical Node.js app that might be 800MB as a single-stage build can be 150MB with multi-stage. The `–from=builder` directive copies from a named stage. You can also copy from arbitrary Docker images (`–from=nginx:latest`) to pull in specific files without inheriting the full image. The `.dockerignore` file: equivalent to `.gitignore` but for Docker build context — always include `node_modules`, `.git`, `*.md`, test files, and other build artifacts that should not be sent to the Docker daemon. Sending a large build context is the most common cause of slow Docker builds.

Container Security

Running as a non-root user: by default, containers run as root — a significant security risk if the container is compromised (root in the container may map to root on the host). Always switch to a non-root user in production images. For Node.js, the official image includes a `node` user:
“`dockerfile
USER node
“`
For custom users: `RUN addgroup -S appgroup && adduser -S appuser -G appgroup`. Minimal base images: prefer `alpine`-based images (5MB) or `distroless` images (Google’s images with no shell, package manager, or other tools — only the application and runtime). A distroless Python image can be under 50MB; standard Python images are 1GB+. Secrets and environment variables: never bake secrets into images with `ENV` or `ARG` — they are visible in the image layers and in `docker inspect`. Use Docker secrets (for Swarm), Kubernetes secrets, or secret injection at runtime via your orchestrator. Scanning images: `docker scout` (built into Docker Desktop), `trivy`, or `snyk` scan images for known CVEs in base images and dependencies. Run scans in CI pipelines before pushing to registries. Read-only file system: `docker run –read-only` prevents any writes to the container filesystem (mount tmpfs for directories that need write access like /tmp). Reduces attack surface significantly. Resource limits: always set CPU and memory limits in production — a container without limits can consume all host resources:
“`yaml
# Docker Compose
deploy:
resources:
limits:
cpus: ‘0.5’
memory: 512M
“`
Health checks: define `HEALTHCHECK` in your Dockerfile or `healthcheck` in Compose — orchestrators use this to determine when a container is ready and when to restart it:
“`dockerfile
HEALTHCHECK –interval=30s –timeout=3s CMD wget -qO- http://localhost:3000/health || exit 1
“`

上一篇 ggplot2 科研配色方案:5 套可直接用的代码
下一篇 Multi-Agent AI Systems: Architectures and Design Patterns