Docker Compose Volumes and Backups Explained
I ran docker compose down -v to force a clean restart, out of muscle memory from a different project where that flag was harmless. It wasn't harmless here — it deleted a named volume along with the containers.

I ran docker compose down -v to force a clean restart of a stack, out of muscle memory from a different project where that flag was harmless. It wasn't harmless here. The -v deleted the stack's named volume along with the containers, and for about thirty seconds before the restic restore finished, I genuinely didn't know if a year of Vaultwarden entries still existed anywhere.
TL;DR
- Plain
docker compose downremoves containers and the default network — named volumes survive. - Adding
-valso deletes every named volume declared in that compose file. There is no confirmation prompt. - Bind mounts map a specific host folder into a container; back them up by pointing a backup tool straight at that folder.
- Named volumes live under Docker's own storage path; back them up through a helper container, and use a database's own dump tool for live database data, never a raw file copy.
- The right storage type is a per-service decision, not a project-wide default.
KEY-STAT: 30 — Seconds between running docker compose down -v by mistake and finding out whether the restic restore actually worked
The near-miss: what docker compose down -v actually deletes
Plain docker compose down stops and removes the stack's containers and its default network — anything stored in a named volume is untouched, because Docker doesn't delete storage on a whim. Add -v (or --volumes) and Compose also removes every named volume declared in that compose file, silently and immediately. It doesn't ask which volume, and it doesn't distinguish "I meant it" from "that flag was left over from another terminal history entry." I had a restic backup, and the restore worked — but the thirty seconds between running the command and confirming that made the difference between a story I tell and a genuine data-loss incident.
Bind mounts vs named volumes, in one table
| Bind mount | Named volume | |
|---|---|---|
| Storage location | A specific folder you choose on the host | Managed by Docker under its own storage path |
| Host visibility | Direct — ls the folder any time |
Indirect — inspect via docker volume commands |
| Portability across hosts | Tied to that exact host path | More portable; the app doesn't assume a host layout |
| Backup method | Point any backup tool straight at the folder | Needs a helper container, or the app's own export tool |
| Typical use here | Configs, media, anything backed up directly | Database engine data (Postgres, MariaDB) |
Bind mounts: a host folder as a container's storage
A bind mount is a direct line from a host path to a container path — ./vw-data:/data mounts the vw-data folder sitting next to the compose file straight into the container. Because it's a real, visible folder on the host, any backup tool that already knows how to back up a directory — restic, rsync, a NAS sync job — works on it with zero container-aware logic. That's exactly why configs and media on this homelab live on bind mounts: the backup step doesn't need to know Docker exists at all.
Named volumes: Docker-managed storage
A named volume — declared once under a compose file's top-level volumes: key and referenced by name — lives under Docker's own storage path (/var/lib/docker/volumes/ on Linux) instead of a path you chose. That indirection is a feature for anything that shouldn't assume details about the host: a database engine manages its own file layout inside that volume and doesn't care where Docker physically keeps it. The tradeoff is that backing it up isn't a plain folder copy — it needs either a helper container or the database's own export tool.
This homelab's actual storage choice, service by service
| Service | Storage type | Why |
|---|---|---|
Vaultwarden (/data) |
Bind mount | Backup target for restic needs to be a plain folder, not a Docker-managed path |
| Caddy config and certs | Bind mount | Edited by hand often enough that direct host access matters |
| Jellyfin media library | Bind mount to existing NAS storage | The Raspberry Pi NAS already owns that storage; Jellyfin just points at it |
| Postgres data directory | Named volume | Database files should not be raw-copied live; back it up with pg_dump, not a folder copy |
Full setup detail for the Vaultwarden bind-mount layout is in the Vaultwarden self-hosting guide.
Backing up a bind mount
No container involvement needed — point the backup tool straight at the folder:
restic backup ./vw-data --tag vaultwarden
Backing up a named volume correctly
For a named volume holding plain files (not a live database), a short-lived helper container mounts both the volume and a host folder, then archives it:
docker run --rm \
-v vw_internal_data:/source:ro \
-v "$(pwd)/backup":/backup \
alpine tar czf /backup/volume-backup.tar.gz -C /source .
Warning
That pattern is fine for static files, but a raw tar of a live database's named volume can capture it mid-write and produce a backup that looks fine and fails to restore. For Postgres or MariaDB, run the engine's own dump tool against the running container — docker compose exec postgres pg_dump -U user dbname > backup.sql — never a file-level copy of a live database's volume.
Tip
Whichever method you use, restore it into a throwaway location and check the data is actually there at least once. A backup that has never been restored is a hope, not a backup — that's exactly the check that mattered during the down -v near-miss above.
Decision guide: bind mount or named volume?
| Service type | Recommendation | Why |
|---|---|---|
| Config files, certs, anything hand-edited | Bind mount | Direct host access; plain-folder backup |
| Media libraries and existing NAS storage | Bind mount | Points at storage that already exists outside Docker |
| Database engine data (Postgres, MariaDB, Redis persistence) | Named volume + the engine's own dump tool | Avoids raw-copy corruption risk on live data |
| App data with no live-write concerns (static app state) | Either — bind mount if you want direct visibility | Named volume is fine too; the risk is specifically live databases |
Storage choice is independent of the service's restart policy — see the restart policies guide — and independent of which network it joins, covered in Docker Compose networking. All three are separate decisions made per service, not one global default for the whole stack.
The bottom line
Put configs and media on bind mounts so any generic backup tool can reach them directly. Put live database data on named volumes and back it up with the database's own tool, never a raw copy. Then actually restore a backup once, before docker compose down -v becomes the thing that tests your backup strategy for you.
Frequently asked questions
Does docker compose down delete volumes?
No, not by default. Plain docker compose down removes containers and the default network but leaves named volumes intact. Only adding the -v (or --volumes) flag deletes the named volumes declared in that compose file.
What is the difference between a bind mount and a named volume?
A bind mount maps a specific folder you chose on the host into the container, so you can browse and back it up directly. A named volume is storage Docker manages itself under its own path, better isolated from host-path assumptions but requiring a helper container or the app's own tool to back up.
How do you back up a Docker named volume?
Run a short-lived container that mounts the named volume read-only alongside a host backup folder, then archive the volume's contents with tar. For a live database, use the database engine's own export tool (pg_dump, mysqldump) instead of a raw file copy.
Is it safe to copy a database's Docker volume directly for backup?
Not while the database is running. A raw copy of a live database's files can capture it mid-write, producing a backup that appears complete but fails to restore cleanly. Use the database engine's own dump tool against the running container instead.
More from Self-Hosting & Privacy

The power blipped, the host rebooted, and half the homelab stack came back up on its own — the other half just sat there, exited. Every container had a restart: line; they weren't all the same line.

Two containers on the same host could reach each other, and neither one was supposed to. Both had landed on Docker Compose's default bridge network — a network that doesn't ask permission before connecting things.

Gitea and Forgejo run on well under 1GB of RAM; GitLab CE documents a 4GB minimum. This comparison covers real resource footprint, CI runner maturity (Gitea/Forgejo Actions vs GitLab CI), and GitHub migration friction for homelab hardware.
Stay in the loop
Get the latest articles delivered to your inbox. No spam, unsubscribe anytime.
Self-Hosted Apps: What's Actually Worth It
Every self-hosted apps roundup is an unvetted list with no honest 'skip this one.' Six categories actually tested on a homelab — AI chatbot, music, automation, Reddit, Git, and CI/CD — with real hardware cost and one category that isn't worth it for most people.
Continue Reading