vM.

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.

Additional Resources

  • Docker volumes documentation
  • Docker Compose startup order
  • Caddy reverse_proxy directive