A production Docker Compose checklist for VPS deployments
Turn a development Compose file into a predictable production stack with pinned images, health checks, limits, secrets, logging, and recoverable data.

Docker Compose is perfectly capable of running a small production application. The danger is shipping the development file unchanged: floating tags, public databases, no restart policy, and volumes nobody has tested restoring.
Use this checklist before pointing DNS at the server.
Pin what you deploy
Avoid latest. Pin a version or, for maximum repeatability, an image digest:
services:
app:
image: ghcr.io/example/app:2.4.1
restart: unless-stopped
Record the previous image so rollback is one edit and docker compose up -d away.
Keep internal services internal
Only the reverse proxy needs public ports. Use expose for app-to-app traffic:
services:
app:
expose: ["3000"]
db:
image: postgres:17
expose: ["5432"]
proxy:
ports:
- "80:80"
- "443:443"
Publishing Postgres or Redis to 0.0.0.0 is rarely justified.
Add health checks
Container “running” does not mean application “ready.”
services:
app:
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
The health endpoint should verify essential dependencies without performing expensive work.
Bound resource use
One runaway service should not starve SSH and the reverse proxy:
services:
worker:
mem_limit: 1g
cpus: 1.5
Leave headroom for the kernel, filesystem cache, and Docker itself. On a 4 GB VPS, assigning 4 GB across hard limits is not headroom.
Handle secrets outside Git
Use files readable only by root, a secret manager, or Compose secrets where supported by the image. At minimum, keep .env.production out of the repository:
sudo chown root:root .env.production
sudo chmod 600 .env.production
Never put credentials directly in compose.yml or bake them into the image.
Rotate logs
Set a default logging policy:
x-logging: &default-logging
driver: json-file
options:
max-size: "20m"
max-file: "5"
services:
app:
logging: *default-logging
Without limits, a noisy container can fill the root disk.
Make state recoverable
Name every persistent volume, document its purpose, and back up the data using the application's supported method. A tar archive of a live database directory is not a reliable database backup.
Before launch, rebuild the stack on a disposable VPS using only the Compose file, environment secrets, and backups. Then run:
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose ps
If that rehearsal works, the production server is replaceable. That is the real benefit of Compose.
Separate deploy and data operations
Keep the Compose file, environment template, and backup scripts under version control, but keep secrets and live volumes on the server. A deploy should be repeatable:
git fetch --tags
git checkout v2.4.1
docker compose config --quiet
docker compose pull
docker compose up -d --remove-orphans
docker compose ps
Do not add --volumes to a normal cleanup or deployment command. Volumes are where databases and uploads usually live.
Keep the previous image tag documented. If a health check fails, inspect logs, switch the tag back, and recreate only the affected service. Add a smoke test after every deploy:
curl --fail https://app.example.com/health
curl --fail https://app.example.com/api/version
A Compose file is an operations contract. If a new host cannot be rebuilt from it and the documented backups, the stack is not yet portable.
Continue reading