Skip to content

Named Volumes

Measured on Docker Desktop 4.81.0 / Engine 29.6.1 (macOS, linux/arm64), 2026-09-30.

A named volume is a storage unit that Docker creates and manages on your behalf. Docker stores volume data under its own directory (/var/lib/docker/volumes/ by default, on the machine that runs the engine). You refer to the volume by name — never by a host path.

Because Docker owns the storage location, named volumes:

  • Work the same way on every OS the engine runs on (you pass a name, not a path).
  • Survive docker rm — the volume is separate from the container.
  • Can be shared between multiple containers simultaneously.
  • Are the recommended way to persist application data (Docker docs: volumes).
Terminal window
docker volume create mydata
docker volume ls
docker volume inspect mydata

Real output (the course’s volume is named docker-b2-mydata):

$ docker volume ls
DRIVER VOLUME NAME
local docker-b2-mydata
$ docker volume inspect docker-b2-mydata
[
{
"CreatedAt": "2026-09-30T16:29:16Z",
"Driver": "local",
"Labels": {
"course-agent": "docker-b2"
},
"Mountpoint": "/var/lib/docker/volumes/docker-b2-mydata/_data",
"Name": "docker-b2-mydata",
"Options": null,
"Scope": "local"
}
]
$ docker volume rm docker-b2-nonexist
Error response from daemon: get docker-b2-nonexist: no such volume
$ docker volume rm docker-b2-busy # a running container mounts it
Error response from daemon: remove docker-b2-busy: volume is in use - [b3b9cd6c...]

A volume cannot be removed while any container (even a stopped one) references it. Remove the container first.

There are two syntaxes. Both create the same mount.

Short -v syntax:

Terminal window
docker run -d --name myapp -v mydata:/app/data myimage

Long --mount syntax (explicit and recommended for scripts):

Terminal window
docker run -d --name myapp \
--mount type=volume,source=mydata,target=/app/data \
myimage

If the volume mydata does not exist yet, Docker creates it automatically when you run the container.

Copy-up: an empty volume takes the image’s files

Section titled “Copy-up: an empty volume takes the image’s files”

When you mount an empty volume on a directory that already has content in the image, Docker copies that content into the volume first. Bind mounts do not do this.

$ docker volume create docker-b2-html
$ docker run --rm -v docker-b2-html:/usr/share/nginx/html nginx:1.31-alpine ls -l /usr/share/nginx/html
-rw-r--r-- 1 root root 497 Sep 15 14:18 50x.html
-rw-r--r-- 1 root root 896 Sep 15 14:18 index.html
$ docker run --rm -v docker-b2-html:/data alpine:3.24 ls /data # a different image, same volume
50x.html
index.html

The volume now holds nginx’s default pages even though the second container’s image has none. If the volume is not empty, its content wins and the image’s files at that path are hidden.

A -v /data (no name) or an image VOLUME instruction creates an anonymous volume with a random 64-character name and the label com.docker.volume.anonymous:

$ docker run -d --name anon -v /data alpine:3.24 sleep 60
$ docker inspect -f '{{range .Mounts}}{{.Type}} name={{.Name}} dst={{.Destination}}{{end}}' anon
volume name=886e1acf723f1af30efc071d3c179610f9f7eb902330b289d760d4dbcb9eb3f2 dst=/data

Anonymous volumes are easy to lose track of, which is why an image such as postgres (it declares VOLUME) leaves one behind every time you docker run it without naming a mount.

What docker rm -v and docker volume prune really delete

Section titled “What docker rm -v and docker volume prune really delete”

Both commands are narrower than most people expect:

# rm -v removes anonymous volumes only: the named volume survives
$ docker rm -f -v named # container mounted docker-b2-mydata:/data
$ docker volume ls --filter label=course-agent=docker-b2 -q
docker-b2-html
docker-b2-mydata
# prune without -a removes only unreferenced ANONYMOUS volumes
$ docker volume prune -f --filter label=course-agent=docker-b2
Deleted Volumes:
405eac1fcb1292d90e253b04262d4fb3a18957f074ecae35d5c8600bb9607de7 # the anonymous one
$ docker volume ls --filter label=course-agent=docker-b2
DRIVER VOLUME NAME
local docker-b2-html
local docker-b2-mydata
# -a also removes unreferenced NAMED volumes
$ docker volume prune -f -a --filter label=course-agent=docker-b2
Deleted Volumes:
docker-b2-mydata
docker-b2-html
  • docker rm -v deletes the container’s anonymous volumes; a named volume stays (proven: both docker-b2-* volumes still listed).
  • docker volume prune (Engine 29) deletes unused anonymous volumes; add -a / --all for unused named volumes too (reference).
  • The --filter label=… limited every prune above to this lesson’s objects. The Docker Desktop VM in this test held 77 volumes from other projects; an unfiltered -a would have deleted every unreferenced one of them. Treat -a as destructive.

Data persists across container replacements

