Keeping Postgres Data Across Docker Deploys, with Startup Order and Domain Routing
- Author
- Vishal Maurya
- Published on
- Reading time
- 4 min read
Overview
Two bad surprises hit people deploying a database with Docker Compose. First, after a redeploy the database is empty. Second, the app crashes on startup because Postgres was not ready yet. Add a domain that should point at the right container, and the first deployment becomes a series of small failures.
This guide fixes each in turn.
1. Why the Data Disappeared
A container's filesystem is discarded when the container is removed. The official postgres image stores data in /var/lib/postgresql/data (check the image documentation if you use a recent major version, as the layout can differ). If that path is not backed by a volume, recreating the container loses everything.
Use a named volume.
services:
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
secrets:
- db_password
volumes:
pgdata:
secrets:
db_password:
file: ./secrets/db_password.txt
A named volume survives docker compose down and docker compose up. It is deleted by docker compose down -v and by docker volume prune, so never add -v to a deploy script. Also pin the major version: changing postgres:16 to a newer major tag does not upgrade the data directory and will refuse to start.
2. Initialization Runs Only Once
The POSTGRES_USER, POSTGRES_PASSWORD and POSTGRES_DB variables, and scripts in /docker-entrypoint-initdb.d, apply only when the data directory is empty. If you change the password in compose later, the existing database keeps the old one. Change it with ALTER USER inside the database instead.
3. Make the App Wait for a Healthy Database
depends_on alone only waits for the container to start, not for Postgres to accept connections. Combine it with a healthcheck.
db:
# ...
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 5s
timeout: 3s
retries: 10
api:
image: ghcr.io/your-user/api:1.4.2
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgresql://app@db:5432/appdb
Inside the Compose network, the hostname is the service name, db, not localhost. Still make the application retry its connection on startup, because the database can restart later while the app is running.
4. Route a Domain with Caddy
Keep the database off the public internet: do not publish port 5432. Publish only the reverse proxy and let it reach the app over the internal network.
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- api
volumes:
caddy_data:
caddy_config:
# Caddyfile
api.example.com {
reverse_proxy api:8000
}
Point the DNS A record of api.example.com at the server before starting, so Caddy can obtain a certificate automatically. Persist caddy_data, otherwise certificates are requested again on each recreate, which can hit rate limits. With Nginx the same idea applies: use proxy_pass http://api:8000; and handle certificates separately.
5. Back Up the Database
A volume protects against container removal, not against disk failure, a bad migration or an accidental DROP. Schedule logical backups and store them off the host.
docker compose exec -T db pg_dump -U app -Fc appdb > /backups/appdb-$(date +%F).dump
Restore into a scratch database periodically to prove the backup works. Run pg_restore with the matching or newer client version.
6. A Safe Deploy Sequence
docker compose pull
docker compose up -d
docker compose ps
Never run docker compose down -v in production. If you must change the database major version, plan a dump and restore or use pg_upgrade as described in the PostgreSQL documentation.
Conclusion
Persist the data with a named volume, gate the app on a healthcheck, expose only the proxy, and keep tested backups. Those four habits prevent most first-deploy database incidents.
If you need help hardening a Compose-based Postgres deployment, I can help. Contact me.