14.3.2.3 Compose Database Service
A focused guide to Compose Database Service, connecting core concepts with practical Docker and container operations.
A Compose database service is the database engine defined as one service within a larger Compose app stack, configured with the persistence, networking isolation, and startup-ordering guarantees that distinguish a production-appropriate database definition from the convenient but underspecified version often used for quick local development.
Persisting data through a named volume
The single most important property of a database service definition is that its data lives in a named volume rather than the container's own writable layer, so that replacing or recreating the container, which happens routinely during deployments, never discards the database's actual contents:
services:
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
A database service definition with no volume mount at all is one of the most common and most consequential mistakes in a Compose stack, since it appears to work correctly right up until the first time the container is recreated.
Network isolation for the database
The database service generally has no reason to be reachable from outside the stack, and should be placed on an internal network reachable only by the services that genuinely need to query it directly:
services:
db:
networks:
- internal
# no ports published to the host
api:
networks:
- internal
- public
networks:
internal:
public:
Omitting a ports mapping for the database entirely, rather than relying only on a firewall or access control list to restrict it, removes the exposure at the source instead of attempting to filter it after the fact.
Health checks for accurate startup ordering
A database container reporting as "started" is not the same as it being ready to accept connections; the database process itself may still be performing initialization work after the container has technically started. A health check that actually attempts a connection gives dependent services an accurate signal:
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 10
api:
depends_on:
db:
condition: service_healthy
Without this health condition, depends_on alone only guarantees the database container started before the api container, not that it was actually ready to accept queries, which is a frequent cause of an application crashing on its very first startup attempt against a database that has not finished initializing.
Credentials and initialization
Database images typically support environment variables for initial setup, but production credentials supplied this way should come from a secret rather than a plaintext value in the Compose file:
services:
db:
image: postgres:16
environment:
- POSTGRES_USER=app_user
- POSTGRES_DB=app
secrets:
- db_password
# POSTGRES_PASSWORD_FILE points the image at the mounted secret
secrets:
db_password:
file: ./secrets/db_password.txt
Most official database images support a _FILE-suffixed variant of their password environment variable specifically to support reading the credential from a mounted file rather than requiring it directly in the environment.
Resource limits appropriate to database workloads
Databases frequently have different resource needs than typical application containers, particularly around memory for caching and shared memory for certain operations, and a production database service definition should reflect this explicitly rather than inheriting generic defaults:
services:
db:
image: postgres:16
shm_size: '1gb'
deploy:
resources:
limits:
memory: 4G
cpus: "2"
Under-provisioning a database's memory or shared memory allocation is a common cause of degraded performance that is easy to misattribute to the database engine itself rather than to the container resource configuration surrounding it.
Backup as a separate, explicit concern
The database service definition itself does not provide backups; a separate, scheduled process, often defined as its own short-lived service or an external cron job, should handle that independently:
services:
db-backup:
image: postgres:16
entrypoint: ["sh", "-c", "pg_dump -h db -U app_user app > /backups/app-$(date +%Y%m%d).sql"]
networks:
- internal
volumes:
- ./backups:/backups
secrets:
- db_password
Defining the backup process as its own stack-aware service, rather than a script run entirely outside of Docker's knowledge of the stack, keeps the backup mechanism's network access and credential handling consistent with the rest of the stack's own conventions.
Single instance as the default, deliberate constraint
Most relational database images run as a single instance within a Compose stack, since clustering a database for high availability is considerably more involved than simply increasing a replica count, and most database engines are not designed to run correctly as multiple independent writers without dedicated clustering software:
services:
db:
deploy:
replicas: 1
Treating this as an explicit, deliberate constraint, rather than an oversight, keeps the stack's actual availability characteristics honest: the database is the stack's single point of failure for data access unless a real clustering or managed database solution has been specifically adopted.
Common mistakes
- Defining a database service with no volume mount at all, losing all data the first time the container is recreated.
- Publishing the database's port directly to the host out of convenience, exposing it beyond the services within the stack that actually need it.
- Relying on
depends_onwithout a health condition, allowing a dependent service to start before the database is actually ready to accept connections. - Passing database credentials as plain environment variables in the Compose file instead of using the image's file-based credential support alongside a proper secret.
- Assuming a higher replica count would provide database redundancy the same way it does for a stateless service, without accounting for the clustering complexity real database high availability requires.
A production-appropriate Compose database service definition treats persistence, network isolation, accurate health-based startup ordering, and credential handling as non-negotiable defaults, while treating backup and high availability as explicit, separately designed concerns rather than something the basic service definition already provides.