Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Find orphan Docker volumes without deleting everything

By updated

Improve this page

A volume that no container uses is called orphan by Docker, but that does not prove it is useless: it may hold the database of a project you stopped or renamed. This lesson shows you how to find that volume among others, read its contents without any risk, keep a copy that can be read back, reattach it to a Compose service without recreating it, and then delete only what is disposable. The measurements come from a Debian 13 VM with Docker Engine 29.9.2 and Compose 5.6.1.

What you will learn

Each step is checked against the real state of the engine, not against an assumption.

  • You tell a named volume from an anonymous volume, and you know what docker rm, docker rm -v and docker run --rm do with each.
  • You find a volume by its labels, its name or its container, and you read its contents with a disposable container mounted read-only.
  • You back up a volume with tar, then read the archive back before deleting anything.
  • You reattach the old volume of a renamed Compose project with external: true, without recreating it.
  • You predict what docker volume prune and docker compose down -v remove, and you delete a volume by its name only.

Prerequisites

You must know how to create a named volume, see how it differs from an anonymous volume and filter it with dangling=true: the lesson Docker volumes: keep your data and mount files explains it. You must also know the Compose project name and the effect of down -v, covered in Persist the data of a recreated stack. To back up the database of a Compose stack with a logical export rather than a copy of the volume, the guide Back up and restore a Compose application takes over.

Run the blocks in order and in a single terminal: they reuse the IMAGE variable and the current folder. Every object of the demonstration carries the c41b-m- prefix, which lets you target them and nothing else. Do not run this lesson on a host that holds volumes of your own: docker volume ls would show them too, and you could mistake them for those of the demonstration.

Terminal window
export IMAGE="alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a"
mkdir -p ~/donnees-orphelines/ctx ~/donnees-orphelines/projet
cd ~/donnees-orphelines
docker volume ls
DRIVER VOLUME NAME

The list is empty on the measurement VM. If yours already contains volumes, note them down so you can check at the end that they did not move, but prefer a disposable VM.

What is an orphan volume, and why is it not necessarily useless?

A volume is orphan when no container references it, even a stopped one. Docker lists it with the dangling=true filter, which groups named volumes and anonymous volumes without distinction. This filter only says that no container uses the volume: it says nothing about the value of what the volume contains.

A named volume carries the name you give it, for example docker volume create c41b-m-data. An anonymous volume is born without a name, when a container declares a path with no source or when the image declares a VOLUME instruction: Docker then gives it a name made of 64 hexadecimal characters. The following Dockerfile declares VOLUME /donnees, after writing a file in that folder.

Terminal window
cat > ctx/Dockerfile <<EOF
FROM $IMAGE
RUN mkdir /donnees && echo graine > /donnees/graine.txt
VOLUME /donnees
CMD ["sleep","300"]
EOF
docker build -q -t c41b-m-img:1 ctx
docker run -d --name c41b-m-v1 c41b-m-img:1
ANON=$(docker inspect -f '{{range .Mounts}}{{.Name}}{{end}}' c41b-m-v1)
docker volume ls
sha256:3e5fd844868389b03664723cfcd37d39ecc4aa0f667dde67e8bd4314e260ed9b
fb988cb1a0266f90a6f3b50c5e2dbfed6be3050b600e77a7760f7e6cdcdf8d7b
DRIVER VOLUME NAME
local 7eecf463ce016e35092868234b465ae42e53e9c80d58b32db65843383677c0d6

In the last output, look at the name: 64 hexadecimal characters, no readable name. docker run created this volume by itself, and the ANON variable keeps its name for the rest of the lesson. What was written in /donnees before VOLUME is copied into the volume at the first start: graine.txt shows up again further down.

docker volume inspect confirms its origin: the com.docker.volume.anonymous label marks an anonymous volume, and CreatedAt gives its creation date in UTC.

Terminal window
docker volume inspect "$ANON"
[
{
"CreatedAt": "2026-11-04T18:32:38Z",
"Driver": "local",
"Labels": {
"com.docker.volume.anonymous": ""
},
"Mountpoint": "/var/lib/docker/volumes/7eecf463ce016e35092868234b465ae42e53e9c80d58b32db65843383677c0d6/_data",
"Name": "7eecf463ce016e35092868234b465ae42e53e9c80d58b32db65843383677c0d6",
"Options": null,
"Scope": "local"
}
]