Section titled “Data persists across container replacements”

The key property of a named volume: you can delete the container and recreate it, and all the data written to the volume is still there.

Terminal window
# Run a container and write data
docker run --name writer -v mydata:/app/data alpine:3.24 sh -c "echo 'persistent' > /app/data/file.txt"
# Remove the container
docker rm writer
# Start a brand-new container mounting the same volume
docker run --rm -v mydata:/app/data alpine:3.24 cat /app/data/file.txt
# Output: persistent

The snippet below runs a container, writes data to a named volume, removes the container, and mounts the same volume in a new container to prove the data survived.

# 1. Create a named volume
docker volume create demo-vol

# 2. Write data into the volume via a container
docker run --rm -v demo-vol:/data alpine:3.24 sh -c "echo 'data survives!' > /data/message.txt"

# 3. Read it back from a completely new container
docker run --rm -v demo-vol:/data alpine:3.24 cat /data/message.txt

# 4. Inspect the volume
docker volume inspect demo-vol

# 5. Clean up
docker volume rm demo-vol

An AI-written Compose file for a Postgres 18 database, copied from an older tutorial:

🐛 Spot the bug

compose.yaml
services:
db:
image: postgres:18-alpine
environment:
POSTGRES_PASSWORD: <your-db-password>
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Show the answer

The container never becomes healthy. It exits at once with code 1 and a long explanation:

$ docker ps -a --filter name=docker-b2-pg --format '{{.Status}}'
Exited (1) 4 seconds ago
$ docker logs docker-b2-pg
Error: in 18+, these Docker images are configured to store database data in a
format which is compatible with "pg_ctlcluster" (specifically, using
major-version-specific directory names). ...
Counter to that, there appears to be PostgreSQL data in:
/var/lib/postgresql/data (unused mount/volume)
...
The suggested container configuration for 18+ is to place a single mount
at /var/lib/postgresql which will then place PostgreSQL data in a
subdirectory ...

The cause is in the image: docker image inspect postgres:18-alpine shows Volumes: {"/var/lib/postgresql":{}} and PGDATA=/var/lib/postgresql/18/docker. Up to Postgres 17 the data directory was /var/lib/postgresql/data; from 18 the image declares the parent as the volume. The old mount point puts the named volume where the image no longer looks, and Docker also attaches a fresh anonymous volume at /var/lib/postgresql (docker inspect listed both mounts). Mount the volume at /var/lib/postgresql instead:

compose.yaml
services:
db:
image: postgres:18-alpine
environment:
POSTGRES_PASSWORD: <your-db-password>
volumes:
- pgdata:/var/lib/postgresql
volumes:
pgdata:

With this file docker compose up -d --wait reported Healthy, show data_directory returned /var/lib/postgresql/18/docker, and the volume contained the directory 18/docker. Always read the image’s documented mount point (hub.docker.com/_/postgres) and check docker image inspect --format '{{json .Config.Volumes}}' when you upgrade a major version.

A named volume is storage with a name and its own lifetime. The container references it; the reference is what keeps volume rm from working and what prune looks at. Mounting decides where in the container the storage appears, so the path must match what the program actually writes to — an image that moves its data directory (Postgres 18) silently changes which mount matters.

OptionBenefitCost
Named volumeDocker manages the path, portable across hosts, easy to back up with a throwaway container, copy-up seeds itCannot access the data directly from the host filesystem (on Desktop it is inside the VM) — always goes through a container
Anonymous volumeZero config, VOLUME in an image gives you persistence by defaultRandom name, easy to orphan; rm -v/prune delete it, so “persistent” data may vanish
Bind mountSee and edit files directly from the host right awayTied to a specific host path, hard to move between environments
  • Using a bind mount instead of a named volume for production database data, losing portability.
  • Running docker compose down -v by accident and losing all the data in the volume.
  • Never planning a backup for a volume, assuming “a volume lasts forever” and never backing it up.
  • Running docker volume prune -a or docker system prune -a --volumes on a shared host without checking what else uses it.
  • Mounting the database volume at the path an older major version used.
  • “docker rm -v deletes the container’s volume.” Only anonymous volumes; named volumes always survive it.
  • “docker volume prune deletes all unused volumes.” Only unused anonymous volumes on Engine 29; unused named volumes need -a.
  • “The Mountpoint is a folder on my Mac.” On Docker Desktop it is inside the VM.
  • “Mounting a volume always hides the image’s files.” Only if the volume is non-empty; an empty named volume is first populated from the image (copy-up).
What happens to a named volume when you run docker rm on the container using it?
Which flag mounts a named volume using the short syntax?
What does docker volume prune -f (no -a) delete on Engine 29?
If you specify a volume name that does not exist in a docker run command, what does Docker do?
You mount an empty named volume on /usr/share/nginx/html of the nginx image. What does the volume contain afterwards?
postgres:18-alpine with -v pgdata:/var/lib/postgresql/data exits with code 1. What is the fix?