How to back up and restore Docker volumes
Mount the volume read-only into a throwaway container and have it write a tar archive to the host: docker run --rm -v myvol:/data:ro -v "$(pwd)":/backup alpine tar czf /backup/myvol.tar.gz -C /data .. To restore, extract the archive into a new volume the same way. If the volume holds a database, stop its container first or dump the database instead, because files copied mid-write may not start again.
Named volumes, bind mounts and anonymous volumes
Docker gives containers storage in three ways, and each one is backed up differently:
| Type | Where the data lives | How to back it up |
|---|---|---|
Named volume: -v shop_uploads:/uploads | A directory Docker manages, by default /var/lib/docker/volumes/<name>/_data on Linux | With a helper container, as shown below |
Bind mount: -v /srv/shop/uploads:/uploads | A host directory you chose | Like any other directory, with tar or rsync on the host |
Anonymous volume: -v /uploads with no name | A volume named with a random ID | Give it a name first. docker run --rm deletes a container's anonymous volumes when it exits. |
Docker's documentation describes volumes as completely managed by Docker. Read and write them through a container, as below, rather than through the host path, which depends on how Docker was installed.
Find the volumes a container uses
List the volumes on the host. Add -q to print names only, which is handy in scripts:
docker volume lsCompose puts the project name in front of every volume it creates. The project name defaults to the name of the directory holding the Compose file, so a volume declared as uploads in /srv/shop is called shop_uploads.
To see what one container mounts, print the Mounts section of docker inspect as JSON:
docker inspect --format '{{json .Mounts}}' shop-web-1Each entry has a Type (volume or bind), a Name for volumes, the host Source and the container Destination. To go the other way, --filter volume= lists the containers that mount a volume, and -a includes stopped ones:
docker ps -a --filter volume=shop_uploadsdocker volume inspect shop_uploads prints the volume's driver, labels and Mountpoint, the host directory that holds its files.
Keep the copy consistent
The helper container reads files while your application may still be writing them. For uploads, configuration and other files that are written once, that is fine. For anything that changes files in place, such as a database, pick one of these:
| Method | Downtime | What you get |
|---|---|---|
Stop the container: docker stop shop-db-1 | Until the copy finishes | A clean copy. Docker sends SIGTERM, then SIGKILL after 10 seconds; give a slow database longer with -t 60. |
Pause it: docker pause shop-db-1, later docker unpause shop-db-1 | Until the copy finishes; requests hang instead of failing | Files that do not change during the copy, but nothing the process still held in memory. A database sees it like a power cut. |
| Dump the database instead | None | A consistent, portable file. See how to back up a database in Docker Compose. |
For a Compose project, docker compose stop stops every service and docker compose start starts them again, with containers and volumes left as they were.
Managing containers with Portainer? Its own settings live in a volume too: see how to back up Portainer.
Back up a named volume
docker run --rm -v shop_uploads:/data:ro -v "$(pwd)":/backup alpine tar czf /backup/shop_uploads-$(date +%F).tar.gz -C /data .--rmdeletes the helper container when tar finishes.-v shop_uploads:/data:romounts the volume at/data.:romakes it read-only, so the backup cannot change it.-v "$(pwd)":/backupbind-mounts the current directory at/backup, so the archive lands on the host.alpineis a small image whose BusyBoxtarkeeps ordinary files, permissions and owners.tar czf FILEcreates (c) a gzip-compressed (z) archive in FILE (f).$(date +%F)adds today's date, such as2026-10-03.-C /data .changes into the volume and archives its contents, so paths in the archive are relative and extract cleanly anywhere.
If you mistype the volume name, Docker does not complain. It creates a new, empty volume with that name, and you archive nothing. Check names with docker volume ls and remove a stray volume with docker volume rm.
List the archive on the host to confirm it holds your files:
tar -tzvf shop_uploads-2026-10-03.tar.gz | headDocker's own documentation shows a variant: --volumes-from mounts every volume of an existing container at the same paths, so one command archives them all. The :ro suffix mounts them read-only:
docker run --rm --volumes-from shop-web-1:ro -v "$(pwd)":/backup alpine tar czf /backup/shop-web-1.tar.gz /var/www/uploadstar drops the leading /, so that archive holds paths such as var/www/uploads/logo.png. Restore it with -C / in a container that mounts the volume at the same path.
Restore into a new volume
Restore into a fresh volume, check it, then switch the application over. A bad archive never touches the live data.
docker volume create shop_uploads_restoreddocker run --rm -v shop_uploads_restored:/data -v "$(pwd)":/backup:ro alpine tar xzf /backup/shop_uploads-2026-10-03.tar.gz --numeric-owner -C /dataxextracts. tar runs as root in the helper container, so it restores each file's permissions and owner.--numeric-owneruses the user and group IDs stored in the archive. Without it, tar first looks owner names up in the helper image's own user list, and an archive made in a different image can come back with the wrong owners.:roon/backupstops the restore from writing into your backup directory.
Count the files in the original and the restored volume and compare:
docker run --rm -v shop_uploads_restored:/data:ro alpine sh -c 'find /data -type f | wc -l'To run a Compose service on the restored volume, declare it external and give its real name. Compose then uses it as is: it does not create it or add the project prefix, and it stops with an error if the volume is missing.
volumes:
uploads:
external: true
name: shop_uploads_restoredTo restore under the original name instead, stop the containers that use it, remove it with docker volume rm shop_uploads, and extract into a new volume with the old name.
Move a volume to another host
Pipe the archive over SSH instead of writing a file. The first container writes it to standard output. The second, started with -i so it reads standard input, extracts it into a volume of the same name on the other host, which Docker creates on first use:
docker run --rm -v shop_uploads:/data:ro alpine tar czf - -C /data . | ssh root@new-host 'docker run --rm -i -v shop_uploads:/data alpine tar xzf - --numeric-owner -C /data'-f - means standard output when creating and standard input when extracting. Stop the application on the old host first if its data still changes. With Compose, keep the same project name on the new host so the volume names match. Compose then prints a warning that the volume already exists but was not created by Docker Compose and uses it; declaring it external as above silences that.
The new host also needs the images the containers ran. Most can be pulled again, but some cannot: see how to back up Docker images.
Automate it with cron
This script archives a list of volumes, stops if a name is wrong, checks each archive, and deletes archives older than seven days:
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/var/backups/docker-volumes"
KEEP_DAYS=7
VOLUMES="shop_uploads shop_config"
STAMP="$(date +%Y-%m-%d_%H%M)"
mkdir -p "$BACKUP_DIR"
find "$BACKUP_DIR" -name '*.partial' -type f -delete
for VOL in $VOLUMES; do
OUT="$BACKUP_DIR/$VOL-$STAMP.tar.gz"
docker volume inspect "$VOL" > /dev/null
docker run --rm -v "$VOL":/data:ro -v "$BACKUP_DIR":/backup alpine \
tar czf "/backup/$VOL-$STAMP.tar.gz.partial" -C /data .
tar -tzf "$OUT.partial" > /dev/null
mv "$OUT.partial" "$OUT"
done
find "$BACKUP_DIR" -name '*.tar.gz' -type f -mtime +"$KEEP_DAYS" -deletesudo chmod 755 /usr/local/bin/docker-volume-backup.sh0 3 * * * root /usr/local/bin/docker-volume-backup.sh >> /var/log/docker-volume-backup.log 2>&1docker volume inspect fails for a volume that does not exist, and set -e stops the script there instead of archiving a new empty volume. tar -tzf reads the whole archive, so a truncated file fails before it is renamed. Leave database volumes out unless you stop their containers around the copy. More in the cron guide.
The archives share a disk with the volumes they protect, so copy them off the server too, for example with rclone.
What not to do
- Don't copy
/var/lib/docker/volumeswhile containers run. Files change during the copy, and a database copied that way may refuse to start. The path is also an internal detail that varies between installs. - Don't back up all of
/var/lib/docker. Images and containers can be pulled and recreated. What you need is the volumes, the bind-mounted directories and your Compose files. - Don't treat cleanup commands as harmless.
docker compose down -vremoves the project's named volumes.docker volume prune --allremoves every named volume no container uses, including those of a project you just took down. - Don't keep the only copy on the same server. See the 3-2-1 backup rule.
Common errors
| Symptom | Cause and fix |
|---|---|
| The archive is tiny and lists no files | The volume name was wrong, so Docker created an empty volume. Check docker volume ls, then remove the stray volume. |
Restored files end up in a subfolder such as data/ | The archive was made from an absolute path instead of with -C /data .. Extract with --strip-components=1, or make the archive again. |
volume is in use when removing a volume | A container, perhaps a stopped one, still mounts it. Find it with docker ps -a --filter volume=NAME and remove that container first. |
| The application reports permission errors after a restore | File owners changed. Extract as root with --numeric-owner. |
Permission denied on /backup on Fedora, RHEL or another SELinux host | SELinux blocks the bind mount. Add :z to it, as in -v "$(pwd)":/backup:z, and only on a dedicated backup directory, never a system one. |
| The database will not start after a restore | The volume was archived while the database was writing. Restore an archive taken with the container stopped, or restore from a dump. |
Self-hosted apps such as Uptime Kuma and Vaultwarden keep a SQLite database in their volume, which a plain tar can copy mid-write: see how to back up a SQLite database safely. Running Coolify? See how to back up Coolify.
Frequently asked questions
- Where are Docker volumes stored?
- On a standard Linux install, under
/var/lib/docker/volumes/<name>/_data. Rundocker volume inspectwith the volume name to see the exact Mountpoint, and read or write it through a container rather than directly. - Can I back up a Docker volume while the container is running?
- Yes. The helper container reads it without stopping anything, which is fine for files that are written once, such as uploads. For a database, stop the container for the copy or dump the database instead.
- Does docker compose down delete volumes?
- No. Named volumes survive docker compose down. docker compose down -v deletes them, and so does docker volume prune --all once no container uses them.
- How do I copy a Docker volume to another server?
- Pipe a tar archive over SSH. A helper container on the old host writes the archive to standard output, and another on the new host, started with -i, extracts it into a volume of the same name.
- How do I back up a bind mount?
- A bind mount is an ordinary host directory, so back it up with tar or rsync like any other directory. No helper container is needed.
How this was checked
Commands, limits and prices were checked against these official pages, on October 3, 2026:
- Docker documentation: Volumes
- Docker documentation: Bind mounts
- Docker CLI reference: docker container run
- Docker CLI reference: docker volume ls, inspect and prune
- Docker CLI reference: docker inspect
- Docker CLI reference: docker container ls (filters)
- Docker CLI reference: docker container stop and pause
- Compose file reference: volumes (external, name)
- Docker CLI reference: docker compose down
- BusyBox tar source (options, owner handling)