A mount with no name, such as -v /donnees2, gave the same result: a volume named with 64 hexadecimal characters. The guide Docker volumes already shows how to spot and delete an anonymous volume; this lesson is about what you do with one when you no longer remember what it was for.

What happens to an anonymous volume when you remove its container?

Removing a container with docker rm leaves its anonymous volume in place. The volume then becomes orphan: it shows up with dangling=true, and nothing will delete it until you ask. To remove the volume together with the container, you need the -v option, or you must start the container with --rm.

The table below sums up what each action leaves behind. The demonstration removes c41b-m-v1 without -v and checks that its volume stays, orphan: it also serves as the basis for the following sections.

ActionAnonymous volume of the container
docker rm or docker rm -fstays, orphan
docker rm -v or docker rm -fvremoved with the container
docker run --rmremoved when the container stops
Terminal window
docker rm -f c41b-m-v1
docker volume ls --filter dangling=true
c41b-m-v1
DRIVER VOLUME NAME
local 7eecf463ce016e35092868234b465ae42e53e9c80d58b32db65843383677c0d6

As long as c41b-m-v1 existed, it referenced its volume, which was therefore not orphan. After docker rm -f, the volume appears in the list, as the first row of the table says. With -v or --rm, the volume leaves with the container.

A stopped container, and even a container only created and never started, keeps its volume referenced. The test below creates a container from the image that declares VOLUME, lets it stop, then lists the volumes that are still referenced with dangling=false.

Terminal window
docker run --name c41b-m-v3 c41b-m-img:1 true
docker volume ls --filter dangling=false
docker rm -v c41b-m-v3
DRIVER VOLUME NAME
local 6b739925f13f30bf22dfdc4aa3b64984bc6d37d67ae7a95d94284406cbba421f
c41b-m-v3

The volume of c41b-m-v3 appears under dangling=false because the stopped container still exists. Remember it: a volume that is "not mounted" may still be referenced, and that is a good thing, since docker volume rm then refuses to delete it.

How do you find the right volume among several?

You find a volume by its labels, by its name or by the container that references it. Labels are the most direct marker, but they exist only if you set them at creation: a volume with no label can be told apart only by its name or its date. The demonstration therefore creates three named volumes, one of them without a label, and writes data into the first.

Terminal window
docker volume create --label c41b-m-test=1 --label c41b-m-role=donnees c41b-m-data
docker volume create --label c41b-m-test=1 --label c41b-m-role=jetable c41b-m-jetable
docker volume create c41b-m-sanslabel
docker run --rm -v c41b-m-data:/d "$IMAGE" \
sh -c 'echo commande-42 > /d/recu.txt; mkdir /d/sub; echo x > /d/sub/a'
docker volume ls --filter dangling=true
c41b-m-data
c41b-m-jetable
c41b-m-sanslabel
DRIVER VOLUME NAME
local 7eecf463ce016e35092868234b465ae42e53e9c80d58b32db65843383677c0d6
local c41b-m-data
local c41b-m-jetable
local c41b-m-sanslabel

All four volumes are orphan, named or not, and the list does not say which one holds data. The label filter sorts them out. A key alone, or a key=value pair, works, and two label filters combine with AND.

Terminal window
docker volume ls --filter dangling=true --filter label=c41b-m-test
docker volume ls --filter label=c41b-m-role=jetable
docker volume ls --filter label=c41b-m-test=1 --filter label=c41b-m-role=donnees
DRIVER VOLUME NAME
local c41b-m-data
local c41b-m-jetable
DRIVER VOLUME NAME
local c41b-m-jetable
DRIVER VOLUME NAME
local c41b-m-data

The name filter accepts a substring of the name: --filter name=c41b-m- found the three named volumes. The --format Go template prints the labels as key=value,key=value. A volume with no label is recognized by its empty column.

Terminal window
docker volume ls --filter dangling=true --filter name=c41b-m- \
--format '{{.Name}}|{{.Labels}}'
c41b-m-data|c41b-m-role=donnees,c41b-m-test=1
c41b-m-jetable|c41b-m-role=jetable,c41b-m-test=1
c41b-m-sanslabel|

