Docker has become the standard way to package and deploy applications. But there's a significant difference between a Dockerfile that "works on my machine" and one that's production-ready. This guide covers the practices that matter when your containers are serving real traffic.

Multi-Stage Builds

Multi-stage builds are the single most impactful optimization. They dramatically reduce image size by separating the build environment from the runtime:

# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# Runtime stage
FROM node:20-alpine
WORKDIR /app
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]

The result: your final image contains only what's needed to run the application — no build tools, no source code, no dev dependencies.

Security Essentials

Never Run as Root

By default, containers run as root. This is a security risk — if an attacker escapes the container, they have root access to the host. Always create and switch to a non-root user:

RUN addgroup -S app && adduser -S app -G app
USER app

Use Minimal Base Images

Choose the smallest base image that works for your application:

  • alpine — ~5MB, good for most cases
  • distroless — Google's minimal images, no shell
  • scratch — empty image, for statically compiled binaries (Go, Rust)
# For Go applications — scratch image = ~10MB total
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o server .

FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/server"]

Scan for Vulnerabilities

Integrate image scanning into your CI pipeline. Tools like Trivy, Snyk, or Docker Scout catch known CVEs before deployment:

# In CI pipeline
trivy image --severity HIGH,CRITICAL myapp:latest

Health Checks

Health checks let the orchestrator (Docker Swarm, Kubernetes) know when your container is actually ready to serve traffic:

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
    CMD wget -qO- http://localhost:3000/health || exit 1

Your health endpoint should verify actual dependencies — database connection, cache availability, etc. — not just return 200.

Layer Caching Strategy

Docker builds images layer by layer, caching unchanged layers. Order your Dockerfile instructions from least to most frequently changed:

# 1. Base image (rarely changes)
FROM node:20-alpine

# 2. System dependencies (rarely changes)
RUN apk add --no-cache tini

# 3. App dependencies (changes when packages update)
COPY package*.json ./
RUN npm ci --only=production

# 4. Application code (changes most often)
COPY . .

This way, changing your application code doesn't invalidate the dependency cache.

Resource Limits

Always set memory and CPU limits to prevent a single container from consuming all host resources:

# docker-compose.yml
services:
  api:
    image: myapp:latest
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.50'
        reservations:
          memory: 256M
          cpus: '0.25'

Logging Best Practices

Write logs to stdout/stderr, not to files inside the container. This lets Docker's logging driver handle collection and rotation:

// In your application
const logger = {
    info: (msg, meta) => console.log(JSON.stringify({
        level: 'info', msg, ...meta,
        timestamp: new Date().toISOString()
    })),
    error: (msg, meta) => console.error(JSON.stringify({
        level: 'error', msg, ...meta,
        timestamp: new Date().toISOString()
    }))
};

Structured JSON logs are easier to parse and query in log aggregation systems like ELK, Loki, or Datadog.

Summary

Production-ready Docker images are:

  • Small — multi-stage builds, minimal base images
  • Secure — non-root user, vulnerability scanning, no secrets in layers
  • Observable — health checks, structured logging, metrics
  • Efficient — optimized layer caching, resource limits
  • Reproducible — pinned versions, deterministic builds

Apply these practices consistently, and you'll spend less time debugging container issues in production and more time building features.