"Compose isn't for production" is one of those statements that's repeated more than it's examined. For a single-host deployment — which describes a large share of real-world web applications — Compose with a few specific additions is genuinely solid. Kubernetes solves problems that most projects don't have yet, at a cost of complexity that most teams underestimate.

When Compose is the right call

If your application fits on one host, or you're comfortable with a small number of hosts and manual failover, Compose is a reasonable production tool. Where it falls apart is multi-host orchestration, automatic failover, and complex scaling rules — at that point you actually need Kubernetes or Nomad, and no amount of Compose tuning will get you there.

For everything before that line, here's what makes the difference between a fragile setup and one that survives a server reboot at 3am without you.

Restart policies that actually recover

The default Compose restart policy is "no restart at all," which is almost never what you want in production. unless-stopped is the one to reach for — it restarts on crash and on host reboot, but respects an explicit docker compose stop.

docker-compose.yml

services:
  api:
    image: registry.example.com/api:1.4.2
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"

That memory limit matters more than people expect. Without it, a memory leak in one container can take down everything else on the host through OOM pressure, rather than just getting killed itself.

Health checks, not just running containers

A container can be "running" and completely unable to serve traffic — stuck behind a database connection that never resolved, or waiting on a dependency that crashed. Docker's default view of "running" doesn't capture that. A health check does.

docker-compose.yml

services:
  api:
    healthcheck:
      test: ["CMD", "curl", -f, "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s

Pair this with depends_on using the condition: service_healthy form, so your API container doesn't start accepting traffic before its database connection pool is actually ready:

services:
  api:
    depends_on:
      db:
        condition: service_healthy

Handling secrets without committing them

The number of public repos with a database password sitting in a committed docker-compose.yml is higher than it should be. Compose supports a secrets block that reads from files outside version control, which costs almost nothing to set up.

docker-compose.yml

services:
  db:
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt
!

Add secrets/ to .gitignore before you create the file, not after. It's a small habit that's prevented at least two close calls for me personally.

Logging and log rotation

Docker's default logging driver keeps every line of container output on disk forever unless told otherwise. On a long-lived server, that's a slow-motion disk space incident. Set rotation explicitly:

services:
  api:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Updating without downtime

For a single-replica service, a plain docker compose up -d means a brief gap while the old container stops and the new one starts. For most internal tools that's fine. For anything customer-facing, running two replicas behind a lightweight reverse proxy like Caddy or Traefik, combined with the health check from above, gets you a rolling update without needing a full orchestrator:

services:
  api:
    deploy:
      replicas: 2

Traefik picks up healthy containers automatically via labels, so a rolling deploy script just needs to update one container at a time and wait for its health check to pass before moving to the next. It's not as elegant as a Kubernetes rolling update, but it's a fraction of the operational overhead for a team that doesn't need the rest of what Kubernetes offers.

ML

Mikko Laaksonen

DevOps engineer based in Tampere, Finland. Writes about Linux, Docker, databases, and web design.

More about Mikko →