To read one precise value, docker volume inspect --format '{{json .Labels}}' returns a JSON object.

The other way in is the container. docker ps -a --filter volume= lists the containers, stopped or only created, that reference a volume; the output is empty when none does. To see it, create a container that mounts c41b-m-data on /lu read-only, without starting it.

Terminal window
docker create --name c41b-m-user -v c41b-m-data:/lu:ro "$IMAGE" sleep 300
docker ps -a --filter volume=c41b-m-data --format '{{.Names}} {{.Status}}'
docker ps -a --filter volume=c41b-m-jetable --format '{{.Names}} {{.Status}}'
docker ps -a --filter volume=/lu --format '{{.Names}}'
5939ba135634f49dd570b7b35b2cffb2f1e9bf3a74efc0aefa5405e198d84045
c41b-m-user Created
c41b-m-user

The first command finds the container, even with the Created status. The second returns nothing: no container references c41b-m-jetable. The third shows that the filter also accepts a mount point (/lu). As soon as this container exists, c41b-m-data goes from dangling=true to dangling=false, and docker volume rm refuses to delete it.

Terminal window
docker volume rm c41b-m-data
Error response from daemon: remove c41b-m-data: volume is in use - [5939ba135634f49dd570b7b35b2cffb2f1e9bf3a74efc0aefa5405e198d84045]

This refusal is a safeguard: the engine does not delete a referenced volume, even when the container that references it is not running. It also tells you which container holds it. Remove that container before going on.

Terminal window
docker rm c41b-m-user
c41b-m-user

How do you read a volume without attaching it to the service?

To see what a volume contains, mount it read-only in a disposable container. The :ro suffix forbids any write, so reading changes nothing, and --rm removes the container at the end. The volume stays orphan, since the container no longer exists when the command ends.

Terminal window
docker run --rm -v c41b-m-data:/d:ro "$IMAGE" ls -la /d
docker run --rm -v c41b-m-data:/d:ro "$IMAGE" cat /d/recu.txt
total 16
drwxr-xr-x 3 root root 4096 Nov 4 18:32 .
drwxr-xr-x 1 root root 4096 Nov 4 18:32 ..
-rw-r--r-- 1 root root 12 Nov 4 18:32 recu.txt
drwxr-xr-x 2 root root 4096 Nov 4 18:32 sub
commande-42

Look at the size of recu.txt: 12 bytes, that is commande-42 and its trailing newline. The same read checks that the protection works: an attempted write fails with exit code 1.

Terminal window
docker run --rm -v c41b-m-data:/d:ro "$IMAGE" \
sh -c 'echo piege > /d/ecriture.txt'
sh: can't create /d/ecriture.txt: Read-only file system

The long form --mount type=volume,src=c41b-m-data,dst=/d,readonly gives the same result, and reading also works on an anonymous volume designated by its 64-hexadecimal-character name. Here is the anonymous volume from the start, which ANON has designated since the container was created: the first command reads graine.txt, copied from the image, and the second proves that no container references it. Noting this name while the container existed saves you from searching among all the orphan anonymous volumes of the host, some of which are not yours.

Terminal window
docker run --rm -v "$ANON":/d:ro "$IMAGE" cat /d/graine.txt
docker ps -a --filter volume="$ANON"
graine
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

The docker ps -a command prints only its header: no container references this volume. You now know what each candidate contains, which keeps you from deleting a volume on its looks alone.

How do you back up a volume before deleting it?

A backup uses a disposable container that mounts the volume read-only, and a second mount, writable, for the archive. The archive must live outside the volume you may be about to delete, otherwise it disappears with it. Here, a folder on the host plays that role.

Terminal window
mkdir -p /tmp/c41b-m-bak
docker run --rm -v c41b-m-data:/source:ro -v /tmp/c41b-m-bak:/backup \
"$IMAGE" tar -czf /backup/data.tgz -C /source .
ls -l /tmp/c41b-m-bak
total 4
-rw-r--r-- 1 root root 184 Nov 4 18:32 data.tgz

The archive weighs 184 bytes and belongs to root, since the container writes as root. An archive that has not been read back proves nothing. Two complementary reads check it: the list of its contents, which shows that the files are there, and the contents of one precise file, which shows that it is readable.

