DEV Community

Prince
Prince

Posted on

Docker Compose in Production on a Single VPS: A Practical Checklist

Kubernetes gets most of the attention, but a large share of real production workloads run on one or two virtual machines with Docker Compose. For a SaaS in its first years, an internal tool, or a content site, a single well configured VPS is cheaper, easier to reason about, and fast to recover.

The catch is that the Compose file that works on a laptop is not the Compose file you want in production. This checklist covers the changes that matter most, in roughly the order I apply them.

1. Pin image versions

image: postgres:latest is a time bomb. The next docker compose pull can bring a new major version and a data format your volume does not support.

services:
  db:
    image: postgres:16.4-alpine
Enter fullscreen mode Exit fullscreen mode

Pin to a specific minor or patch version, and upgrade deliberately. For your own images, tag with the Git SHA or a release version rather than latest, so you always know what is running and can roll back to a known tag.

2. Always set a restart policy

Containers crash, and hosts reboot for kernel updates. Without a restart policy, a reboot means downtime until someone notices.

    restart: unless-stopped
Enter fullscreen mode Exit fullscreen mode

unless-stopped survives reboots but respects a manual docker compose stop, which is usually what you want.

3. Add health checks and use them

A running container is not the same as a healthy service. Health checks let Docker know when your app is actually ready, and depends_on can wait for them.

  api:
    build: .
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 20s
  db:
    image: postgres:16.4-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 10s
      retries: 5
Enter fullscreen mode Exit fullscreen mode

Keep the health endpoint cheap. It should confirm the process can serve requests and reach critical dependencies, not run a full test suite.

4. Keep secrets out of the Compose file

Hardcoded passwords in docker-compose.yml end up in Git history. Use an .env file that is excluded from version control, or Docker secrets for more sensitive values.

  api:
    env_file: .env.production
Enter fullscreen mode Exit fullscreen mode

Set file permissions to 600, and keep a separate env file per environment. Rotate anything that was ever committed, even briefly.

5. Do not publish database ports

A common mistake is ports: - "5432:5432" on the database service. On a public VPS, that exposes Postgres to the internet, and Docker's iptables rules can bypass host firewalls like UFW.

Services on the same Compose network can already reach each other by name. Only publish the ports your reverse proxy needs, and bind them to localhost if the proxy runs on the host:

    ports:
      - "127.0.0.1:3000:3000"
Enter fullscreen mode Exit fullscreen mode

6. Put a reverse proxy in front

You want TLS, HTTP to HTTPS redirects, and the ability to run several apps on one machine. Caddy and Traefik both handle Let's Encrypt certificates automatically. Caddy wins on simplicity; Traefik wins when you want routing driven by container labels.

Either way, the proxy is the only thing that should listen on ports 80 and 443.

7. Use named volumes and back them up

Named volumes survive container recreation and are easy to find:

volumes:
  pgdata:
services:
  db:
    volumes:
      - pgdata:/var/lib/postgresql/data
Enter fullscreen mode Exit fullscreen mode

A volume is not a backup. Schedule logical dumps (pg_dump) to object storage such as S3 or a compatible provider, keep several days of history, and test a restore at least once a quarter. Untested backups are a hope, not a plan.

8. Cap log size

Docker's default json-file driver grows without limit. A chatty container can fill the disk and take down everything on the host.

x-logging: &default-logging
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

services:
  api:
    logging: *default-logging
Enter fullscreen mode Exit fullscreen mode

Using a YAML anchor keeps the setting consistent across services. If you need search and retention, ship logs to a central store instead of keeping them on the box.

9. Set resource limits

One runaway process should not starve the rest. Compose supports memory and CPU limits under deploy.resources, which modern Docker Compose applies outside Swarm as well:

    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"
Enter fullscreen mode Exit fullscreen mode

Start generous, watch real usage for a week, then tighten.

10. Plan how you deploy updates

docker compose up -d --build on the server works, but it rebuilds on the production machine and briefly stops the old container before the new one is ready. Better options, from simplest to most robust:

  • Build images in CI, push to a registry, and only pull and up -d on the server.
  • Run two instances behind the proxy and replace them one at a time.
  • Use a blue/green setup where the proxy switches traffic only after the new version passes its health check.

The last two are the basis of zero downtime deployments with Docker, and they depend on the health checks from step 3.

11. Harden the host

Compose settings do not help if the host is weak. At minimum:

  • Disable SSH password login and use keys.
  • Enable unattended security updates.
  • Allow only ports 22, 80, and 443 at the firewall or provider level.
  • Run containers as a non-root user where the image supports it.
  • Keep enough free disk: prune old images with docker image prune on a schedule.

12. Monitor from the outside

Internal health checks tell Docker what is wrong. External monitoring tells you. A simple uptime checker hitting your public URL every minute, plus alerts on disk usage and memory, catches most incidents before users report them.

Putting it together

None of these steps is complicated, but skipping any one of them is how a small outage turns into a long night. The order above also works as a review checklist for an existing server: go through it top to bottom and fix whatever is missing.

If you want a longer walkthrough with a complete example file, this production Docker Compose guide goes deeper on env handling, networking, and deployment workflow.

A single VPS with a disciplined Compose setup will carry you further than most people expect. Move to an orchestrator when you genuinely need multi-node scheduling, not because the Compose file on your laptop was never production ready.

Top comments (0)