Persisting Database Data in Containers
Databases inside containers need storage that survives the container itself. Docker containers are easy to delete and recreate, so database files must live in a volume instead of only in the container writable layer.
This lesson shows how to persist PostgreSQL data with Docker named volumes, how Compose manages database volumes, and how to avoid the common mistakes that cause accidental data loss. The same principles apply to MySQL, MariaDB, Redis, MongoDB, and other stateful services.
Overview: How Database Persistence Works
A Docker image is a stack of read-only filesystem layers. When Docker starts a container, it adds a thin writable layer for that container’s changes. If PostgreSQL writes database files into that ordinary writable layer, those files belong to that container instance. Stop the container and the files remain for now; remove the container with docker rm, and the writable layer is removed too.
A Docker volume changes where selected paths store data. A named volume such as pgdata is a Docker-managed storage location mounted into the container at runtime. For PostgreSQL, the important path is /var/lib/postgresql/data. When that path is backed by a volume, PostgreSQL still sees a normal directory, but writes go to the volume instead of the container layer.
This is why container replacement becomes safe. You can remove a broken PostgreSQL container and start a new one from the same image, mounting the same pgdata volume at the same path. The process, container ID, and writable layer are new; the database files are the same.
Named volumes are usually the right default for database data. On Linux Engine, Docker stores local volumes under Docker’s storage area, commonly below /var/lib/docker/volumes. On Docker Desktop for macOS and Windows, the data is inside Docker Desktop’s Linux VM. You normally inspect and access volumes through Docker commands rather than editing those internal directories by hand.
Bind mounts can persist database data too, but they tie the database to a specific host path and host filesystem behavior. That can be useful for local experiments, but for most Docker-based database services a named volume is clearer, more portable, and less likely to inherit surprising permissions.
One important detail: database images often initialize themselves only when the data directory is empty. The official PostgreSQL image uses environment variables such as POSTGRES_DB, POSTGRES_USER, and POSTGRES_PASSWORD during first initialization. If the volume already contains a database cluster, changing those variables later does not rewrite the existing database. That is a feature, not a bug: persistent data should not be silently reinitialized every time a container starts.
Syntax
The common command forms are:
docker volume create VOLUME_NAME
docker run --name CONTAINER_NAME --mount type=volume,source=VOLUME_NAME,target=/database/data/path IMAGE
docker compose up -d
| Part | Purpose |
|---|---|
docker volume create VOLUME_NAME |
Creates a Docker-managed named volume before the database container starts. |
--mount type=volume |
Tells Docker to attach a named volume, not a bind mount. |
source=VOLUME_NAME |
The volume name Docker should create or reuse. |
target=/var/lib/postgresql/data |
The path inside the PostgreSQL container where database files are stored. |
-e "POSTGRES_PASSWORD=changeme" |
Sets a local-development initialization variable. Use an obvious placeholder in examples and a real secret store in production. |
docker compose down |
Removes Compose containers and networks but keeps named volumes by default. |
docker compose down --volumes |
Also removes project volumes. This deletes database data for that Compose project. |
For PostgreSQL, mount the volume at /var/lib/postgresql/data. For other database images, check the image documentation for the correct data directory, such as /var/lib/mysql for many MySQL images.
Examples
Example 1: Run PostgreSQL with a persistent volume
Create a named volume and start PostgreSQL with that volume mounted at its data directory:
docker volume create lesson-pgdata
docker run -d --name lesson-postgres --mount type=volume,source=lesson-pgdata,target=/var/lib/postgresql/data -e "POSTGRES_PASSWORD=changeme" -e "POSTGRES_DB=appdb" postgres:16-alpine
Output:
lesson-pgdata
2f4b7c9d8a1e6b3c5d0f123456789abcdeffedcba9876543210abcdef1234567
The first line is the created volume name. The long value is the new detached container ID. PostgreSQL initializes a new database cluster because lesson-pgdata is empty the first time it is mounted. From this point forward, the database files are in the volume, not only in the container writable layer.
Example 2: Remove and recreate the container without losing data
A container can be replaced while the named volume remains:
docker stop lesson-postgres
docker rm lesson-postgres
docker run -d --name lesson-postgres-replacement --mount type=volume,source=lesson-pgdata,target=/var/lib/postgresql/data -e "POSTGRES_PASSWORD=changeme" postgres:16-alpine
Output:
lesson-postgres
lesson-postgres
8a1e6b3c5d0f2f4b7c9d123456789abcdeffedcba9876543210abcdef7654321
The old container stopped and was removed, but lesson-pgdata was not removed. The replacement container sees the existing PostgreSQL data directory. Notice that POSTGRES_DB is not repeated here; because the volume already contains a database cluster, PostgreSQL does not initialize a new one from environment variables.
Example 3: Persist database data with Compose
For real projects, Compose keeps the service, environment, ports, and volume declaration in one file:
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: changeme
POSTGRES_USER: appuser
POSTGRES_DB: appdb
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
docker compose up -d
docker compose ps
docker compose down
Output:
[+] Running 3/3
✔ Network app_default Created
✔ Volume app_pgdata Created
✔ Container app-db-1 Started
NAME IMAGE SERVICE STATUS
app-db-1 postgres:16-alpine db Up 6 seconds
[+] Running 2/2
✔ Container app-db-1 Removed
✔ Network app_default Removed
Compose creates a project-scoped volume, commonly named like app_pgdata. Plain docker compose down removes the container and network but leaves the volume. The port mapping is only for client access from the host; it is unrelated to persistence. In Dockerfiles, EXPOSE is only metadata and would not publish this port by itself.
Example 4: Back up a PostgreSQL database logically
For PostgreSQL, a logical backup with pg_dump is usually safer than copying live database files:
docker exec lesson-postgres-replacement pg_dump -U postgres appdb > appdb.sql
Output:
A successful pg_dump command often prints no terminal output because the SQL dump is redirected to appdb.sql on the host. This approach asks PostgreSQL to produce a consistent backup instead of copying files while the database engine may be writing to them.
How It Works Step By Step
- The Docker CLI sends a container creation request to the Docker daemon, including the image name, environment variables, and mount definition.
- The daemon pulls
postgres:16-alpineif the image is not already available locally. The image layers remain read-only. - Docker creates or finds the named volume
lesson-pgdata. - Docker creates a new container writable layer, then mounts the volume at
/var/lib/postgresql/databefore the PostgreSQL process starts. - On first start with an empty data directory, the PostgreSQL entrypoint initializes a database cluster and applies initialization environment variables.
- PostgreSQL writes tables, indexes, WAL files, and metadata under the mounted path, so those files land in the volume.
- If the container is stopped or removed, Docker deletes the container runtime resources and writable layer, but the named volume remains.
- When a replacement container mounts the same volume at the same target path, PostgreSQL starts from the existing database files instead of creating a new cluster.
Common Mistakes
Running a database without a volume
This looks fine until the container is removed:
docker run --name throwaway-postgres -e "POSTGRES_PASSWORD=changeme" -d postgres:16-alpine
PostgreSQL stores data inside the container filesystem. If you remove that container, the database files disappear with it. Fix it by mounting a named volume at the database data directory:
docker run --name durable-postgres --mount type=volume,source=durable-pgdata,target=/var/lib/postgresql/data -e "POSTGRES_PASSWORD=changeme" -d postgres:16-alpine
Expecting changed initialization variables to modify an existing database
After a PostgreSQL volume has data, this does not rename the existing database or user:
docker run --name changed-env --mount type=volume,source=lesson-pgdata,target=/var/lib/postgresql/data -e "POSTGRES_PASSWORD=changeme" -e "POSTGRES_DB=newdb" postgres:16-alpine
The entrypoint sees an existing database cluster and skips first-time initialization. Use SQL migrations, createdb, or a deliberate fresh volume when you want a new database.
Deleting Compose volumes during routine shutdown
This command is a data reset, not a normal stop:
docker compose down --volumes
It removes the project volumes declared in the Compose file. Use docker compose down for ordinary cleanup, and reserve --volumes for test environments or intentional resets.
Copying live database files as the only backup
A tar archive of a running database volume may capture files in the middle of writes. For PostgreSQL, prefer pg_dump, pg_dumpall, base backups, or filesystem snapshots coordinated with the database.
Best Practices
- Use named volumes for database data by default; use bind mounts only when a specific host path is truly part of the workflow.
- Mount the volume at the exact data directory required by the database image.
- Pin database image tags, such as
postgres:16-alpine, instead of usinglatestfor reproducible behavior. - Treat
docker volume rm,docker volume prune, anddocker compose down --volumesas destructive operations. - Back up important databases with database-aware tools, not only raw file copies.
- Keep real passwords out of images and command history when possible; use Docker secrets, orchestrator secrets, or ignored environment files for serious deployments.
- Do not rely on changing initialization environment variables after a volume already contains data.
- Give volumes meaningful names such as
app-pgdataso cleanup commands are easier to review. - Test restore procedures, not just backup creation.
Practice Exercises
- Start PostgreSQL with a named volume called
practice-pgdata, remove the container, and start a replacement using the same volume. Expected end state: the volume still exists after both containers are removed. - Create a Compose file for PostgreSQL with a named volume. Run
docker compose down, then verify withdocker volume lsthat the project volume remains. - Make a logical backup with
pg_dumpfrom a running PostgreSQL container. Hint: redirect the output to a file on the host and check that the file contains SQL statements.
Summary
- Database containers need persistent storage because container writable layers are removed with the container.
- A named volume mounted at the database data directory keeps database files outside the container layer.
- PostgreSQL stores its default data under
/var/lib/postgresql/data. - Initialization environment variables apply when a database volume is empty, not every time the container starts.
- Plain
docker compose downkeeps volumes;docker compose down --volumesdeletes them. - Use database-aware backups and test restores before trusting a containerized database with important data.