Terminal window
docker run --rm -v /tmp/c41b-m-bak:/backup:ro "$IMAGE" tar -tzf /backup/data.tgz
docker run --rm -v /tmp/c41b-m-bak:/backup:ro "$IMAGE" \
tar -xzOf /backup/data.tgz ./recu.txt
./
./recu.txt
./sub/
./sub/a
commande-42

The contents of ./recu.txt are indeed commande-42. To detect a later alteration, take the checksum of the archive and then verify it with sha256sum -c: the check answers OK as long as the file is intact.

Terminal window
cd /tmp/c41b-m-bak && sha256sum data.tgz > data.tgz.sha256 && cat data.tgz.sha256 && sha256sum -c data.tgz.sha256
cd ~/donnees-orphelines
6f951f3499084cd184ef95b023f5b12a1544aed2d4d52b24a0d6195f333a9577 data.tgz
data.tgz: OK

A truncated archive is also detected. Cut a copy at 50 bytes, then run the same read: Alpine's tar answers with exit code 1.

Terminal window
head -c 50 /tmp/c41b-m-bak/data.tgz > /tmp/c41b-m-bak/cass.tgz
docker run --rm -v /tmp/c41b-m-bak:/backup:ro "$IMAGE" tar -tzf /backup/cass.tgz
tar: unexpected end of file
tar: short read

The unexpected end of file message signals a cut archive: never delete a volume whose only copy gives this answer. One last test proves that the archive is usable: extracted into a new volume, it gives back the original file.

Terminal window
docker volume create --label c41b-m-test=1 c41b-m-restore
docker run --rm -v c41b-m-restore:/d -v /tmp/c41b-m-bak:/backup:ro \
"$IMAGE" tar -xzf /backup/data.tgz -C /d
docker run --rm -v c41b-m-restore:/d:ro "$IMAGE" cat /d/recu.txt
c41b-m-restore
commande-42

This copy suits a volume that nobody is writing to. For a running database, use the logical export described in Back up and restore a Compose application.

What happens to the volume when a Compose project is renamed?

Compose names its volumes project_volume: changing the project name changes the volume name, so Compose creates a new, empty one, and the old one becomes orphan. The demonstration reproduces it with an Alpine service that keeps a file in its volume: a real PostgreSQL database is not needed to see where the data goes.

The diagram follows the same path as the commands that come next. Look first at the branch of the project c41b-m-app2, which creates an empty volume, then at that of the project c41b-m-app3, which finds the old volume without recreating it.

The project c41b-m-app creates the volume c41b-m-app_dbdata, which holds data. After compose down, the volume is orphan. A project c41b-m-app2 creates an empty volume c41b-m-app2_dbdata. The old volume is found by the label com.docker.compose.project=c41b-m-app, and a project c41b-m-app3 attaches it with external: true, same volume and same creation date.different project nameattached without recreationProject c41b-m-appcompose upVolume c41b-m-app_dbdatadata: ligne-anciennecompose downthe container is removedOrphan volumedangling=trueProject c41b-m-app2compose upVolume c41b-m-app2_dbdataemptyFound by the labelcom.docker.compose.project=c41b-m-appProject c41b-m-app3external: truesame volume, same date
Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

Save this file as ~/donnees-orphelines/projet/compose.yaml. The volume carries a label of your own, so you can find the demonstration again at cleanup time. The exec command writes a file into the volume, then down removes the container.

services:
db:
image: ${IMAGE}
command: ["sleep","600"]
volumes:
- dbdata:/var/lib/data
volumes:
dbdata:
labels:
c41b-m-test: "1"
Terminal window
cd ~/donnees-orphelines/projet
docker compose -p c41b-m-app up -d
docker compose -p c41b-m-app exec db sh -c 'echo "ligne-ancienne" > /var/lib/data/etat.txt'
docker compose -p c41b-m-app down
docker volume ls --filter name=c41b-m-app --filter dangling=true
Network c41b-m-app_default Creating
[...]
Container c41b-m-app-db-1 Started
Container c41b-m-app-db-1 Stopping
[...]
Container c41b-m-app-db-1 Removed
Network c41b-m-app_default Removed
DRIVER VOLUME NAME
local c41b-m-app_dbdata

