Короткий ответ
Данные живут в named volume app_data (или в bind, если так спроектировали), не в контейнере. Обновление: зафиксировать тег registry.example:5000/app/web:1.2.4, docker compose pull/build, up -d без -v, проверить health. Старый контейнер удаляется, том остаётся. Запрещено: compose down -v, volume rm app_data, system prune -a до проверки отката. Перед миграцией схемы — бэкап volume.
Симптомы и как отличить
- «Обновили — база пустая»: почти всегда
-vили другой volume name; - «Откатиться нечем»: prune -a;
- «Файлы на месте, приложение не видит» — UID после смены USER: права;
- bind
./dataотносительный, systemd WorkingDirectory другой.
| Команда | Контейнеры | named volume |
|---|---|---|
up -d | recreate | жив |
down | удалены | жив |
down -v | удалены | удалён |
rm -f контейнера | удалён | жив |
Возможные причины
- Шпаргалка с
down -v. - Сменили ключ volume в YAML (
appdatavsapp_data). - Убрали
name: app_data, появился префикс проекта. - Переехали с named на bind в пустой каталог.
- Миграция приложения дропнула схему (это не Docker).
prune --volumes.
Диагностика
cd /opt/app
docker compose config --volumes
docker volume inspect app_data --format '{{.Name}} {{.Mountpoint}}'
docker inspect app-web-1 --format '{{range .Mounts}}{{.Name}} {{.Destination}}{{"\n"}}{{end}}'
grep -n image: compose.yaml
docker compose psСнимите бэкап, если меняется схема БД. Запишите текущий digest:
docker inspect --format '{{.Image}}' app-web-1
docker compose imagesРешение
Сценарий A. Штатный rolling одного сервиса
image: registry.example:5000/app/web:1.2.4cd /opt/app
docker compose pull web
docker compose up -d web
docker compose ps
docker inspect app-web-1 --format '{{range .Mounts}}{{.Name}} {{end}}'Проверка, что volume тот же app_data. Не указывайте --renew-anon-volumes вслепую (пересоздаёт анонимные; named обычно не трогает, но не смешивайте с экспериментами).
Сценарий B. Нужна миграция БД
- Бэкап
app_data. - Поднять новый образ, который гоняет migrate как one-shot
restart: "no", не always. - Затем
up -d web. - Если migrate упал — не
down -v, откатите тег приложения, restore только если migrate испортил данные.
docker compose run --rm migrateСценарий C. Откат
# вернуть image: ...:1.2.3 в YAML
docker compose pull web
docker compose up -d webОбраз :1.2.3 должен быть локально или в registry. Поэтому не prune -a сразу после выкладки.
Сценарий D. Смена USER в новом образе
stop, chown Mountpoint под новый uid, start. Иначе «данные есть, EACCES». Не rm volume.
Сценарий E. Переезд bind → named
Копирование в окно (rsync в новый volume через alpine), переключение YAML, проверка, потом старый bind не удалять неделю. Процедура копирования — статья бэкапа.
Тег в YAML и digest в inspect должны совпасть с тем, что вы хотели выкатить. :latest на registry.example:5000 мог уехать вперёд, пока вы писали runbook — pin 1.2.4. После pull сравните Id/RepoDigests с CI. Recreate без pull оставит старый локальный latest.
Анонимные volumes (docker inspect Name пустой или случайный hash) при recreate иногда получают новый том, если не named. Если «данные пропали» после обновления web, проверьте, не писали ли в анонимный mount /var/lib/app без ключа в секции volumes:. Исправление: name: app_data, копирование из старого hash-тома, пока его не съел prune. docker volume ls сразу после инцидента, до любой чистки.
Миграции схемы: образ 1.2.4 может требовать колонки, которых нет в дампе 1.2.3. Откат образа без отката миграции оставит БД «впереди». Runbook: migrate forward совместимый или restore volume из бэкапа, снятого до migrate. Не down -v как откат миграции.
up --build на проде собирает не тот контекст (нет git tag) и затирает понимание digest. Собирайте в CI, пушьте, на host.example только pull. Исключение — аварийный hotfix с записью в тикет и последующей заменой тем же Dockerfile из git.
Как проверить, что проблема устранена
docker compose exec web ls -la /var/lib/app | head
docker inspect app-web-1 --format '{{.Config.Image}}'
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/health
docker volume inspect app_data --format '{{.CreatedAt}}'CreatedAt тома не должен стать «только что», если вы не создавали том заново. Данные на месте, health 200, тег 1.2.4.
Если не помогло
- Пустой каталог: смотрите другое имя тома
app_app_data. Подключите старый:external: true+name:. - Новый образ ждёт другую структуру каталогов — это миграция приложения, restore из бэкапа.
- Два реплики web на одном томе sqlite — порча, не «Docker съел».
latestуехал вперёд, отката нет — только registry или бэкап.
Если новый образ сменил путь данных (/var/lib/app → /data), том смонтирован, но приложение смотрит в пустой каталог. Это не потеря volume: добавьте mount на новый путь или symlink в образе, не down -v. Сверьте docker inspect Mounts с конфигом приложения.
Профилактика
- Pin тега, не
:latest. - Запрет
down -vв runbook обновления. - Хранить N старых образов до SLA отката.
- Бэкап volume перед migrate.
- Чеклист: Mounts name до/после.
Перед выкладкой снимите docker compose images и volume inspect app_data в каталог заявки. После — те же команды. Если CreatedAt тома обновился «только что», вы создали новый том: ищите, какой ключ YAML сменился, и подключите старый как external. Не пишите данные в новый пустой том «чтобы заработало», пока не решите судьбу старого.
FAQ
force-recreate потеряет данные?
Нет для named volume. Пересоздаётся контейнер.
Нужно ли останавливать db при обновлении web?
Зависит от миграции. Само обновление образа web том db не трогает.
build vs pull
Сборка на проде хуже. Pull с registry.example:5000. Build — на CI.
Анонимный volume vs named
Анонимный легче потерять при recreate в некоторых сценариях. Для app_data только named с name:.
prune после удачного обновления
Через TTL отката, dangling без -a, не volumes.