Running Docker Compose in Production on a Single VPS
Practice4 minInnovialabs editorial team
In short
Docker Compose on a single VPS is fine for production if you set restart policies, healthchecks, log rotation, HTTPS via a reverse proxy, regular off-site volume backups and a clear update procedure.
Is Compose fit for production
Yes. For small and medium projects, one VPS with Docker Compose is a simple and reliable setup: the whole infrastructure lives in one file that is easy to reproduce and move. The key is not to run it "like on a laptop" but to cover a few mandatory points.
Restart policy
Without it, a container will not come back after a crash or a server reboot.
services:
app:
image: registry.example.com/app:1.4.2
restart: unless-stopped
unless-stopped: always restart unless you stopped the container manually. A good default.always: also brings back containers you stopped by hand once Docker restarts.on-failure: only on a non-zero exit code.
Also make sure Docker itself starts on boot: systemctl enable docker.
Secrets and env files
Keep passwords out of docker-compose.yml and out of Git. Move them to .env or a separate file:
services:
app:
env_file: .env.production
The secrets file gets 600 permissions and goes into .gitignore. Keep a .env.example with empty values in the repository so everyone sees which variables are required.
Healthcheck
restart only reacts to a dead process. If the app hangs while the process is alive, a healthcheck helps:
services:
app:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
worker:
depends_on:
db:
condition: service_healthy
Note that plain Docker without Swarm does not restart a container just because it is unhealthy. The status is for depends_on, monitoring and your own scripts. curl must exist in the image, or use another check.
Log rotation
By default the json-file driver writes logs without a limit, and one day the disk fills up. Set limits:
x-logging: &logging
driver: json-file
options:
max-size: "10m"
max-file: "5"
services:
app:
logging: *logging
You can set the same globally in /etc/docker/daemon.json, but it only applies to newly created containers.
Reverse proxy and HTTPS
Do not publish the app port to the internet. Only ports 80 and 443 of the reverse proxy face outward; the app is reachable only on the internal Compose network.
Options:
| Proxy | Why it is convenient |
|---|---|
| Caddy | obtains and renews Let's Encrypt certificates automatically, minimal config |
| Traefik | configured through container labels, discovers services itself |
| Nginx + certbot | familiar and flexible, but renewal is set up separately |
If you need a port for debugging, bind it to localhost: "127.0.0.1:3000:3000". Remember that Docker writes its own iptables rules, and a published port can bypass UFW.
Updates without long downtime
Use specific image tags instead of latest, so you know what is running and can roll back easily.
Update sequence:
docker compose pull app
docker compose up -d app
docker image prune -f
The new image is downloaded while the old container keeps running, then the container is recreated, so downtime shrinks to the app's startup time. If even that matters, run two copies behind the proxy and update them one by one. Keep database migrations backward compatible so the old and new code can work with the same schema.
Volume backups
Data lives in volumes, and they will not survive a dead disk or server.
- Back up databases with native tools (
pg_dump,mysqldump), not by copying files of a running database. - Archive files from volumes and ship them off the server, to object storage or another machine.
- Schedule backups (cron or a systemd timer) and watch for errors.
- Test a restore on a clean server from time to time: an untested backup is a hope, not a backup.
When one server is not enough
Consider multiple servers or an orchestrator when:
- downtime during maintenance or hardware failure is no longer acceptable;
- vertical scaling of the VPS cannot keep up with load;
- individual services need to scale independently;
- the team ships often and needs canary releases.
A middle step is to move the database to a managed service and run the app on two servers behind a load balancer.