Короткий ответ

Named volume app_data копируют консистентно: для Postgres/MySQL предпочтителен логический dump внутри контейнера; для файловых данных — compose stop сервиса-писателя и tar/rsync из служебного контейнера с --volumes-from или bind Mountpoint. Хостовый fsfreeze на XFS/ext4 тома, где лежит Docker data-root, — инструмент для freeze ФС, не замена пониманию приложения. Не tar каталога Mountpoint, пока Postgres пишет WAL. Не volume rm. Не считайте overlay-слой бэкапом.

Хост host.example, проект /opt/app, каталог копий /var/backups/app.

Симптомы и как отличить

  • restore «успешен», БД не стартует (разорванный WAL);
  • бэкап 20 байтов — копировали пустой mount;
  • бэкапят rw-слой контейнера (docker export) вместо volume;
  • dump SQL есть, файлы uploads нет (два разных тома).
МетодКонсистентностьКогда
pg_dumpвысокая для SQLБД
stop + tar volumeвысокаяфайлы, sqlite после lock
fsfreeze + snapshot СХДвысокаяесли умеете оттаивать
tar живого Mountpointнизкаяне для БД

Возможные причины

  1. Живой Postgres + cp Mountpoint.
  2. Не тот volume (app_app_data).
  3. Копируют без user и потом EACCES.
  4. Нет места на /var/backups.
  5. Забытый compose start после freeze (downtime).
  6. Только snapshot VM без quiesce гостя.

Диагностика

cd /opt/app
docker compose config --volumes
docker inspect app-db-1 --format '{{range .Mounts}}{{.Type}} {{.Name}} {{.Source}} {{.Destination}}{{"\n"}}{{end}}'
docker volume inspect app_data
df -h /var/backups /var/lib/docker

Определите, писатель — db или web. Стоп писателя.

Решение

Сценарий A. PostgreSQL: dump (предпочтительно)

mkdir -p /var/backups/app
docker compose exec -T db pg_dump -U app -d app --format=custom -f /tmp/app.dump
docker compose cp db:/tmp/app.dump /var/backups/app/app-$(date +%F).dump

Проверка размера и pg_restore --list. Volume тоже стоит периодически копировать как «вторую форму», но dump — RPO логики.

Сценарий B. Файлы / sqlite: stop + tar через alpine

cd /opt/app
docker compose stop web
docker run --rm \
  -v app_data:/volume:ro \
  -v /var/backups/app:/backup \
  registry.example:5000/library/alpine:3.20 \
  tar -C /volume -czf /backup/app_data-$(date +%F-%H%M).tar.gz .
docker compose start web

Образ alpine должен быть заранее на хосте (иначе pull в окне). :ro на volume на время копии.

Проверка архива:

tar -tzf /var/backups/app/app_data-*.tar.gz | head

Сценарий C. fsfreeze на файловой системе хоста

Имеет смысл, если app_data на отдельном XFS и вы снимаете снапшот LVM/СХД:

# только если понимаете mountpoint тома данных, не корень OS вслепую
findmnt "$(docker volume inspect app_data --format '{{.Mountpoint}}')"

Порядок: stop приложения или fsfreeze -f, snapshot, fsfreeze -u, start. Забытый freeze = read-only прод. Не freeze / целиком без окна.

Сценарий D. Restore (кратко)

Новый/очищенный том, не live overwrite без отката:

docker compose stop web
docker run --rm -v app_data:/volume -v /var/backups/app:/backup alpine \
  sh -c 'rm -rf /volume/* && tar -C /volume -xzf /backup/FILE.tar.gz'

rm -rf /volume/* — разрушительно, только на целевом томе после проверки имени. Затем права UID: права, start, health.

Mountpoint /var/lib/docker/volumes/app_data/_data менять «на живую» через tar с хоста можно только после stop писателя: иначе Postgres держит WAL, tar снимет рваные файлы. fsfreeze на ФС, где лежит весь /var/lib/docker, заморозит и overlay, и логи, и чужие тома — слишком широко, если docker-root общий. Лучше отдельный LV под volumes или логический dump.

Копирование docker cp из работающего db файла PGDATA не консистентно. pg_dump идёт через протокол и даёт точку, которую pg_restore понимает. Файлы uploads (если другой том) tar после stop web или с fs-уровневым quiesce приложения (read-only режим CMS, если есть).

Шифрование: архив на /var/backups/app доступен root хоста. Если бэкап уезжает на NAS, не world-readable. Ключ не в образе. Restore-тест на app_data_restore_test обязателен: иначе вы бэкапите пустой tar из-за неверного имени тома месяцами.

Не храните единственную копию на том же диске, что /var/lib/docker. ENOSPC убьёт и прод, и бэкап. Offsite — отдельный runbook, здесь минимум: другая ФС.

Как проверить, что проблема устранена

Процедура бэкапа считается рабочей, когда restore в стороне прошёл:

docker volume create app_data_restore_test
docker run --rm -v app_data_restore_test:/volume -v /var/backups/app:/backup alpine \
  tar -C /volume -xzf /backup/LAST.tar.gz
# поднимите временный compose-файл с volume external app_data_restore_test, проверьте, удалите тестовый стек
docker volume rm app_data_restore_test

Прод-стек Running, app_data на месте, дата архива свежая, размер ненулевой.

Если не помогло

  • Архив 0: volume был другой / приложение пишет не туда.
  • Restore EACCES: uid в tar vs user контейнера.
  • Нет места: сначала диск, не «сожмите живой Postgres».
  • dump ок, uploads нет — второй volume в YAML.

Профилактика

  • Расписание dump + файловый tar, offsite.
  • Запрет down -v в cron.
  • Тестовый restore раз в месяц.
  • Pin образа alpine для backup job.
  • Мониторинг возраста /var/backups/app.

Имя файла бэкапа должно содержать имя тома и дату, не backup.tar.gz. Иначе restore-тест возьмёт не тот архив. Проверка tar -tzf и размера относительно du Mountpoint (сжатие учтите) — в тот же день, не через месяц.

FAQ

Достаточно ли snapshot VM?

Как слой 3-2-1 — да, как единственный консистентный бэкап БД — нет, без quiesce.

volumes-from vs -v name

Оба ок. Явный -v app_data:/volume яснее, меньше ошибиться контейнером.

Можно ли бэкапить overlay2?

Нет как метод данных приложения.

fsfreeze vs stop

stop понятнее большинству команд. fsfreeze для короткого RPO на СХД, если stop недопустим и ФС поддерживает.

Шифровать архив?

Да, в секретах ключ не в образе. Не оставляйте dump world-readable.