Docker Compose Production Deployment: Health Checks and Resource Limits

Docker Compose is often treated as a local development tool, but it is officially supported for production: on a single server or a handful of hosts, Compose can run a maintainable application stack. The key is separating "development configuration" from "production configuration" and adding the production essentials: health checks, resource limits and restart policies.

1. Why Compose Can Be Used in Production

Compose is a single-host multi-container orchestrator. For projects with thousands to tens of thousands of daily active users that do not need horizontal scaling across many nodes, it is far lighter than Kubernetes: no control plane, no steep learning curve, and a single docker compose up -d to go live. When you need cluster-level scheduling, upgrade to Kubernetes instead.

2. Key Changes for Production Configuration

Per the official guidance, do not reuse compose.yaml as-is in production. Put the differences in a separate compose.production.yaml and layer them with -f:

docker compose -f compose.yaml -f compose.production.yaml up -d

Typical production changes include:

  • Remove application-code bind mounts: in production the code should stay inside the image and never be editable from the host.
  • Change port bindings: do not expose databases or Redis to the public internet.
  • Reduce log verbosity: set environment variables to suppress debug output.
  • Set a restart policy: for example restart: always, so a crash does not leave the service down.
  • Add log aggregation: for example JSON logs plus a collector.

A concrete split: in the development compose.yaml, the web service mounts source via bind mount and exposes port 3000 locally; compose.production.yaml carries only the overrides:

# compose.production.yaml
services:
  web:
    image: myapp:1.0.0          # override image instead of build
    ports:
      - "127.0.0.1:8080:8080"   # bind to localhost only; put Nginx in front
    environment:
      - NODE_ENV=production
    restart: always
    logging:
      driver: json-file
      options: { max-size: "10m", max-file: "3" }

On the server, docker compose -f compose.yaml -f compose.production.yaml up -d starts everything with production settings, while dev machines keep using the original file untouched.

3. Health Checks and Startup Order

The short syntax of depends_on only guarantees that a dependency starts first, not that it is ready. In production use the long syntax with healthcheck so the web service truly waits until the database is available:

services:
  web:
    image: myapp:1.0.0
    depends_on:
      db:
        condition: service_healthy
    restart: always

  db:
    image: postgres:16
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "appuser"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

start_period is critical: it gives a container time to warm up, preventing a slow-starting service from being marked unhealthy and restarted repeatedly.

4. Resource Limits

An unbounded container can exhaust the memory of the whole machine. In production, declare CPU and memory limits for every service:

services:
  web:
    image: myapp:1.0.0
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M
    pids_limit: 512
    read_only: true
    tmpfs:
      - /tmp

The read_only plus tmpfs combination makes the filesystem read-only, sharply reducing the ability to write malicious files after a compromise; pids_limit also helps defend against fork-bomb style attacks.

How do you size the limits? There is no universal answer, but a reasonable starting point: run first and watch docker stats for the peaks, then set limits at ~1.5x the peak and reservations at ~80% of the baseline. When a 4GB box has to host a database, an app and Redis at once, remember to count MySQL's innodb_buffer_pool_size and Redis's maxmemory into the total, or container limits and process memory end up fighting each other.

5. The Right Way to Roll Out Updates

docker compose build web
docker compose up --no-deps -d web

Build first, then up --no-deps to avoid recreating dependent services. After the update, verify health:

docker compose ps
docker compose logs --tail=100 web

The point of up --no-deps is rebuilding only the web service. To get closer to zero downtime, boot a staging instance from the new image tag and verify before switching, or rehearse a --rollback. Do not sneak database schema changes into docker compose up: run migrations as a separate step with a backup in place, then update the application code.

6. Security and Secrets

Never hard-code passwords in a Compose file in production. Prefer a .env file with environment-variable injection, and use Docker Secrets for more sensitive credentials:

services:
  web:
    secrets:
      - db_password
secrets:
  db_password:
    file: ./secrets/db_password.txt

For more container security practices see container security best practices, and for configuration and secret management see environment variables and configuration management.

7. Monitoring and Operations

After go-live, wire up monitoring: use monitoring and alerting to track restart counts and resource usage, and follow the backup and restore runbook for data protection. For fresh hosts, see the server initialization script.

Common Pitfalls

  • Forget to back up named volumes: containers can be recreated, volumes cannot. Back one up with docker run --rm -v myvolume:/data -v $PWD:/backup alpine tar czf /backup/vol.tgz -C /data ..
  • Commit .env to the repo: .env usually holds passwords; add it to .gitignore and route secrets through Docker Secrets or a dedicated secret manager.
  • Expose every port: binding database or Redis ports to 0.0.0.0 hands internal services to the public internet — a prime target for scanners.
  • Ignore log rotation: without max-size, log files grow until they fill the disk; one of the most common production incidents.

16IDC Note

Productionizing Compose is really about turning development convenience into operational discipline: health checks make deployments verifiable, resource limits let one machine safely host many applications, and restart policies let failures self-heal. For small and mid-sized projects this is the crucial step from "it runs" to "it runs reliably", and a natural stepping stone before migrating to Kubernetes later.

Reference: Docker Compose production guide https://docs.docker.com/compose/how-tos/production/ ; Compose file reference https://docs.docker.com/reference/compose-file/services/ ; Backing up volumes https://docs.docker.com/storage/volumes/#back-up-restore-or-migrate-data-volumes

Source: https://docs.docker.com/compose/how-tos/production/ , https://docs.docker.com/reference/compose-file/services/