Docker Volumes
Docker volumes are Docker-managed storage areas that let data live beyond the life of a container. They matter because a container’s own writable layer is disposable: remove the container, and files written only inside that layer are gone.
Use volumes for databases, uploaded files, queues, caches that should survive restarts, and any state you do not want tied to one container instance. A volume is not an image layer; it is a separate mount attached to a container at runtime.
Overview: How Docker Volumes Work
A Docker image is a stack of read-only layers. When Docker starts a container, it adds a thin writable container layer on top. That writable layer is convenient for temporary files, but it is coupled to the container. If you run docker rm, Docker removes that container layer.
A volume solves this by mounting a separate storage location into the container filesystem. With a named volume, Docker creates and manages the storage under Docker’s own data area. On Linux Engine this is typically somewhere under /var/lib/docker/volumes; on Docker Desktop it lives inside the Linux VM, not directly in your normal macOS or Windows filesystem. You normally do not edit those files by hand. You ask Docker to create, inspect, mount, and remove volumes.
Volumes are different from bind mounts. A named volume is identified by a Docker name such as pgdata and is the recommended default for persistent service data. A bind mount maps a specific host path, such as $(pwd)/src, into a container and is common for local development where your editor and the container need to see the same source files. Bind mounts expose host layout and permissions more directly; volumes are more portable across Docker hosts and Compose projects.
When a volume is mounted over a path in the container, that mount hides whatever was already at that path for the lifetime of the container. If the volume is empty and the image contains files at that destination, Docker can copy the image’s existing directory contents into the new volume for the default local volume driver. This behavior is useful for initializing application data, but you should not rely on it as a full database migration system.
Volumes have their own lifecycle. Stopping or removing a container does not remove a named volume. That is intentional. It means you can replace a broken container with a fresh one and keep the same database files. It also means unused volumes can accumulate, and deleting a volume is a real data deletion operation.
Syntax
The main volume commands and mount forms are:
docker volume COMMAND [OPTIONS]
docker run --mount type=volume,source=VOLUME_NAME,target=/path/in/container IMAGE COMMAND
docker run -v VOLUME_NAME:/path/in/container IMAGE COMMAND
| Form | Meaning |
|---|---|
docker volume create NAME |
Create a named volume before it is used. |
docker volume ls |
List Docker-managed volumes. |
docker volume inspect NAME |
Show metadata, driver, mountpoint, labels, and options. |
docker volume rm NAME |
Delete a volume. Docker refuses if a container still uses it. |
docker volume prune |
Delete unused local volumes. Treat this as destructive cleanup. |
--mount type=volume,source=NAME,target=PATH |
Explicit modern mount syntax. Easier to read and less ambiguous. |
-v NAME:PATH |
Short legacy-style syntax that is still widely used. |
--rm |
Remove the container after it exits. Named volumes survive unless they were anonymous volumes created for that container. |
Prefer --mount when teaching or scripting because each part is named. The short -v form is acceptable, but it can be harder to spot whether you meant a named volume or a bind mount.
Examples
Example 1: Create, use, and inspect a named volume
This creates a named volume, writes a file into it from one container, then reads it from another:
docker volume create notes-data
docker run --rm --mount type=volume,source=notes-data,target=/data alpine:3.20 sh -c 'echo "hello from a volume" > /data/message.txt'
docker run --rm --mount type=volume,source=notes-data,target=/data alpine:3.20 cat /data/message.txt
docker volume inspect notes-data
Output:
notes-data
hello from a volume
[
{
"Name": "notes-data",
"Driver": "local",
"Mountpoint": "/var/lib/docker/volumes/notes-data/_data"
}
]
Both containers were temporary because of --rm, but the named volume remained. The file was not stored in either container’s writable layer. It was stored in notes-data, then mounted again for the second container. The exact Mountpoint differs on Docker Desktop because the Docker daemon runs inside a managed VM.
Example 2: Persist PostgreSQL data
Databases are the classic volume use case. PostgreSQL stores its data under /var/lib/postgresql/data, so mount a volume there:
docker volume create pgdata
docker run -d --name lesson-postgres --mount type=volume,source=pgdata,target=/var/lib/postgresql/data -e "POSTGRES_PASSWORD=changeme" postgres:16-alpine
docker stop lesson-postgres
docker rm lesson-postgres
docker run -d --name lesson-postgres-2 --mount type=volume,source=pgdata,target=/var/lib/postgresql/data -e "POSTGRES_PASSWORD=changeme" postgres:16-alpine
Output:
pgdata
6f6a1f0d5c8e
lesson-postgres
lesson-postgres
9d5c8e6f6a1f
The container was removed, but pgdata was not. The second PostgreSQL container starts with the same data directory. The password here is an obvious local-development placeholder; real database passwords should come from your deployment platform, Docker secrets, or an ignored environment file.
Example 3: Use a named volume in Compose
Compose is often cleaner for services with storage because the volume declaration lives beside the service definition:
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: changeme
POSTGRES_USER: appuser
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
docker compose up -d
docker compose ps
docker compose down
Output:
[+] Running 3/3
✔ Network storage-demo_default Created
✔ Volume storage-demo_pgdata Created
✔ Container storage-demo-db-1 Started
NAME IMAGE SERVICE STATUS
storage-demo-db-1 postgres:16-alpine db Up 5 seconds
[+] Running 2/2
✔ Container storage-demo-db-1 Removed
✔ Network storage-demo_default Removed
docker compose down removes containers and the project network, but it leaves the named volume. Compose prefixes the physical volume name with the project name, so top-level pgdata may appear as something like storage-demo_pgdata. Use docker compose down --volumes only when you intentionally want to delete that data.
Example 4: Back up a volume to a tar file
A simple backup pattern is to mount the volume read-only and also bind mount the current directory as a destination:
docker run --rm --mount type=volume,source=pgdata,target=/data,readonly --mount type=bind,source="$(pwd)",target=/backup alpine:3.20 tar -czf /backup/pgdata-backup.tar.gz -C /data .
Output:
This command normally prints nothing when it succeeds. It creates pgdata-backup.tar.gz in the current host directory. For production databases, prefer database-aware backup tools such as pg_dump or filesystem snapshots coordinated with the database, because copying live database files can produce inconsistent backups.
How It Works Step by Step
- The Docker CLI sends the
docker runrequest to the Docker daemon with the image name and mount specification. - The daemon creates the named volume if it does not already exist.
docker volume createmakes this explicit, butdocker run --mount type=volume,source=namecan create it too. - Docker prepares the container from the image’s read-only layers and a new thin writable layer.
- Before the main process starts, Docker mounts the volume at the requested target path inside the container namespace.
- Reads and writes under that target path go to the volume, not to the container writable layer.
- If the container stops or is removed, Docker removes the container resources, but the named volume remains.
- Another container can mount the same volume later. Multiple containers can mount the same volume at the same time, but the application must be designed for shared access.
Common Mistakes
Storing important data only in the container layer
This command writes inside the container filesystem without a volume:
docker run --name temp-notes alpine:3.20 sh -c 'echo "important" > /data.txt'
If you remove temp-notes, that file goes with the container. Fix it by mounting a named volume at the directory that holds the data:
docker run --name durable-notes --mount type=volume,source=notes-data,target=/data alpine:3.20 sh -c 'echo "important" > /data/data.txt'
Using docker compose down --volumes casually
This is a destructive reset for a Compose project:
docker compose down --volumes
It removes named volumes declared by the project. That is useful for test resets, but it can delete database files you expected to keep. Use plain docker compose down when you only want to remove containers and networks.
Confusing a bind mount with a named volume
The left side determines the mount type in short syntax:
docker run --rm -v "$(pwd)/html:/usr/share/nginx/html" nginx:1.27-alpine
Because $(pwd)/html is a host path, this is a bind mount, not a named volume. That is fine for local source files, but it is less portable than a Docker-managed volume for service data. For a named volume, use a volume name instead of a host path.
Mounting over files you expected from the image
If an image has useful files in /app/data and you mount an existing empty volume there, the mount hides the image directory while the container is running. The files are not deleted from the image, but the process sees the mounted volume at that path. Choose mount targets deliberately and initialize data explicitly for important applications.
Best Practices
- Use named volumes for persistent application data such as database directories, uploads, and broker state.
- Use bind mounts mainly for local development source code or for deliberate host integration.
- Prefer
--mountin examples and scripts becausetype,source, andtargetare explicit. - Name volumes after the data they hold, such as
pgdataoruploads-data, not after a temporary container. - Inspect volumes before deleting them, and treat
docker volume rm,docker volume prune, anddocker compose down --volumesas destructive commands. - Do not edit Docker’s internal volume directories directly. Use containers, backup tools, or Docker commands to access volume data.
- Back up important volumes. For databases, prefer database-aware backup and restore procedures.
- Pin image tags such as
postgres:16-alpineandalpine:3.20so examples and deployments are reproducible. - Remember that
EXPOSEdocuments ports only; it does not publish a port or affect volume behavior.
Practice Exercises
- Create a volume named
todo-data. Use analpine:3.20container to write a text file into it, remove the container, then use a second container to read the file. Expected end state: the second container prints the same text. - Write a
compose.yamlforpostgres:16-alpinewith a named volume mounted at/var/lib/postgresql/data. Start it, stop it withdocker compose down, then verify the volume still exists withdocker volume ls. - Compare a bind mount and a named volume. Mount
$(pwd)into an Nginx container for a static site, then mount a named volume into a separate Alpine container. Identify which one depends on a specific host path.
Summary
- A container writable layer is tied to that container; a named volume survives container removal.
- Docker manages named volumes, usually under the daemon’s storage area or Docker Desktop VM.
- Volumes are the recommended default for persistent service data; bind mounts are common for local development files.
- Mounting a volume at a path sends reads and writes at that path to the volume instead of the container layer.
- Compose named volumes survive plain
docker compose down, butdocker compose down --volumesremoves them. - Deleting or pruning volumes deletes data, so inspect and back up important volumes first.
