Docker Compose in Production: Node.js, PostgreSQL, and Backups
Build a safer Docker Compose production stack for Node.js and PostgreSQL with health checks, private networks, volumes, secrets, log rotation, backups, and rollback.
Article by Phạm Minh Thiện
Front-end developer working directly with Next.js, NestJS, Docker, Nginx, and Ubuntu VPS deployments.
Reviewed on 7/21/2026.
Table of contents
Docker Compose is useful for a small VPS, but a production Compose file needs more than docker compose up. The application, database, storage, logs, health checks, and recovery plan should be designed together.
A safer Compose baseline
services:
api:
image: registry.example.com/myapp:${APP_VERSION}
restart: unless-stopped
env_file: .env.production
depends_on:
db:
condition: service_healthy
networks: [internal]
ports:
- "127.0.0.1:3000:3000"
healthcheck:
test: ["CMD", "wget", "--spider", "--quiet", "http://127.0.0.1:3000/health"]
interval: 30s
timeout: 5s
retries: 3
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: app
POSTGRES_USER: app_user
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
networks: [internal]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app_user -d app"]
interval: 10s
timeout: 5s
retries: 5
networks:
internal:
volumes:
postgres_data:
The exact health command depends on the image. Keep it lightweight and make sure the image actually contains the command you call.
Keep the database private
Do not publish PostgreSQL’s 5432 port to the internet unless an external client genuinely needs it and access is restricted. The API can reach db:5432 over the internal Compose network.
Treat volumes as data, not backups
The named volume keeps the database across container replacement. It is not a backup. Back up the logical database and important files to storage outside the VPS or provider account:
docker compose exec -T db pg_dump -U app_user -d app > /var/backups/app-$(date +%F).sql
Encrypt backups where appropriate, restrict their permissions, and test restoring them on an isolated instance.
Handle secrets safely
Do not commit .env.production, passwords, or API tokens. Use a secret manager, encrypted CI variables, or a protected file deployed out of band:
chmod 600 .env.production
Remember that docker compose config and process inspection can expose interpolated secrets.
Rotate logs and monitor disk
An application that logs indefinitely can fill a VPS. Configure Docker’s logging driver or use a central collector. At minimum, monitor disk and inode usage:
docker system df
df -h
df -i
Avoid pruning images or deleting volumes automatically without understanding what the deployment needs.
Deploy an immutable image
Build and scan an image in CI, tag it with a commit SHA, push it to a registry, and deploy that exact tag:
docker compose pull api
docker compose up -d --no-build api
docker compose ps
Avoid using only latest when you need to identify or roll back a release.
Migrations and rollback
Run database migrations as an explicit release step. Prefer backward-compatible changes when old and new application instances may overlap.
docker compose run --rm api npm run migrate
docker compose up -d api
curl --fail http://127.0.0.1:3000/health
If the new image fails, return to the previous immutable tag and inspect the failed migration. A database rollback may require a backup restore; it is not always equivalent to rolling back application code.
Production checklist
- The database has a persistent volume and an off-host backup.
- Backup restore has been tested.
- Only the reverse proxy is publicly exposed.
- Services restart after a host reboot.
- Health checks represent useful readiness.
- Logs have retention limits and alerts.
- Secrets are outside Git and protected on disk.
- Images have immutable release tags.
- Deployment and rollback commands are documented.
Continue reading
Related articles
Docker Compose Ignores .env and PostgreSQL Says Password Authentication Failed
Understand .env, env_file, and environment in Docker Compose, inspect variables inside a container, and fix PostgreSQL passwords when a volume already exists.
Read article →Docker Port Is Already Allocated: Find the Process Using a Port
Fix Docker port is already allocated, address already in use, and EADDRINUSE errors on Windows and Linux without stopping the wrong process.
Read article →How to Fix Docker Exit Code 137 on a VPS
Diagnose OOM kills, RAM pressure, swap, and heavy Docker builds before changing a small VPS or restarting production services.
Read article →