Docker Compose isn’t just for development: on a single VPS (or a few), it’s a simple, solid way to run production apps. But a compose.yaml that works on a laptop needs a few additions before it runs reliably for months. Here’s a production checklist.

Basics: Docker Compose tutorial.

A production-ready example

# /opt/app/compose.yaml
services:
  app:
    image: registry.example.com/myapp:1.8.3          # pinned version, never :latest
    restart: unless-stopped
    env_file: .env
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "127.0.0.1:8080:8080"                         # only reachable via the reverse proxy
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
    deploy:
      resources:
        limits:
          cpus: "1.5"
          memory: 768M
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"

  db:
    image: postgres:16.4
    restart: unless-stopped
    env_file: .env.db
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
      interval: 10s
      retries: 5
    deploy:
      resources:
        limits:
          memory: 1G
    logging:
      driver: json-file
      options: { max-size: "10m", max-file: "5" }

volumes:
  pgdata:

The checklist

1. Pin image versions. postgres:16.4, or at least postgres:16, never latest, so a pull can’t silently jump a major version. See package pinning.

2. Restart policies. restart: unless-stopped brings containers back after crashes and reboots. Make sure Docker itself starts at boot: systemctl enable docker.

3. Healthchecks + depends_on. Containers wait for dependencies to be healthy, and docker ps shows unhealthy services.

4. Resource limits. Stop one container from starving the server. See Docker CPU and memory limits.

5. Don’t publish ports publicly. Bind to 127.0.0.1, and put a reverse proxy (Nginx, Caddy, Traefik) with HTTPS in front. Docker-published ports bypass UFW rules. See Docker and UFW and reverse proxy for multiple containers.

6. Secrets outside the compose file. Use .env files with chmod 600 (not in Git), or Docker/Compose secrets files. See Docker secrets and secrets management.

7. Limit logs. The default json-file driver grows forever. Set max-size/max-file per service, or globally in /etc/docker/daemon.json. See Docker logging drivers.

8. Named volumes or bind mounts for data, and back them up. See Docker volume backups.

9. Run as non-root where possible (user: "1000:1000"), with read-only filesystems for stateless apps (read_only: true + tmpfs for /tmp), and cap_drop: [ALL]. See Linux capabilities.

10. Keep the project in Git (compose file, configs, without secrets), so the deployment is reproducible.

Deploying updates

cd /opt/app
docker compose pull
docker compose up -d --remove-orphans
docker image prune -f
  • up -d recreates only containers whose image or config changed.
  • Expect a few seconds of downtime per recreated container. For zero-downtime, run two app replicas behind the proxy and update one at a time, or use a tool such as docker-rollout, or Swarm/Kubernetes. See Docker Swarm.
  • Automate with CI (GitHub Actions over SSH), rather than editing on the server. See GitHub Actions deploy to VPS.

Automatic updaters (Watchtower) are convenient for hobby setups. For production, deliberate, tested updates are safer. See Watchtower.

Monitoring

  • docker compose ps, docker stats, docker compose logs -f --tail=100 app.
  • External uptime checks against the public URL. See Uptime Kuma.
  • Disk usage of /var/lib/docker. See Docker disk cleanup.

Start on boot

With restart: unless-stopped, containers start automatically when Docker starts. No extra systemd unit is needed, unless you want docker compose up to run at boot for other reasons.

Quick reference

Item Setting
Versions Pinned tags
Recovery restart: unless-stopped + healthchecks
Limits deploy.resources.limits
Ports 127.0.0.1:host:container + reverse proxy
Logs max-size, max-file
Update pull → up -d → image prune

Related: Docker VPS hosting and Coolify.

  • Docker
  • DevOps
  • Production