Skip to content
Home
Docker Compose for Production: Tips and Best Practices

Docker Compose for Production: Tips and Best Practices

DevOps DevOps 8 min read 1511 words Beginner ExcellentWiki Editorial Team

Docker Compose is best known as a development tool — docker compose up starts your entire stack with one command. But Compose is also capable of running in production, especially for small to medium deployments where a full Kubernetes cluster is overkill.

This guide covers production-hardening techniques for Docker Compose: multi-stage builds, health checks, logging drivers, secret management, resource constraints, and orchestration patterns with Docker Swarm.

Multi-Stage Builds

Multi-stage builds produce smaller, more secure images by separating build environment from runtime:

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Runtime
FROM nginx:stable-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

The final image contains only the nginx runtime and static files — no Node.js, no source code, no dev dependencies. This reduces the attack surface and image size from ~500 MB to ~25 MB.

Multi-Stage with Build Arguments

ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine AS builder
ARG APP_ENV=production
RUN echo "Building for $APP_ENV"

FROM node:${NODE_VERSION}-alpine AS runtime
COPY --from=builder /app/dist /app
CMD ["node", "/app/server.js"]

Override build args in compose.yaml:

services:
  app:
    build:
      context: .
      args:
        NODE_VERSION: 22
        APP_ENV: staging

Health Checks

Health checks tell Docker when a container is truly ready. Unlike the process-based check (is the process running?), a health check tests application-level readiness:

services:
  api:
    build: .
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

Dependent Service Startup

Health checks enable ordered startup with depends_on:

services:
  database:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  api:
    build: .
    depends_on:
      database:
        condition: service_healthy

Without the health check condition, depends_on only waits for the container process to start — which happens before the database is ready to accept connections. The service_healthy condition waits until the health check passes.

Logging Configuration

Structured Logging

Configure your application to emit JSON logs, then use Docker’s logging drivers:

services:
  app:
    build: .
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Centralized Logging with Loki

For production, forward logs to a centralized system:

services:
  app:
    build: .
    logging:
      driver: "loki"
      options:
        loki-url: "http://loki:3100/loki/api/v1/push"
        loki-retries: "2"
        loki-max-backoff: "100ms"

  loki:
    image: grafana/loki:3.0
    ports:
      - "3100:3100"

  promtail:
    image: grafana/promtail:3.0
    volumes:
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - ./promtail-config.yml:/etc/promtail/config.yml

The json-file driver with rotation prevents disks from filling. For structured querying, aggregate logs with Loki + Grafana or the ELK stack.

Secrets Management

Never hardcode secrets in compose.yaml or Dockerfiles. Docker Compose supports secrets natively:

services:
  app:
    build: .
    secrets:
      - db_password
      - api_key

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    environment: "API_KEY"

Inside the container, secrets appear as files at /run/secrets/<name>:

# app.py
with open("/run/secrets/db_password") as f:
    db_password = f.read().strip()

For Docker Swarm mode, secrets are encrypted during transit and at rest. Always add secrets/ to .gitignore.

Resource Constraints

Limit CPU and memory per service to prevent one container from starving others:

services:
  app:
    build: .
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: "512M"
        reservations:
          cpus: "0.25"
          memory: "256M"

  worker:
    build: .
    deploy:
      resources:
        limits:
          memory: "1G"

When a container exceeds its memory limit, Docker kills it with an OOM error. CPU limits throttle the container at the kernel level. Set reservations for guaranteed minimum resources and limits for hard caps.

OOM Priority

services:
  database:
    image: postgres:16
    deploy:
      resources:
        limits:
          memory: "2G"
    oom_kill_disable: true
    oom_score_adj: -500

oom_kill_disable: true prevents the database from being killed under memory pressure. oom_score_adj adjusts the OOM killer priority — lower values are less likely to be killed. Use this carefully for critical infrastructure services only.

Networking

Custom Networks

services:
  api:
    build: .
    networks:
      - frontend
      - backend

  database:
    image: postgres:16
    networks:
      - backend
    expose:
      - "5432"

  nginx:
    image: nginx:stable-alpine
    networks:
      - frontend
    ports:
      - "80:80"

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # No external access

The internal: true network blocks external access — the database is only reachable by services on the same network. This is defense in depth: even if the API is compromised, the database port is not directly exposed.

DNS and Service Discovery

Compose provides built-in DNS resolution. Services reach each other by service name:

services:
  api:
    build: .
    environment:
      DATABASE_URL: "postgres://user:password@database:5432/db"

No need to hardcode IP addresses. Compose automatically registers service names in an internal DNS.

Orchestration with Docker Swarm

For multi-node production deployments, Docker Swarm provides native orchestration:

version: "3.8"
services:
  app:
    image: myapp:latest
    deploy:
      mode: replicated
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first
      restart_policy:
        condition: on-failure
        max_attempts: 3
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]

