Skip to main content
RunBook Academy

Docker & ContainersXXI Β· BackupBind mount backups

Bind mount backups β€” preserving the host path layout

Intermediate⏱ ~22 mindockertar

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

Not yet marked complete on this device.

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.

Read-only / Safebind mount inventory
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
Read-only / Safethe inventory
$ 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=true

Illustrative 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

Read-only / Safehost tar
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.

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.

Read-only / Safesymlink handling
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'
Read-only / Safetar -tvzf
$ 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/app

Illustrative 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-04 deploy 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/secrets or /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

Data-loss riskrestore
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 | head

ls -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.

Service impact possiblequiesce both writers
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.timer

lsof +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

  1. 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.

  2. 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?

  3. Q3. Why does `--numeric-owner` matter more for container data than for ordinary host backups?

  4. Q4. Which of these should appear in a bind mount backup set? Select all that apply.

  5. Q5. What is the value of running `lsof +D /srv/app/uploads` before archiving it?

Passing score: 75%. Answers are checked in this browser.