Data persistence and backup patterns

Volume drivers and where data really lives, permission mismatches, database containers, and a backup you have restored at least once.

Where the bytes live

docker volume create --driver local \
  --opt type=nfs --opt o=addr=10.0.0.5,rw --opt device=:/export/data shared
docker volume ls
docker volume inspect pgdata --format '{{.Mountpoint}} {{.Driver}}'

sudo ls -la /var/lib/docker/volumes/pgdata/_data   # the real path on the host
docker run --rm -v pgdata:/data alpine ls -la /data

docker volume rm pgdata            # fails while a container uses it
docker volume prune                # deletes every unused volume - read twice
SituationChooseBecause
Database or uploads that must outlive the containerNamed volumeDocker manages the path, and it survives docker compose down
Source code during developmentBind mountEdits on the host appear immediately
Config file or certificate you manageRead-only bind mountThe host stays the source of truth
Scratch data that must never hit disktmpfsMemory only, gone with the container
Data on NFS or a cloud diskVolume with a driverThe storage lives outside the host
Files you stop caring about after the container diesContainer layerNothing to clean up
  • docker compose down -v removes named volumes too. That flag is a data-loss command, not a cleanup command.
  • An anonymous volume (no name given) is orphaned as soon as the container is recreated, and it still occupies disk until pruned.
  • Backing up /var/lib/docker with a file copy is not a substitute for a data-aware backup, and it is not portable across engines.

Database containers

docker run -d --name db \
  -e POSTGRES_PASSWORD=secret -e POSTGRES_USER=app -e POSTGRES_DB=app \
  -v pgdata:/var/lib/postgresql/data \
  --health-cmd "pg_isready -U app" --health-interval 5s \
  postgres:16-alpine

# initialisation scripts run only on an empty data directory
# docker-entrypoint-initdb.d/*.sql

# consistent logical dump, regardless of how the volume is stored
docker exec -e PGPASSWORD=secret db pg_dump -U app app | gzip > app-2026-09-18.sql.gz
docker exec -e PGPASSWORD=secret db psql -U app -c '\l'

# restoring
gunzip -c app-2026-09-18.sql.gz | docker exec -i -e PGPASSWORD=secret db psql -U app app
⚠️
Copying a database's files while the server is running gives you a torn snapshot — the data files and the write-ahead log are not copied atomically, and the restore may fail or silently lose transactions. Stop the container, use a filesystem snapshot, or take a logical dump; only those three are trustworthy.

Backing up and restoring

# archive a volume through a helper container, no host paths required
docker run --rm -v pgdata:/data -v "$PWD/backups:/backup" alpine \
  tar -czf /backup/pgdata-$(date +%F).tar.gz -C /data .

# restore into a fresh volume
docker volume create pgdata-restore
docker run --rm -v pgdata-restore:/data -v "$PWD/backups:/backup" alpine \
  sh -c 'tar -xzf /backup/pgdata-2026-09-18.tar.gz -C /data && ls -la /data'

# volume-to-volume copy
docker run --rm -v pgdata:/from -v pgdata-restore:/to alpine \
  sh -c 'cd /from && tar -cf - . | (cd /to && tar -xf -)'

# object storage with deduplication and retention
restic -r s3:s3.example.com/app-backups backup /srv /etc
restic -r s3:s3.example.com/app-backups forget --keep-daily 7 --keep-weekly 4 --prune
restic -r s3:s3.example.com/app-backups restore latest --target /tmp/verify
  • Test the restore into a throwaway volume on a schedule. A backup nobody has restored is a hypothesis, not a backup.
  • Keep one copy where the compromised host cannot write: append-only or object-lock storage defeats ransomware that reaches your credentials.
  • Record what must accompany the data: the Compose file, the image digest, the environment variables and the database version. Data alone is often unrestorable.
  • Alert when the job fails. Silent schedules are discovered only during an incident.

FAQ

My container writes as UID 10001 and the bind mount is owned by root. What is the right fix?
Make the ownership match rather than loosening permissions: chown -R 10001:10001 ./data on the host, or set user: to the UID that owns the directory. Never answer this with chmod 777.
Can I just mount a cloud disk into the container?
Yes, but mount it on the host and bind-mount or use a volume driver; giving the container a block device and a filesystem utility means the container now needs the privileges to format disks.

Container security: users, capabilities and secrets Production concerns: resources, health and logging

Last refreshed 2026-09-18.