Deploy with:

docker stack deploy -c compose.yaml myapp

Swarm handles rolling updates, load balancing, and node failure. The start-first update order starts a new container before stopping the old one, ensuring zero-downtime deployments.

Configs for Shared Configuration

services:
  app:
    image: myapp:latest
    configs:
      - source: app_config
        target: /etc/app/config.yml

configs:
  app_config:
    file: ./config.yml

Unlike secrets, configs are stored unencrypted and are useful for non-sensitive configuration files like nginx configs or application settings.

Backup and Recovery

Database Backups

services:
  db-backup:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data
    entrypoint: |
      sh -c '
      while true; do
        pg_dump -h database -U postgres mydb > /backups/db-$(date +%Y%m%d).sql
        sleep 86400
      done'
    depends_on:
      database:
        condition: service_healthy

Run backups as a separate service, not a cron job inside the database container. This keeps the database image focused and allows backup tools to be updated independently.

Best Practices

  • Pin image tags to specific versions — Never use :latest in production. Use postgres:16.4 or specific SHAs.
  • Use read-only root filesystemsread_only: true prevents writes to the container filesystem. Mount tmpfs for ephemeral writes.
  • Set user directives — Avoid running as root: user: "1000:1000" with matching UID/GID.
  • Enable restart policiesrestart: unless-stopped for daemon services.
  • Scan images for vulnerabilities — Use docker scout or Trivy in your CI pipeline.
  • Use .dockerignore — Exclude node_modules, .git, and build artifacts to keep build context small.

Summary

Docker Compose is production-capable when configured correctly. Multi-stage builds produce minimal images. Health checks ensure reliable service startup order. Logging drivers and centralized log aggregation keep operations manageable. Secrets and network isolation reduce the attack surface. Resource constraints prevent noisy neighbors. For multi-node deployments, Docker Swarm adds orchestration without leaving the Compose ecosystem.

Production Docker Compose Patterns

Multi-Environment Configuration

Docker Compose supports multiple configuration files for different environments. The base docker-compose.yml defines shared service definitions, while environment-specific files override settings. A production compose file (docker-compose.prod.yml) might set resource limits, enable health checks, configure log rotation, and specify production environment variables. The override mechanism uses docker compose -f docker-compose.yml -f docker-compose.prod.yml up. YAML anchors and extensions reduce duplication across services — a logging configuration anchor can be referenced by all services using <<: *logging-config. Environment variables are sourced from .env files, with separate files for each environment and secrets managed through Docker secrets or external vaults.

Health Checks and Self-Healing

Docker Compose supports health checks that monitor container health using commands like curl, wget, or application-specific endpoints. The healthcheck directive specifies the test command, interval, timeout, retries, and start period. When a service depends on another, condition: service_healthy ensures the dependency is fully ready before starting dependent services. Docker Compose’s restart policies — always, unless-stopped, on-failure — provide automatic recovery from crashes. For more sophisticated orchestration, consider Docker Swarm (built into Docker) or multi-node configurations. In production, health checks combined with reverse proxy load balancers (Traefik, Nginx) enable zero-downtime deployments through rolling updates with controlled service scaling.

Networking and Security

Docker Compose creates a default bridge network for all services in the compose file, enabling DNS-based service discovery. Production deployments should define explicit networks and isolate sensitive services. An internal network for backend services (database, cache) should have no external access, while a public-facing network connects the reverse proxy to the outside. This network segmentation limits the blast radius if a service is compromised. Secrets management in production requires avoiding environment variables for sensitive data — use Docker secrets (available in Swarm mode) or mount encrypted volumes. The security_opt and cap_drop directives reduce container privileges: cap_drop: ALL followed by cap_add for only required capabilities implements the principle of least privilege for each container.

Frequently Asked Questions

Is Docker Compose production-ready? Docker Compose is suitable for small-scale production deployments with simple orchestration needs — single server or small cluster, low traffic volume, and acceptable downtime during maintenance. For high-availability, auto-scaling, or zero-downtime deployments, Kubernetes or Docker Swarm provides the necessary orchestration capabilities.

How do I handle secrets in Docker Compose? For Docker Compose in Swarm mode, use Docker secrets which are encrypted at rest and in transit. For standalone Compose, use environment variables from secure sources, external secret management tools (HashiCorp Vault, AWS Secrets Manager), or encrypted .env files. Never commit secrets to version control.

How do I scale services with Docker Compose? Docker Compose supports deploy.replicas in version 3+ files when used with Docker Swarm. For single-node deployments, use multiple compose service definitions with different ports or container names. For true horizontal scaling across machines, migrate to Docker Swarm or Kubernetes.

For a comprehensive overview, read our article on Blue Green Deployments.

For a comprehensive overview, read our article on Ci Cd Pipeline Guide.

Section: DevOps 1511 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top