Ch. 13 · Docker

Docker Volumes: When Container Data Survives or Gets Deleted

Named and anonymous volumes outlive containers in different ways. Trace which commands keep data, which delete it, and how to back it up.

~5 min readintermediateupdated Oct 7, 2026

Containers are meant to be thrown away. Volumes are where you put the things you don’t want thrown away with them. The interview question isn’t really “what is a volume” but “when does the data disappear”, and the answer depends on which kind of volume you have and which command you ran.

All the commands below were run on Docker Engine with the local volume driver and an alpine:3.20 image. Container and volume names are shortened for readability.

A named volume survives the container

Create one, write to it from one container, read it from another:

docker volume create data
docker run --rm -v data:/data alpine:3.20 sh -c 'echo v1 > /data/state.txt'
docker run --rm -v data:/data alpine:3.20 cat /data/state.txt
Terminal
v1
Text

The first container was removed the moment it exited (--rm), yet the file is still there. Docker manages the storage location:

$ docker volume inspect data --format '{{.Driver}} {{.Mountpoint}}'
local /var/lib/docker/volumes/data/_data
Text

On Linux that path is on the host. On Docker Desktop for Mac or Windows it’s inside the Linux VM, so you won’t find it in Finder, which is a frequent source of confusion. Either way, don’t edit files there directly; go through a container.

A volume that’s attached to a container, running or stopped, can’t be removed:

$ docker volume rm data
Error response from daemon: remove data: volume is in use - [5c2eb71e2f69...]
Text

That message is a safety net. If you see it during cleanup, find the container with docker ps -a --filter volume=data before deciding anything.

Anonymous volumes and how they pile up

An anonymous volume is one Docker names for you with a 64-character hex ID. You get one when you write -v /cache with no name, or when the image declares VOLUME /cache and you don’t mount anything there. Many official images do this: Postgres declares /var/lib/postgresql/data, for instance.

Here’s a test image with a VOLUME instruction:

FROM alpine:3.20
VOLUME /cache
CMD ["sh", "-c", "date > /cache/t"]
dockerfile

Running it without --rm creates and keeps an anonymous volume:

$ docker run --name v2 volimage
$ docker inspect v2 --format '{{range .Mounts}}{{.Type}} {{.Name}} -> {{.Destination}}{{end}}'
volume dc3ca4e841095605be35a39e6ff126bf343419ecbcd3d527a6d9ca92d81657b0 -> /cache
Text

What happens next depends on how you remove things:

Action Named volume Anonymous volume
docker stop / docker start kept kept
docker rm kept kept, now orphaned
docker rm -v kept deleted
docker run --rm (on exit) kept deleted
docker compose down kept kept, orphaned
docker compose down -v deleted deleted
docker volume prune kept (since Engine 23) deleted if unused

Counting docker volume ls before and after confirms the rm -v and --rm rows: no volume is left behind in either case, while a plain docker rm leaves one. A Compose down followed by up doesn’t reattach the old anonymous volume either; it simply stays on disk. Orphaned anonymous volumes are the classic reason a CI host’s disk fills up; every test run of a Postgres image leaves one behind. The no space left on device guide covers cleaning those up safely.

The other trap with anonymous volumes is the one from the image/container question: you recreate a database container, a fresh anonymous volume gets attached, and the database looks empty. The old data is in some other hex-named volume. Name your volumes.

Compose: where down -v deletes your database

In Compose, top-level volumes: entries become named volumes prefixed with the project name:

name: shop
services:
  app:
    image: alpine:3.20
    command: ["sh", "-c", "date >> /data/log; cat /data/log"]
    volumes:
      - appdata:/data
volumes:
  appdata:
yaml
$ docker compose up
 Volume shop_appdata Created
...
$ docker compose down
 Container shop-app-1 Removed
 Network shop_default Removed
$ docker volume ls
DRIVER    VOLUME NAME
local     shop_appdata
$ docker compose down -v
 Volume shop_appdata Removed
Text

Plain down keeps the data, so the next up sees every line appended so far. down -v is the reset button. It’s useful in a local dev loop and catastrophic when someone pastes it from a README onto a shared staging box. If a volume must never be deleted by Compose, declare it external: true and create it outside the project; down -v never removes external volumes.

Renaming the project (a different directory name, or a changed name:) also changes the volume name. The old volume still exists, the stack just doesn’t look for it any more. “Compose lost my data after I moved the folder” is usually this.

The first-mount copy and why upgrades don’t change it

When you mount an empty volume onto a path where the image already has files, Docker copies the image’s files into the volume first. Mounting a fresh volume over Alpine’s /etc/apk:

$ docker run --rm -v seed:/etc/apk alpine:3.20 ls /etc/apk
arch
keys
protected_paths.d
repositories
world
Text

With --mount type=volume,src=nocopy,dst=/etc/apk,volume-nocopy the copy is skipped and the directory is empty. Bind mounts never do this copy; they always hide what’s underneath.

The copy happens only once, when the volume is empty. After that, the volume’s contents win. So if version 2 of your image ships new default config files in a path that’s backed by a volume, containers keep showing version 1’s files. This bites people who put application code or static assets in a volume: they deploy a new image and the old assets stay. Volumes are for state the application writes, not for things the image provides.

Backing up and moving a volume

There’s no docker volume export. The standard method is a throwaway container that mounts the volume read-only and a host directory for the archive:

docker run --rm \
  -v data:/data:ro \
  -v "$PWD/backup":/backup \
  alpine:3.20 tar czf /backup/data.tgz -C /data .
Terminal
$ tar tzf backup/data.tgz
./
./state.txt
Text

Restore is the same in reverse (tar xzf into a new volume). For a live database, a filesystem copy taken while it’s writing can be inconsistent; use the database’s own dump tool (pg_dump, mysqldump) or stop the container first. Volume drivers other than local (NFS, cloud block storage) change where the bytes live but not these lifecycle rules.

Interview practice

Your team’s compose.yaml runs Postgres with a named volume pgdata. A developer reports that after pulling the latest changes and running docker compose up, the database is empty, but nobody ran down -v. The diff shows someone added name: payments at the top of the Compose file. What happened and how do you recover?

Answer

Compose prefixes volume names with the project name. Before the change the project name came from the directory, say backend, so the data lived in backend_pgdata. With name: payments the project is now payments, Compose looked for payments_pgdata, didn’t find it, and created a new empty volume. Nothing was deleted.

Confirm with docker volume ls: both backend_pgdata and payments_pgdata should be listed. To recover, either point the service at the old volume (declare pgdata with name: backend_pgdata and external: true), or copy the data across with a temporary container that mounts both volumes and runs cp -a. Stop the database first so the copy is consistent. Going forward, setting explicit name: values on important volumes makes them independent of the project name.

Keep going

More in Docker

read ✓Docker · mid

Docker Bind Mounts and Host Coupling

Bind mounts map a host path into a container: great for live code, risky for ownership, portability and security. See where each one bites.

~6 min readread →
read ✓Docker · mid

Docker Compose Dependencies and Readiness

Plain depends_on only orders container starts. Use healthchecks, service_healthy and service_completed_successfully to wait for a ready database.

~5 min readread →
read ✓Docker · mid

Docker Health Checks and Useful Probes

How a HEALTHCHECK moves from starting to unhealthy, why plain Docker never restarts an unhealthy container, and what a good probe checks.

~5 min readread →
esc