Databases Done Right
"It works" vs "it survives a reboot" — the difference is one line.
The one line that matters
volumes:
- pgdata:/var/lib/postgresql/data
Without that line, your database is gone the moment you redeploy. Every Postgres tutorial includes it; every first-time deployment forgets it. Make it the first thing you write in any database service block, before the image tag.
Pin the major version
Use postgres:16 or mysql:8.4, never postgres:latest. Databases do not auto-migrate between major versions — pulling latest the day Postgres 17 ships will fail to start on your old data files and you will spend an afternoon doing a surprise migration at 2 AM. Pinning the major version also makes your setup reproducible, which matters when you rebuild the box a year later.
Health checks so the app waits
Without a health check, your app container starts before Postgres is ready and crash-loops until someone notices. Fix it:
db:
image: postgres:16
healthcheck:
test: ["CMD", "pg_isready", "-U", "app"]
interval: 5s
retries: 10
app:
depends_on:
db:
condition: service_healthy
Compose will hold app until pg_isready succeeds. Your logs get quieter, your uptime goes up.
Connection pooling at your scale
Postgres has a hard cap on concurrent connections (default 100, and each one costs ~10 MB of RAM). Frameworks like Next.js or Laravel open new connections per request in development and will exhaust the pool in seconds under real load. The fix is PgBouncer in transaction mode, sitting between your app and Postgres. For a single-box side project, you can often skip it by setting connection_limit=5 in your ORM — but once you scale to serverless functions or multiple app replicas, PgBouncer is mandatory.
Never expose the database port
In compose, never write ports: ["5432:5432"] on your database. The internal Docker network already lets your app reach it — publishing the port opens it to the entire internet and the bots will find it. If you need direct access from your laptop, use ssh -L 5432:db:5432 deploy@server to tunnel it over SSH instead. One less attack surface, zero performance cost.
Key takeaways
- One missing volume line loses your database on redeploy
- Pin major versions; never use `latest` for databases
- Health checks let the app wait for Postgres to be ready
- Never publish 5432 to the internet — tunnel over SSH instead