The volume stayed after down, and it is orphan. Running the same file again under another project name, with the -p option, creates a volume that does not have the data.

Terminal window
docker compose -p c41b-m-app2 up -d
docker compose -p c41b-m-app2 exec db cat /var/lib/data/etat.txt
docker volume ls --filter name=c41b-m-app
cat: can't open '/var/lib/data/etat.txt': No such file or directory
DRIVER VOLUME NAME
local c41b-m-app2_dbdata
local c41b-m-app_dbdata

The file is not found in the new volume: this is the classic symptom of a renamed project, "my data has disappeared". It has not disappeared, it is in the old volume. The com.docker.compose.* labels that Compose sets point to it: com.docker.compose.project holds the name of the old project, and com.docker.compose.volume the name of the volume in the file.

Terminal window
docker volume ls --filter dangling=true --filter label=com.docker.compose.project=c41b-m-app \
--format '{{.Name}} {{.Labels}}'
docker ps -a --filter volume=c41b-m-app_dbdata --format '{{.Names}} {{.Status}}'
docker run --rm -v c41b-m-app_dbdata:/d:ro "$IMAGE" cat /d/etat.txt
docker volume inspect --format '{{.Name}} {{.CreatedAt}}' c41b-m-app_dbdata
c41b-m-app_dbdata c41b-m-test=1,com.docker.compose.config-hash=b84a3080856fee3da845527dc5e95d6594d6379216aca293fa56b9ad3a380d4d,com.docker.compose.project=c41b-m-app,com.docker.compose.version=5.6.1,com.docker.compose.volume=dbdata
ligne-ancienne
c41b-m-app_dbdata 2026-11-04T18:32:45Z

The label filter found the old volume. The second command printed nothing: no container references it. The read-only read confirmed ligne-ancienne, and the last command records the creation date of the volume, to compare further down. On a real host, the backup from the previous section is required before any reattachment; the demonstration skips it because this volume is disposable. Then stop the project that is no longer needed.

Terminal window
docker compose -p c41b-m-app2 down

To reattach the old volume without recreating it, declare it external, with its exact name. Compose does not create it and does not manage it: it mounts it as is. The service only needs to read, so the mount is read-only. Save this file as ext.yaml, in the same folder.

services:
db:
image: ${IMAGE}
command: ["sleep","600"]
volumes:
- dbdata:/var/lib/data:ro
volumes:
dbdata:
external: true
name: c41b-m-app_dbdata
Terminal window
docker compose -p c41b-m-app3 -f ext.yaml up -d
docker compose -p c41b-m-app3 -f ext.yaml exec db cat /var/lib/data/etat.txt
docker compose -p c41b-m-app3 -f ext.yaml exec db sh -c 'echo x > /var/lib/data/n.txt'
docker volume inspect --format '{{.Name}} {{.CreatedAt}}' c41b-m-app_dbdata
Network c41b-m-app3_default Creating
Network c41b-m-app3_default Creating
Network c41b-m-app3_default Created
Network c41b-m-app3_default Created
Container c41b-m-app3-db-1 Creating
Container c41b-m-app3-db-1 Created
Container c41b-m-app3-db-1 Starting
Container c41b-m-app3-db-1 Started
ligne-ancienne
sh: can't create /var/lib/data/n.txt: Read-only file system
c41b-m-app_dbdata 2026-11-04T18:32:45Z

Look at three things. The data is served, the write is refused, and the creation date is the same as the one recorded before the reattachment (2026-11-04T18:32:45Z): the volume was not recreated. No Volume ... Creating line appears in the output of up, some of whose lines show up twice when Compose writes outside an interactive terminal. An external volume is not removed by down -v: stop this project with down -v and check that the volume is still there.

Terminal window
docker compose -p c41b-m-app3 -f ext.yaml down -v
docker volume ls --filter name=c41b-m-app_dbdata
Container c41b-m-app3-db-1 Stopping
[...]
Network c41b-m-app3_default Removed
DRIVER VOLUME NAME
local c41b-m-app_dbdata

Why is a volume name without external dangerous?

Writing only name: c41b-m-app_dbdata in the volumes section, without external: true, seems to work but exposes the volume: Compose does reuse the existing volume, but it considers it its own, and a down -v of the new project deletes it. The following test demonstrates it with nom.yaml, a copy of ext.yaml without the external: true line. It deletes c41b-m-app_dbdata: on the demonstration VM, this volume is of no further use, but never replay this test on a volume that holds real data.

