That moment: you run docker volume rm, expecting a clean sweep, and Docker replies with a smug little error: volume is in use. Production is waiting. Your pager is warming up. And the volume name you want to delete is the one your application team promised “is totally unused now.”
The trick is that Docker is usually telling the truth—just not the whole truth. Volumes can be held by stopped containers, orphaned Compose projects, Swarm tasks, plugin drivers, or a stale definition you forgot existed. This guide is how you remove or replace a Docker volume without losing data, and without doing the classic “rm -rf and pray” routine.
What Docker means by “volume is in use”
Docker’s “volume is in use” message is not poetic. It’s a specific refusal: Docker won’t delete a volume if any container—running or stopped—still references it in its container configuration. Docker tracks this at the metadata level (container config + mount references), not by checking whether any process currently has files open.
That distinction matters. A volume can be “in use” even when:
- The container is exited but still exists.
- The container was created by Compose months ago, and nobody cleaned it up.
- A Swarm service task is still registered, even if it’s restarting on another node.
- A container was replaced, but the old one is still around as a ghost with a meaningful name like
myapp_web_1.
Also: Docker makes a clear separation between volumes (managed by Docker, usually under /var/lib/docker/volumes) and bind mounts (your host path). Bind mounts don’t have “volume is in use” the same way; instead you’ll destroy your own data like an adult.
One more subtlety: the error is usually triggered by docker volume rm refusing to proceed. If you try to brute-force with system cleanup commands, you can delete more than you intended. The goal here is targeted removal or replacement, with a data migration plan you can explain to an auditor without sweating.
Fast diagnosis playbook
If you only have five minutes before someone asks “can we just restart the host?”, do this. In order. Don’t freestyle.
First: identify the volume and its driver
- Inspect the volume (name, driver, labels, mountpoint).
- If the driver is not
local, stop and read twice: external drivers have different semantics and may involve remote storage.
Second: find every container referencing it (running or stopped)
- List containers attached to the volume via filter.
- Inspect suspicious containers and confirm the mount list includes your volume.
Third: decide between three actions
- Unblock deletion: remove the referencing container(s), then remove the volume.
- Replace without data loss: copy data into a new volume, then repoint containers/Compose to it.
- Keep but detach: stop containers, update config to stop using the volume, and keep it around as a backup.
Fourth: verify data safety
- Take a snapshot-like backup (tar export) of the volume contents.
- Do a cheap integrity check: expected files exist, sizes look sane, and app can read them after remount.
Nothing here is glamorous. It’s the point. The glamorous fixes are the ones you regret later.
A few facts and historical context (because it changes how you debug)
- Docker volumes existed early, but matured later. Early Docker users leaned heavily on bind mounts; named volumes became the default “safe place” as operational patterns matured.
- Docker’s volume “in use” check is metadata-driven. Docker won’t delete a volume referenced by any container definition, even if the container is stopped and nothing has the files open.
- Compose labels changed how volumes get “orphaned.” Docker Compose attaches labels like project and service; you can use those labels to track where a volume came from, but also to discover abandoned stacks.
- Overlay filesystems are not your volume. The writable layer of a container (overlay2) is different from a volume. Deleting a container deletes the writable layer, but not named volumes.
- Swarm introduced distributed scheduling, not distributed storage. People confuse these. Swarm can reschedule tasks; your local volume cannot teleport to another node.
- Volume drivers matter.
localbehaves like local directories. Plugin drivers (NFS, cloud volumes, CSI-ish integrations) can have their own attach/detach rules and failure modes. - “Anonymous volumes” are a known footgun. They are created automatically (especially in older Compose patterns) and can pile up. They’re harder to track because the name looks like a hash.
- Prune commands became popular after disk incidents. Disk pressure pushed teams toward
docker system prune. It helps—until it removes something you thought was “temporary.”
Golden rules: how not to lose data
- Assume the volume contains data you care about until proven otherwise. “It’s just cache” has destroyed more databases than malware.
- Never delete a volume until you can name what uses it. If you can’t, you’re not deleting it; you’re gambling.
- Back up before you migrate. “But we’re copying it” is not a backup. Copy operations fail in creative ways: permissions, special files, broken symlinks, partial transfers, and silent truncation if the filesystem is full.
- Prefer making a new volume and migrating data into it. Replacing in place is tempting but leaves you with no rollback story.
- Be precise with cleanup commands. If you reach for
docker system prune --volumesin production, at least write your postmortem outline first. It’ll save time later.
Joke #1: If you ever feel useless, remember there’s a “force delete” button in a production environment. It’s there mostly to test your backup strategy.
One quote, because operations is a discipline, not a vibe. Tom Limoncelli has a paraphrased idea often repeated in SRE circles: paraphrased idea: If it’s not documented and practiced, it’s not a procedure; it’s a wish.
— Tom Limoncelli (paraphrased idea).
Hands-on tasks: commands, outputs, and decisions
These are the practical moves. Each task gives you a command, a realistic kind of output, and the decision you make. Run them in order when you’re under pressure; cherry-pick when you’re doing planned maintenance.
Task 1: Confirm the error and the exact volume name
cr0x@server:~$ docker volume rm pgdata
Error response from daemon: remove pgdata: volume is in use - [2d8c1f8b6a1d5e2f6f4e62b2d64a0d1b8a5b0a7c7d0f1a2b3c4d5e6f7a8b9c0d]
What it means: Docker found at least one container referencing the volume. The long hex is a container ID.
Decision: Don’t try again. Find the referencing container(s) and decide whether to remove or migrate.
Task 2: Inspect the volume (driver, labels, mountpoint)
cr0x@server:~$ docker volume inspect pgdata
[
{
"CreatedAt": "2025-11-20T09:12:13Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "billing",
"com.docker.compose.volume": "pgdata"
},
"Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
"Name": "pgdata",
"Options": null,
"Scope": "local"
}
]
What it means: It’s a local named volume created by Compose project billing. This is your first breadcrumb.
Decision: If Driver is not local, pause and identify the storage backend and detach semantics. If it is local, proceed with container discovery and migration options.
Task 3: Find containers that reference the volume (fast filter)
cr0x@server:~$ docker ps -a --filter volume=pgdata --format 'table {{.ID}}\t{{.Names}}\t{{.Status}}'
CONTAINER ID NAMES STATUS
2d8c1f8b6a1d billing-postgres-1 Up 3 days
c91a44dd210e billing-postgres-old Exited (0) 2 weeks ago
What it means: Two containers reference it. One is running, one is exited but still blocking deletion.
Decision: If you want to delete the volume, you must remove both containers (or at least remove their reference by removing the containers). If you want to replace the volume, you’ll migrate and then recreate containers with the new volume.
Task 4: Confirm mount usage inside the container config
cr0x@server:~$ docker inspect 2d8c1f8b6a1d --format '{{json .Mounts}}'
[{"Type":"volume","Name":"pgdata","Source":"/var/lib/docker/volumes/pgdata/_data","Destination":"/var/lib/postgresql/data","Driver":"local","Mode":"z","RW":true,"Propagation":""}]
What it means: The container is definitely using pgdata and mounting it at the PostgreSQL data directory.
Decision: Treat it as database storage. Plan a controlled stop, consistent backup, and migration. No cowboy copies while the database is writing.
Task 5: Identify the Compose project from labels (who owns this)
cr0x@server:~$ docker inspect 2d8c1f8b6a1d --format '{{json .Config.Labels}}'
{"com.docker.compose.project":"billing","com.docker.compose.service":"postgres","com.docker.compose.version":"2.24.6"}
What it means: This is Compose-managed, not a random manual docker run.
Decision: Prefer changes through Compose (or at least keep Compose state consistent), otherwise the next docker compose up may recreate old things and surprise you.
Task 6: See what’s in the volume (quick sanity check)
cr0x@server:~$ sudo ls -lah /var/lib/docker/volumes/pgdata/_data | head
total 132K
drwx------ 19 999 999 4.0K Jan 3 10:11 .
drwx-----x 3 root root 4.0K Nov 20 09:12 ..
-rw------- 1 999 999 3 Jan 3 10:11 PG_VERSION
drwx------ 6 999 999 4.0K Jan 3 10:11 base
drwx------ 2 999 999 4.0K Jan 3 10:11 global
drwx------ 2 999 999 4.0K Jan 3 10:11 pg_wal
What it means: It’s a real Postgres data directory. Permissions match typical container UID usage. Deleting this would be an “update your resume” event.
Decision: If you must replace the volume, do it with a controlled database dump/restore or filesystem-level copy while stopped, depending on RPO/RTO and DB type.
Task 7: Stop the right container(s) cleanly
cr0x@server:~$ docker stop billing-postgres-1
billing-postgres-1
What it means: Postgres is stopping. If your container has a proper stop signal, it should checkpoint and exit cleanly.
Decision: If stop hangs, don’t jump to docker kill immediately. First check logs and health. Forced kills increase the chance you’ll copy inconsistent data.
Task 8: Verify no containers are still referencing the volume (including exited ones)
cr0x@server:~$ docker ps -a --filter volume=pgdata --format 'table {{.ID}}\t{{.Names}}\t{{.Status}}'
CONTAINER ID NAMES STATUS
2d8c1f8b6a1d billing-postgres-1 Exited (0) 3 seconds ago
c91a44dd210e billing-postgres-old Exited (0) 2 weeks ago
What it means: Both containers still exist and still reference the volume.
Decision: If you’re trying to remove the volume, you must remove these containers. If you’re migrating, keep them until you finish backup, then remove or recreate with the new volume.
Task 9: Create a backup tarball of the volume (cheap insurance)
cr0x@server:~$ docker run --rm -v pgdata:/data -v $PWD:/backup alpine sh -c 'cd /data && tar -cpf /backup/pgdata-backup.tar .'
What it means: You used a temporary container to read the volume and write a tar to your current directory on the host.
Decision: Validate the tar exists and has plausible size. If you’re space-limited, write it to a mounted backup filesystem, not your root disk.
Task 10: Verify backup size and basic contents
cr0x@server:~$ ls -lh pgdata-backup.tar
-rw-r--r-- 1 cr0x cr0x 1.3G Jan 3 10:19 pgdata-backup.tar
cr0x@server:~$ tar -tf pgdata-backup.tar | head
./
./PG_VERSION
./base/
./global/
./pg_wal/
What it means: The tar exists and contains the expected structure.
Decision: You now have a rollback artifact. Still not a perfect database backup, but enough to recover files if you make a mistake during volume replacement.
Task 11: Create the new replacement volume
cr0x@server:~$ docker volume create pgdata_v2
pgdata_v2
What it means: New empty named volume created.
Decision: Use a clearly versioned name. Avoid naming it “new” unless you enjoy archaeology later.
Task 12: Copy data from old volume to new volume (offline copy)
cr0x@server:~$ docker run --rm -v pgdata:/from -v pgdata_v2:/to alpine sh -c 'cd /from && cp -a . /to'
What it means: File-level copy using cp -a to preserve permissions and timestamps.
Decision: Only do this for data that is safe to copy offline (container stopped). For databases, preferred approach is dump/restore when you need consistency guarantees across files; file copy works if the DB is fully stopped and not crash-recovering mid-copy.
Task 13: Compare directory shape quickly (spot obvious copy failures)
cr0x@server:~$ docker run --rm -v pgdata:/from -v pgdata_v2:/to alpine sh -c 'cd /from && find . -maxdepth 2 -type d | sort | tail -n 5 && echo "---" && cd /to && find . -maxdepth 2 -type d | sort | tail -n 5'
./pg_commit_ts
./pg_dynshmem
./pg_logical
./pg_notify
./pg_stat_tmp
---
./pg_commit_ts
./pg_dynshmem
./pg_logical
./pg_notify
./pg_stat_tmp
What it means: The directory layout matches at a quick glance.
Decision: Proceed, but don’t confuse “looks similar” with “bit-for-bit consistent.” For critical systems, do app-level checks after restart.
Task 14: Remove the old containers that keep the old volume “in use”
cr0x@server:~$ docker rm billing-postgres-old
billing-postgres-old
cr0x@server:~$ docker rm billing-postgres-1
billing-postgres-1
What it means: Containers are removed; their references to the volume are gone.
Decision: Now you can remove pgdata if you truly want, or keep it around until the new setup proves stable.
Task 15: Recreate the container (or Compose stack) pointing at the new volume
cr0x@server:~$ docker run -d --name billing-postgres-1 \
-e POSTGRES_PASSWORD=redacted \
-v pgdata_v2:/var/lib/postgresql/data \
postgres:16
8b2fa1d22b2c4b8f8f0e6e7f3a2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d
What it means: New container is running with the replacement volume.
Decision: Validate application health. If this is Compose-managed, translate the change into compose.yaml and run via Compose instead of ad-hoc docker run long-term.
Task 16: Confirm the new container uses the new volume
cr0x@server:~$ docker inspect billing-postgres-1 --format '{{range .Mounts}}{{.Name}} -> {{.Destination}}{{"\n"}}{{end}}'
pgdata_v2 -> /var/lib/postgresql/data
What it means: The new container mount is correct.
Decision: Only now consider removing the old volume.
Task 17: Remove the old volume (optional, after validation)
cr0x@server:~$ docker volume rm pgdata
pgdata
What it means: Docker allowed deletion because no containers reference the volume.
Decision: If you want an additional safety net, don’t delete yet—rename or keep it for a few days, depending on your change policy and disk budget.
Task 18: Hunt anonymous/orphaned volumes and understand what you’re about to prune
cr0x@server:~$ docker volume ls --format 'table {{.Name}}\t{{.Driver}}'
NAME DRIVER
billing_cache local
pgdata_v2 local
b3f3b9d9d79e0a9dfb2a2d8c6b77d8b8c0a... local
What it means: That long hash-like name is often an anonymous volume. Sometimes harmless. Sometimes it’s the only copy of someone’s upload directory because “temporary.”
Decision: Inspect before pruning. You’re not paid to be brave; you’re paid to be right.
Replace a volume safely (three patterns)
Pattern A: “Lift-and-shift” file copy (good for static data, careful with databases)
This is what we did above with cp -a via a transient Alpine container. It’s fast, it’s simple, and it’s correct when the application is fully stopped and the data format is file-consistent when offline.
Works well for:
- Web content directories
- Static configuration bundles
- Artifact caches you can rebuild (still, don’t delete blindly)
- Databases only when fully stopped and you accept crash-recovery semantics
Avoid for:
- Live databases (copying while running) unless you really enjoy corrupted WAL segments
- Apps that maintain multiple files with cross-file consistency requirements while running
Pattern B: App-level migration (dump/restore) for strong consistency
If you need correctness more than speed, do the app-level export/import. For PostgreSQL that’s typically pg_dump / pg_restore or physical backup tooling. For MySQL, mysqldump or physical backup tools. For Elasticsearch, snapshot/restore. The point is: you migrate the data in a supported way, not by copying the engine’s internal files mid-flight.
Operationally, this pattern is slower but predictable. It’s also the only migration that gives you a story you can defend during an incident review: “We used the supported backup method.”
Pattern C: Keep the volume, replace the containers (when the volume isn’t the problem)
Sometimes you don’t need to replace the volume at all. You just need to delete containers that reference it so you can remove another volume, or you need to rebuild an app container while keeping data intact.
In that case:
- Stop the container.
- Remove the container (not the volume).
- Recreate container pointing to the same named volume.
This is the safest move when the data is precious and your only issue is a container config change.
Compose and Swarm: special ways to get fooled
Compose: volumes persist even when you think you “took the stack down”
docker compose down removes containers and networks by default, but it does not remove named volumes unless you add --volumes. That’s usually good. It also means the volume stays around and can accumulate drift: schema upgrades, old migrations, files nobody remembers.
Compose also helps you identify owners through labels. Use them. They exist for a reason.
Swarm: tasks and scheduling amplify confusion
In Swarm, a service task may be “shutdown” but still exists in history. Meanwhile your named volume exists on a single node. If you think “Swarm will move it,” you’re confusing orchestration with storage.
Swarm + local volumes is fine if you pin services to nodes and understand failover. If you want storage mobility, you need a driver/backing store that supports it (and you need to test it under failure, not in a slide deck).
Joke #2: Swarm will happily reschedule your container to a node that doesn’t have the data. It’s not malicious—just optimistic.
Common mistakes (symptom → root cause → fix)
1) “docker volume rm says in use, but docker ps shows nothing running”
Symptom: No running containers, volume still “in use.”
Root cause: Stopped containers still exist and still reference the volume.
Fix: List all containers, including exited, filtered by volume. Remove the containers (or recreate them with a different volume).
cr0x@server:~$ docker ps -a --filter volume=pgdata
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c91a44dd210e postgres:16 "docker-entrypoint.s…" 2 weeks ago Exited (0) 2 weeks ago billing-postgres-old
2) “I deleted the container and Docker still won’t delete the volume”
Symptom: You removed one container, still blocked.
Root cause: More than one container references the volume (often old/renamed/failed deployments).
Fix: Use the volume filter and remove all referencing containers.
3) “We replaced the volume and the app booted… with an empty dataset”
Symptom: App starts fresh, missing state.
Root cause: New volume was mounted, but data was never migrated (or migrated to the wrong path/volume).
Fix: Confirm mount names and destinations with docker inspect. Copy data offline or restore from backup tar. Then redeploy.
4) “Copy succeeded, but permissions are broken”
Symptom: App fails with permission denied errors after migration.
Root cause: Copy method didn’t preserve ownership/mode bits (e.g., using cp without -a, or extracting tar as root with wrong flags, or copying through a Windows host path).
Fix: Use cp -a or tar with preserved ownership. Validate UID/GID expectations inside the container.
5) “docker system prune –volumes removed something critical”
Symptom: Data loss. Sudden app reinitialization. Angry people.
Root cause: Prune removed volumes not currently referenced by any container, even if they contained valuable state or were intended as a backup.
Fix: Stop using prune as a scalpel. Before pruning, inventory volumes and label/tag anything valuable. Keep explicit backups outside Docker volume management.
6) “The volume is local, but the host disk is full”
Symptom: Writes fail, containers crashloop, migrations fail mid-transfer.
Root cause: Docker volumes share the host filesystem. If root fills, everything gets weird.
Fix: Check disk usage before and after migration. Move Docker data root or storage, or expand capacity. Don’t attempt a huge tarball on a nearly full root disk.
Checklists / step-by-step plan
Checklist A: Remove a volume you truly don’t need (safely)
- Inspect volume; confirm driver is expected.
- Find all containers referencing it (running and stopped).
- Confirm the contents are non-critical or already backed up.
- Remove the referencing containers.
- Remove the volume.
- Verify the application still works (because sometimes “unused” means “unused today”).
Checklist B: Replace a volume without data loss
- Inspect the volume and identify the owning stack (Compose labels help).
- Schedule a maintenance window if the workload is stateful.
- Stop the app/container cleanly.
- Take a tar backup of the old volume (store it somewhere safe).
- Create a new volume with a versioned name.
- Migrate data (offline file copy or app-level restore).
- Recreate containers/stack pointing to the new volume.
- Run validation: logs, health checks, data sanity queries.
- Keep the old volume for a grace period, then delete it.
Checklist C: “I need it gone now” emergency mode
- Stop the container using the volume.
- Export a tar backup immediately.
- Remove referencing containers.
- Only then delete the volume.
- Document what you did while it’s still in your brain cache.
Three corporate mini-stories from the trenches
Incident caused by a wrong assumption: “Exited means unused”
A mid-size SaaS company had a billing stack managed by Compose. The team wanted to clean up disk space on a busy Docker host. Someone ran a quick check: docker ps looked clean. They saw no container using an old volume name, so they tried to remove it. Docker said “volume is in use.” Annoying, but at least it blocked them.
The wrong assumption came next: they decided Docker was confused and attempted a broader cleanup. They removed containers they recognized, then re-ran prune commands to “get it over with.” The volume disappeared—not the targeted one, but a different “unused” one holding a tiny sidecar’s state that turned out to be the only copy of a payment reconciliation cache.
The outage wasn’t dramatic; it was worse. Payments didn’t fail immediately. They drifted. Reconciliation jobs started producing mismatched reports, which triggered a compliance review. The engineering fix took a day; the trust repair took weeks.
The postmortem conclusion was painfully boring: a stopped container can still reference a volume, and “unused by running containers” is not the same as “safe to delete.” They added a rule: any volume deletion requires listing referencing containers via docker ps -a --filter volume=... and taking a tar backup first. Nobody liked the extra steps. Everybody liked not doing the incident again.
Optimization that backfired: “Let’s prune volumes nightly”
A different org tried to be proactive about disk pressure. They added a nightly job that ran docker system prune -af --volumes. It worked for weeks. Disk graphs were flatter. The team felt like grown-ups.
Then an engineer deployed a new version of a service that performed a blue/green container swap. The old container was removed as expected, and the new one started. But the volume holding user-uploaded files was temporarily detached during the deployment choreography—there was a short window where no container referenced the volume.
The nightly prune landed in that window and deleted the upload volume. The next morning, the service was “healthy” but quietly serving missing images and rebuilding indexes against an empty directory. The fix was not “restore from backup,” because there wasn’t one. The fix was “tell customers their data is gone,” which is a great way to learn what legal counsel sounds like on a conference call.
The lesson: prune is not an optimization; it’s a policy decision. If you want automated cleanup, tag volumes and prune by explicit criteria, not by absence of current references. Also: if you store user data in a Docker volume, treat it like you would treat a database—backups, retention, and a restore drill.
Boring but correct practice that saved the day: “Versioned volumes + rollback window”
A finance-adjacent company ran Postgres in Docker on a few dedicated hosts. Not trendy, but stable. They did routine upgrades by creating a new volume per change: pgdata_2025q4, pgdata_2025q4_patch1, that sort of thing. Each change included a tarball backup stored off-host and a brief validation script that ran basic SQL checks.
During one upgrade, the container started, but queries got slower. Not catastrophically, just enough to trigger latency alarms. The team suspected an underlying storage regression, maybe filesystem mount options or a subtle kernel issue. They didn’t debate it for hours. They rolled back.
Rollback was simple: stop container, point back to the old volume, restart. The old volume still existed because their process always kept the prior volume for a defined grace period. The customer-visible incident was minimal.
Afterwards, they debugged calmly and found that an “innocent” change in the migration step had altered permissions on a subdirectory, forcing Postgres into less optimal behavior. They fixed the migration, ran validation again, and moved forward. The boring practice—versioned volumes and a rollback window—did exactly what it was supposed to do: it made the worst day less exciting.
FAQ
1) Why does Docker say a volume is in use when the container is stopped?
Because Docker’s check is based on container configuration references, not on running processes. A stopped container still “uses” the volume in Docker’s metadata until the container is removed.
2) How do I find the container that’s using my volume?
Use the volume filter on docker ps -a:
cr0x@server:~$ docker ps -a --filter volume=pgdata
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2d8c1f8b6a1d postgres:16 "docker-entrypoint.s…" 3 days ago Up 3 days billing-postgres-1
3) Can I “detach” a volume from a container without removing the container?
Not really. Docker doesn’t support editing a container’s mount configuration in-place. You recreate the container with the desired mounts.
4) Is it safe to copy a database volume with cp -a?
Only if the database is fully stopped and you understand the engine’s file-level consistency expectations. For strong guarantees, use database-native backup and restore tooling.
5) What’s the safest way to replace a volume in Docker Compose?
Create a new named volume, migrate data into it, update the Compose file to reference the new volume name, then docker compose up -d. Keep the old volume for rollback until you’re satisfied.
6) Why do I have volumes with long random-looking names?
Those are often anonymous volumes created implicitly (for example, when an image declares a volume and Compose/run didn’t name it explicitly). They’re easy to accumulate and hard to attribute. Inspect them before deleting.
7) What does “Driver: local” really mean?
It means Docker stores the data on the host filesystem under Docker’s data root. It is not replicated, not magically shared across nodes, and not protected from host disk failure unless you add protection elsewhere.
8) Can I rename a Docker volume?
Docker doesn’t have a rename operation for volumes. The standard approach is: create a new volume with the desired name, copy data, then update containers/Compose to use it.
9) Is docker volume prune safe?
It deletes volumes not referenced by any containers. That can still be dangerous if you keep backup volumes detached on purpose or if your deployment temporarily detaches volumes. Use it only with policy and awareness.
Conclusion: practical next steps
When Docker says “volume is in use,” it’s usually protecting you from yourself—by enforcing that a referenced volume can’t be deleted out from under a container definition. Your job is to identify the reference chain, decide whether you’re deleting or replacing, and make data safety explicit.
Next steps you can take today:
- Pick one host and inventory volumes with labels and owners. Unknown volumes are future incidents.
- Adopt a standard replacement pattern: new versioned volume + migration + validation + rollback window.
- Write a one-page runbook that includes: volume inspect, container discovery, tar backup, and the approval step to delete.
- Stop treating prune as routine housekeeping in production unless you can prove it won’t delete valuable “currently unattached” data.
The goal isn’t to never delete volumes. The goal is to delete them on purpose.