Docker & ContainersXXI Β· BackupBind mount backups
Bind mount backups β preserving the host path layout
What you'll learn
- Enumerate every bind mount on a host rather than relying on memory
- Preserve numeric ownership, xattrs, ACLs and SELinux labels through a backup
- Explain why tar does not follow symlinks by default and when you want it to
- Restore to a host with a different UID map without breaking the application
Prerequisites
Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-12
A named volume is an object Docker manages: it has a name, docker volume ls
finds it, and the engine knows which containers reference it. A bind mount is
none of those things. It is a host path that a container happens to have
mounted, and the moment the container stops, nothing in Docker records that the
path mattered.
That is the whole difficulty. Bind mount backups fail not because tar is hard but because nobody knew the path was load-bearing.
First, find them all
You cannot back up what you have not enumerated, and the list is never the one in the Compose file β it is the Compose file plus the override file plus the containers somebody started by hand.
docker ps -a --format '{{.Names}}' | while read -r c; do
docker inspect --format '{{range .Mounts}}{{if eq .Type "bind"}}{{$.Name}} | {{.Source}} -> {{.Destination}} rw={{.RW}}
{{end}}{{end}}' "$c"
done | sort -u$ docker ps -a | while read ... docker inspect .../app-caddy | /srv/caddy/Caddyfile -> /etc/caddy/Caddyfile rw=false
/app-caddy | /srv/caddy/data -> /data rw=true
/app-db | /etc/localtime -> /etc/localtime rw=false
/app-web | /srv/app/uploads -> /var/www/uploads rw=true
/app-web | /var/run/docker.sock -> /var/run/docker.sock rw=trueIllustrative output
Read that output as a classification exercise, because the backup decision is different for each kind:
/srv/caddy/dataβ state you cannot recreate. It holds issued TLS certificates and the ACME account key. Losing it means re-issuing every certificate, against rate limits, during whatever outage you were already in. Back it up./srv/app/uploadsβ user data. Irreplaceable. Back it up./srv/caddy/Caddyfileβ configuration. It should be in git; the host copy is a deployment artefact. Back up the git repository, not the path./etc/localtimeβ a host detail, recreated by the OS. Do not back it up./var/run/docker.sockβ not data at all. It is a socket granting root equivalent on the host to whatever is in that container, and its appearance in a backup inventory is a security finding rather than a backup one.
Backing up: what tar keeps and what it drops
SRC=/srv/app/uploads
DEST=/backup
STAMP=$(date +%F)
sudo tar --numeric-owner --xattrs --acls --selinux -czf "$DEST/uploads-$STAMP.tar.gz" -C "$SRC" .Four flags, each covering a specific way the restore breaks.
--numeric-owner stores UIDs and GIDs as numbers rather than resolving them to
names. This is the one that matters most for containers, and it is worth being
precise about why: the containerβs www-data and the hostβs www-data are
usually different numbers. The container process runs with a numeric UID that
the host kernel enforces; the name is irrelevant to it. Without
--numeric-owner, tar records the hostβs name for UID 82 and on restore
resolves that name on the new host, which may map it to 33. The files come
back owned by a user the container has never heard of.
--xattrs preserves extended attributes β capabilities set with setcap, and
anything an application stores out of band. --acls preserves POSIX ACLs, which
are invisible to ls -l and are how many applications actually grant access.
--selinux preserves the security context, which on RHEL, Rocky, Alma or Fedora
is the difference between an application that starts and one that gets
Permission denied on a file that ls -l says it owns.
Symlinks: tar does not follow them by default
This is worth stating plainly because the opposite is widely believed.
GNU tar archives a symbolic link as a symbolic link. It stores the link and
its target string, and does not read through it. -h (--dereference) is the
flag that changes this: with -h, tar reads the file the link points at and
stores that content under the linkβs name.
SRC=/srv/app
# Default: links are stored as links. The archive is small and faithful.
tar -czf /backup/app.tar.gz -C "$SRC" .
# --dereference: links are replaced by a copy of what they point to.
tar -czhf /backup/app-flat.tar.gz -C "$SRC" .
# Which is which - inspect rather than assume
tar -tvzf /backup/app.tar.gz | grep '^l'$ tar -tvzf /backup/app.tar.gz | grep '^l'lrwxrwxrwx root/root 0 2026-03-04 11:22 ./current -> ./releases/2026-03-04
lrwxrwxrwx root/root 0 2025-11-19 09:07 ./secrets -> /run/secrets/appIllustrative output
Both defaults have a failure mode, which is why you have to look:
- Storing the link is right for a
current -> releases/2026-03-04deploy symlink, and wrong if the link points outside the backed-up tree. On restore you get a dangling link and an application that cannot find its data. - Following the link is right when a data directory was symlinked to another
filesystem for space, and catastrophic when a link points at
/run/secretsor/etcβ you have just copied secrets or the hostβs configuration into a backup archive that has a different access-control story than the original.
The second line of the illustrative output is the one to notice. ./secrets -> /run/secrets/app under -h puts your live secrets in a tarball on a backup
server. That is a plain data leak produced by a flag people add for tidiness.
Restoring, especially to a different host
ARCHIVE=/backup/uploads-2026-08-12.tar.gz
STAGING=/srv/restore/uploads
sudo mkdir -p "$STAGING"
# Extract into staging, preserving everything the archive carried
sudo tar --numeric-owner --xattrs --acls --selinux -xzf "$ARCHIVE" -C "$STAGING"
# Verify before it goes anywhere near the application
sudo find "$STAGING" -type f | wc -l
sudo ls -lan "$STAGING" | head
sudo ls -Z "$STAGING" 2>/dev/null | headls -lan rather than ls -la is deliberate: -n prints numeric UIDs, which is
what the container kernel enforces. ls -la resolves them to host names and
tells you what the host thinks, which is the wrong question.
Consistency applies here too
Everything in the volume backups lesson about smeared copies is true of bind mounts, with one addition: a bind mount is more likely to be written by something outside Docker as well. A directory shared between a container and a host cron job has two writers, and stopping the container quiesces only one of them.
For a database on a bind mount β an anti-pattern, but a common one β use the databaseβs dump tool, exactly as for a volume. For a certificate store like Caddyβs or Traefikβs, the writes are infrequent and atomic-rename based, so a live tar is genuinely low risk. For an uploads directory, a live tar may miss an in-progress upload, which is usually acceptable and should be a decision you recorded rather than one you defaulted into.
SRC=/srv/app/uploads
sudo systemctl stop app-importer.timer
docker compose -f /srv/app/compose.yaml stop web
# Prove nothing has the tree open before copying
sudo lsof +D "$SRC" || echo 'no open file handles'
sudo tar --numeric-owner --xattrs --acls --selinux -czf "/backup/uploads-$(date +%F).tar.gz" -C "$SRC" .
docker compose -f /srv/app/compose.yaml start web
sudo systemctl start app-importer.timerlsof +D on the directory is the verification: it lists every process holding a
file open beneath it. Empty output means both writers are genuinely stopped.
Non-empty output names the thing you forgot.
Knowledge check
Knowledge check Β· 5 questions
Q1. GNU tar follows symbolic links by default, so a backup of a directory containing a symlink to /run/secrets will copy the secrets into the archive.
Q2. You restore a bind-mounted uploads directory to a new host. `ls -l` shows the right owner name and the right mode, but the container gets Permission denied on every write. What should you check next?
Q3. Why does `--numeric-owner` matter more for container data than for ordinary host backups?
Q4. Which of these should appear in a bind mount backup set? Select all that apply.
Q5. What is the value of running `lsof +D /srv/app/uploads` before archiving it?
Passing score: 75%. Answers are checked in this browser.