20.2.1.3 Compose Database Persistence
A focused guide to Compose Database Persistence, connecting core concepts with practical Docker and container operations.
Database containers in a Compose application need persistent storage so that data survives container restarts, image updates, and docker compose down operations. Without a volume, a database container's data lives only in the container's writable layer — removed when the container is removed. Volumes decouple the data's lifecycle from the container's lifecycle, keeping records intact across deployments.
The Problem Without Persistence
A PostgreSQL container without a volume stores all database files in its writable layer:
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: secret
When you run docker compose down, the container is removed. When you run docker compose up again, PostgreSQL starts fresh with an empty database — all data is gone. For development, this may occasionally be intentional. For any data you need to keep, it is a problem.
Adding a Named Volume
Attach a named volume to the path where PostgreSQL stores its data files (/var/lib/postgresql/data):
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myuser
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
The volumes key under the db service maps the named volume db_data to /var/lib/postgresql/data inside the container. The top-level volumes section declares db_data as a Compose-managed volume. Docker creates the volume on first run if it does not exist.
What Persists Across Operations
With the volume in place:
| Operation | Data persists? |
|---|---|
docker compose stop + docker compose start | Yes |
docker compose down + docker compose up | Yes |
docker compose down --rmi all + docker compose up | Yes |
docker compose down -v + docker compose up | No — -v removes volumes |
Upgrading the postgres:15-alpine tag to postgres:16-alpine | Yes — same volume, new image |
The -v flag on docker compose down is the only standard operation that removes data intentionally. It is used when you want a clean slate — for resetting a development environment or testing database initialization scripts.
Verifying the Volume
docker volume ls
DRIVER VOLUME NAME
local myapp_db_data
Compose names the volume <project>_<volume_name>. The project name defaults to the directory name.
docker volume inspect myapp_db_data
[
{
"Name": "myapp_db_data",
"Driver": "local",
"Mountpoint": "/var/lib/docker/volumes/myapp_db_data/_data",
...
}
]
The Mountpoint shows where Docker stores the data on the host filesystem (on Linux). On Docker Desktop (Mac/Windows), the volume lives inside the Docker VM and is not directly browsable from the host.
Initializing the Database
PostgreSQL's official image runs any .sql files placed in /docker-entrypoint-initdb.d/ on first startup — only when the data directory is empty (i.e., a new volume). This is the standard way to create tables, seed data, or configure extensions on first run:
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myuser
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
volumes:
db_data:
The :ro suffix mounts the init script read-only — the container can execute it but cannot modify it. The initialization script runs once on the first docker compose up; subsequent startups skip it because the data directory is already populated.
MySQL and Other Database Paths
The volume mount path differs by database engine:
| Database | Data directory |
|---|---|
| PostgreSQL | /var/lib/postgresql/data |
| MySQL / MariaDB | /var/lib/mysql |
| MongoDB | /data/db |
| Redis | /data |
Each database image documents its data directory. Mount a named volume to that path for persistence.
For MySQL:
services:
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_DATABASE: myapp
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
Bind Mount Alternative for Development
For development, you may prefer a bind mount — mapping a host directory to the database data path. This makes the data directly browsable from the host:
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: secret
volumes:
- ./pgdata:/var/lib/postgresql/data
Compose creates ./pgdata in the project directory. The database files are stored there and visible from the host. On macOS and Windows, bind mount I/O performance for database workloads is significantly slower than named volumes because of filesystem translation overhead. Named volumes are faster for databases in development.
Backing Up the Database
To back up a PostgreSQL database running in Compose:
docker compose exec db pg_dump -U myuser myapp > backup.sql
This runs pg_dump inside the running container and redirects the output to a file on the host. The -U myuser and myapp arguments should match the POSTGRES_USER and POSTGRES_DB environment variables.
To restore:
docker compose exec -T db psql -U myuser myapp < backup.sql
The -T flag disables pseudo-TTY allocation, which is necessary when piping stdin from the host.
Volume Sharing Between Services
Multiple services can mount the same named volume if they need to share files. For example, a web server and a file processor might share an upload directory:
services:
web:
volumes:
- uploads:/app/uploads
processor:
volumes:
- uploads:/data/uploads
volumes:
uploads:
For databases, sharing volumes between multiple database containers is not recommended — database engines are designed to have exclusive access to their data directories.
Database Volume Lifecycle in Practice
On a fresh development machine:
docker compose up -d— Compose createsmyapp_db_data, starts PostgreSQL, runs initialization scripts, creates tables and seeds data.- Developer works with the application for several days, writing records to the database.
docker compose down— containers stop and are removed;myapp_db_datapersists.docker compose up -d— containers start again; PostgreSQL finds existing data and resumes from where it left off.- Developer needs a clean reset:
docker compose down -v— volume is deleted; nextdocker compose up -dstarts fresh.