Docker for Backend Developers: Production Patterns

· 10 min read · DevOps

Docker production patterns for backend developers including multi-stage builds, security, health checks, caching, secrets, and resource limits.

Docker for Backend Developers: Production Patterns

Docker is not just for packaging applications. In production, it is the foundation for consistent deployments, reproducible environments, and horizontal scaling. But the Dockerfile patterns that work in development often fail in production.

This guide covers the Docker patterns that backend developers need for production deployments. For multi-stage build specifics, see the Docker multi-stage builds guide.

Problem

Production Docker deployments fail because of:

  • Images that are 1-2 GB when they should be 100 MB
  • Containers that run as root
  • Build caches that are never used, causing slow CI
  • No health checks, so orchestrators cannot detect failures
  • Secrets baked into images

Production Dockerfile for Python

# Stage 1: Build dependencies
FROM python:3.12-slim AS builder

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# Stage 2: Production image
FROM python:3.12-slim

# Security: non-root user
RUN useradd --create-home appuser
WORKDIR /home/appuser/app

# Copy only installed packages
COPY --from=builder /install /usr/local
COPY . .

# Security: switch to non-root
USER appuser

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Production Dockerfile for Node.js

FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:20-alpine

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /home/appuser/app

COPY --from=builder /app/node_modules ./node_modules
COPY . .

USER appuser
EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "server.js"]

Docker Compose for Development

services:
  api:
    build:
      context: .
      target: builder
    volumes:
      - .:/app
      - /app/node_modules
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/app
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]

volumes:
  pgdata:

Optimizing Build Cache

Layer ordering determines cache efficiency. Put things that change least at the top:

# GOOD: Dependencies cached separately from source code
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

# BAD: Any source change invalidates the dependency cache
COPY . .
RUN pip install -r requirements.txt

Secrets Management

Never put secrets in the image:

# BAD: Secret baked into the image
ENV DATABASE_URL=postgresql://user:password@host/db

# GOOD: Passed at runtime
# (no ENV for secrets in Dockerfile)
# Pass secrets at runtime
docker run -e DATABASE_URL="postgresql://..." myapp

# Or use Docker secrets
echo "postgresql://..." | docker secret create db_url -

Container Logging

Write logs to stdout/stderr, not files:

import logging
import sys

logging.basicConfig(
    stream=sys.stdout,
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
)

Docker captures stdout/stderr automatically. Log aggregation tools (CloudWatch, Datadog, ELK) collect from the Docker log driver.

Resource Limits

Always set memory and CPU limits:

services:
  api:
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"
        reservations:
          memory: 256M
          cpus: "0.5"

Without limits, one container can consume all host resources and crash others.

Common Mistakes

Mistake 1: Running as root. If a vulnerability allows code execution, the attacker has root access. Always use a non-root user.

Mistake 2: Using :latest tag. In production, always pin versions. python:3.12-slim not python:latest.

Mistake 3: No .dockerignore. Without it, COPY . . includes node_modules, .git, and test files in the image.

Mistake 4: No health check. Orchestrators cannot restart unhealthy containers without health checks.

Takeaways

Production Docker patterns start with multi-stage builds for small images, non-root users for security, and health checks for reliability. Cache dependencies separately from source code. Never bake secrets into images. Set resource limits. These patterns apply whether you deploy to Kubernetes, ECS, or a single server.