Terminal window
grep -v 'external: true' ext.yaml > nom.yaml
docker compose -p c41b-m-app4 -f nom.yaml up -d
docker compose -p c41b-m-app4 -f nom.yaml exec db cat /var/lib/data/etat.txt
docker compose -p c41b-m-app4 -f nom.yaml down -v
docker volume ls --filter name=c41b-m-app
time="2026-11-04T18:33:18Z" level=warning msg="volume \"c41b-m-app_dbdata\" already exists but was created for project \"c41b-m-app\" (expected \"c41b-m-app4\"). Use `external: true` to use an existing volume"
Network c41b-m-app4_default Creating
[...]
ligne-ancienne
Container c41b-m-app4-db-1 Removing
Volume c41b-m-app_dbdata Removing
Volume c41b-m-app_dbdata Removed
DRIVER VOLUME NAME
local c41b-m-app2_dbdata

The warning of the first up names the problem, but it stops nothing: the data is served. Then down -v deleted c41b-m-app_dbdata, the volume of another project, which the final list no longer shows. Only external: true protects the volume, and that is why the backup comes before any reattachment. Once the volume is deleted, an external file that designates it does not recreate it, and replaying ext.yaml shows it.

Terminal window
docker compose -p c41b-m-app3 -f ext.yaml up -d
echo "exit code: $?"
external volume "c41b-m-app_dbdata" not found
exit code: 1

This is a safeguard: a typo in the name does not silently produce an empty volume.

Why does a down without -v leave an image volume orphaned?

A docker compose down without -v abandons the anonymous volume that comes from an image's VOLUME. This volume does not have the com.docker.compose.project label: the project filter does not find it, and no later down -v will remove it, since Compose no longer ties it to the project. The demonstration reuses the c41b-m-img:1 image built earlier, with a service that declares no volume. It chains an up, a down without -v, a second up and a down -v.

Save this file as anon.yaml, in the project folder.

services:
app:
image: c41b-m-img:1
Terminal window
cd ~/donnees-orphelines/projet
docker compose -p c41b-m-anon -f anon.yaml up -d
V=$(docker inspect -f '{{range .Mounts}}{{.Name}}{{end}}' c41b-m-anon-app-1)
docker compose -p c41b-m-anon -f anon.yaml down
docker volume ls --filter dangling=true --filter label=com.docker.compose.project=c41b-m-anon
docker volume inspect --format '{{json .Labels}}' "$V"
docker compose -p c41b-m-anon -f anon.yaml up -d
docker compose -p c41b-m-anon -f anon.yaml down -v
docker volume ls -q --filter name="$V"
Network c41b-m-anon_default Creating
[...]
Network c41b-m-anon_default Removed
DRIVER VOLUME NAME
{"com.docker.volume.anonymous":""}
Network c41b-m-anon_default Creating
[...]
Network c41b-m-anon_default Removed
4ca8a39ca46f1e2bedbf3171ec38709cc882d529d94977f478bccd9c6565c8f5

The first list by label is empty: the volume has no project label, only {"com.docker.volume.anonymous":""}. The down without -v therefore left it orphan. The second up created a different volume from the first, and down -v did remove the one of the current container: the volume printed by the last command is the first one, the one that the down without -v had abandoned. A down -v finds only what Compose ties to the project, and this volume is not tied. It can be spotted only by dangling=true and by its contents. Read it back read-only as above before deleting it by name, and back it up if it holds data.

Terminal window
docker volume rm "$V"
4ca8a39ca46f1e2bedbf3171ec38709cc882d529d94977f478bccd9c6565c8f5

What does docker volume prune remove, with and without --all?

docker volume prune deletes the orphan volumes, but without --all it removes only anonymous volumes. With --all, it also removes orphan named volumes. A volume referenced by a container, even one only created, is never deleted. The default prompt recalls it: WARNING! This will remove anonymous local volumes not used by at least one container.

