Короткий ответ
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 | низкая | не для БД |
Возможные причины
- Живой Postgres + cp Mountpoint.
- Не тот volume (
app_app_data). - Копируют без
userи потом EACCES. - Нет места на
/var/backups. - Забытый
compose startпосле freeze (downtime). - Только 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.