The demonstration prepares a precise state, under a common label c41b-m-prune, distinct from c41b-m-test: a prune with --all would otherwise also target the volumes of the previous sections. Three labelled anonymous volumes are created: that of a started container, that of a container only created, and that of a container removed without -v, which is the only orphan of the three. On the named volumes side: one labelled orphan, one attached to a created container, and one without a label.

Terminal window
docker run -d --name c41b-m-run \
--mount type=volume,dst=/a,volume-label=c41b-m-prune=1 "$IMAGE" sleep 600
docker create --name c41b-m-stopped \
--mount type=volume,dst=/a,volume-label=c41b-m-prune=1 "$IMAGE" true
docker create --name c41b-m-gone \
--mount type=volume,dst=/a,volume-label=c41b-m-prune=1 "$IMAGE" true
docker rm c41b-m-gone
docker volume create --label c41b-m-prune=1 c41b-m-n-orphan
docker volume create --label c41b-m-prune=1 c41b-m-n-used
docker volume create c41b-m-n-nolabel
docker create --name c41b-m-nuser -v c41b-m-n-used:/n "$IMAGE" true
docker volume ls --filter dangling=true --filter label=c41b-m-prune -q
3ae78b972bc95ecd2281bb9d0463f6a3c82b5f9140202a65feed5a3090807f4e
db0482628760f62726383aeda6acb107b03bf0cb9ae8fe6d84d537765caaaf2b
1559ae4ae4a1ec0a224a00adac21f7d37506c9f9684d7b2f56e4e2d0281ff753
c41b-m-gone
c41b-m-n-orphan
c41b-m-n-used
c41b-m-n-nolabel
f27cd73187864635f0035d431728c2e5e43a596df81ef7fe397226723b865321
5f55b44d6625bfa2b74c89b5a35e93e8efce8590759be3d8670b56ac8ab478bc
c41b-m-n-orphan

The volume-label option of --mount sets a label on the anonymous volume that Docker creates: it is the way to mark it without giving it a name. The first lines of the output are the identifiers and names that the creation commands print; the last two are the list, which shows what prune can reach: one anonymous and one named volume. Without -f, docker volume prune asks for confirmation, and with an empty stdin it cancels and deletes nothing. Here is the prune without --all:

Terminal window
docker volume prune -f --filter label=c41b-m-prune
docker volume ls --filter label=c41b-m-prune -q
docker volume ls -q --filter name=c41b-m-n-
Deleted Volumes:
5f55b44d6625bfa2b74c89b5a35e93e8efce8590759be3d8670b56ac8ab478bc
Total reclaimed space: 0B
05da6f2d3ddd5b08c531e7550f0ac4bc8621870b65016758fb337c1362823109
c41b-m-n-orphan
c41b-m-n-used
cd23fa2890790174c466808d3df4d697ef3b07cedda6dfc4b1d3ea497ee8082d
c41b-m-n-nolabel
c41b-m-n-orphan
c41b-m-n-used

Only the orphan anonymous volume is gone. What remains is the two anonymous volumes attached to a container, the orphan named volume, the attached named volume and the named volume without a label. The same prune with --all additionally removes the labelled orphan named volume.

Terminal window
docker volume prune -f --all --filter label=c41b-m-prune
docker volume ls --filter label=c41b-m-prune -q
docker volume ls -q --filter name=c41b-m-n-
Deleted Volumes:
c41b-m-n-orphan
Total reclaimed space: 0B
05da6f2d3ddd5b08c531e7550f0ac4bc8621870b65016758fb337c1362823109
c41b-m-n-used
cd23fa2890790174c466808d3df4d697ef3b07cedda6dfc4b1d3ea497ee8082d
c41b-m-n-nolabel
c41b-m-n-used

The named volume attached to a container that never started, and the named volume without a label, stayed: the label of the filter protects the second, and the container's reference protects the first. --all is therefore the action that deletes named data: use it only with a filter that designates nothing but the disposable. The prune also refuses the filter you would need to target the orphans: dangling is not a valid filter for prune.

Terminal window
docker volume prune -f --all --filter label=c41b-m-prune --filter dangling=true
Error response from daemon: invalid filter 'dangling'

An anonymous volume that comes from an image's VOLUME has no usable label: a prune without a filter would touch all the orphan anonymous volumes of the host, including those that are not yours. Delete it by its name, like any volume you have doubts about.

How do you remove only what is disposable?

Delete by exact name, after checking the three pieces of information that decide: the labels, the container that references the volume, and the contents. Each check rules out a dangerous case: a label that does not match the project, a stopped container that holds the volume, contents that nobody has backed up.

Terminal window
docker volume inspect --format '{{json .Labels}}' c41b-m-jetable
docker ps -a --filter volume=c41b-m-jetable --format '{{.Names}} {{.Status}}'
docker run --rm -v c41b-m-jetable:/d:ro "$IMAGE" ls -la /d
docker volume rm c41b-m-jetable
{"c41b-m-role":"jetable","c41b-m-test":"1"}
total 8
drwxr-xr-x 2 root root 4096 Nov 4 18:32 .
drwxr-xr-x 1 root root 4096 Nov 4 18:33 ..
c41b-m-jetable

The four commands answered in order: the label, an absent container (the output is empty), the empty contents of the folder, then the name of the deleted volume.

If the volume is still referenced, docker volume rm answers volume is in use and stops: do not force it, first remove the container that holds it, after checking that it is disposable too. To make the next sorting possible, set your own labels at creation: --label for docker volume create, and labels: under the volume in compose.yaml. The c41b-m-data volume of this lesson carried c41b-m-role=donnees, and that label is what set it apart from c41b-m-jetable at a glance.

How do you clean up the demonstration?

First remove the containers, then the volumes, the image and the folders created for the lesson. Each command targets the c41b-m- prefix, never a whole category: your other resources stay in place. The cleanup deletes the volumes of the demonstration by their labels and names, and the anonymous volume of c41b-m-v1 by its exact name.

Terminal window
cd ~
docker ps -aq --filter name=c41b-m- | xargs -r docker rm -fv
docker volume rm "$ANON"
docker volume ls -q --filter label=c41b-m-test | xargs -r docker volume rm
docker volume ls -q --filter label=c41b-m-prune | xargs -r docker volume rm
docker volume ls -q --filter name=c41b-m- | xargs -r docker volume rm
docker volume ls -q --filter label=com.docker.compose.project=c41b-m-app | xargs -r docker volume rm
docker image ls -q --filter reference='c41b-m-*' | xargs -r docker image rm
rm -rf /tmp/c41b-m-bak ~/donnees-orphelines
docker volume ls
DRIVER VOLUME NAME

The last command must return the starting list. The anonymous volume of c41b-m-v1 has no label: the cleanup deleted it separately, by the name that ANON has kept since the beginning. The Alpine image pulled for the lesson stays in place.

Put it into practice

The lab has you practice the same sequence on a Compose project whose service has lost its volume. Two volumes are orphan, one holds data to keep, the other is disposable. You reconnect the volume to keep to the service, read-only, without recreating it, then you delete only the disposable volume, after reading its labels. The statement forbids docker volume prune, docker system prune and the deletion of a volume without a label or unrelated to the lab. Its tests check that a service is running with the original volume mounted read-only, that its ownership labels are intact, that its creation date has not changed, that the data served is identical to what is expected, and that the disposable volume is gone. The names, paths and values differ from those of the demonstration.

Key points

An orphan volume is judged on its contents, not on its status.

  • A volume is orphan when no container, even stopped or only created, references it: dangling=true says nothing about the value of its data.
  • docker rm leaves the container's anonymous volume; docker rm -v and docker run --rm remove it with the container.
  • You find a volume by its labels, by --filter name= (substring) or by docker ps -a --filter volume=, then you read its contents with a disposable container mounted with :ro.
  • You back up with tar outside the volume, then you read the archive back with tar -tzf and sha256sum -c: unexpected end of file signals a truncated archive.
  • A renamed Compose project creates an empty volume; the old one is found by com.docker.compose.project and reattached with external: true, never with name: alone, whose volume down -v deletes.
  • A docker compose down without -v abandons the anonymous volume of an image's VOLUME: it has no project label, and no later down -v finds it. Delete it by its name.
  • docker volume prune without --all removes orphan anonymous volumes, with --all orphan named volumes too; a label filter bounds the effect, and the dangling filter is refused.
  • You delete by exact name, after reading the labels, the container and the contents.

Knowledge check

Check that you can predict what a deletion leaves or removes, and that you find the right volume before acting.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn