Прямой ответ
Контейнер эфемерен: его записываемый слой живёт до пересоздания (docker rm, обновление образа, рестарт оркестратора) — данные, которые должны пережить контейнер, выносятся в монтирования, и их три вида. Volume (docker volume create dbdata / в compose — именованный том) — управляется Docker, живёт в его области (обычно /var/lib/docker/volumes/<имя>/_data), не удаляется при удалении контейнера — правильный выбор для состояния (БД, очереди, индексы); bind mount (-v /home/me/project:/app или ./src:/app) — произвольный путь хоста внутрь контейнера — для разработки (код с хоста, hot-reload — локальный стек) и конфигов, в проде — реже (привязка к структуре хоста, права/SELinux-нюансы); tmpfs (--tmpfs /tmp) — память, исчезает с контейнером (секреты на время работы, кеш — ничего, что нельзя потерять). Ключевые грабли: (1) анонимные тома (VOLUME /var/lib/postgresql/data в образе без явного имени — Docker создаёт безымянный том; пересоздали контейнер без -v/compose-имени — данные «исчезли» в недрах /var/lib/docker — «postgres потерял базу после update» — почти всегда это); (2) docker rm -v / docker compose down -v — -v удаляет тома (флаг «почистить» стирает данные — down без -v сохраняет — различать всегда); (3) права (контейнер от не-root пользователя (hardening) не пишет в том, созданный root'ом — UID/GID совпадение или chown при инициализации — «permission denied на свежем томе»); (4) том ≠ бэкап (том на том же диске/хосте — смерть диска/rm — данных нет — 3-2-1 — бэкап из тома делается явно). Прод-уровень: данные, которые важны, всё чаще выносят из контейнерных томов вовсе — объектное хранилище для файлов (MinIO/S3), managed-БД для состояния, а контейнер остаётся бессостоятельным вычислителем (12-factor); том — для self-hosted одиночек и dev-сред.
Механика: тома, compose, драйверы
Команды: docker volume create/ls/inspect/rm/prune (prune — удалить неиспользуемые — осторожно: «не используется» = не подключён к живому контейнеру — том с данными между деплоями — кандидат на удаление — prune в cron на проде — способ потерять данные); запуск: docker run -v dbdata:/var/lib/postgresql/data postgres:16 (имя:путь — именованный том; /abs/path:/path — bind; без имени слева — анонимный — избегать); compose-декларация: services: db: volumes: ["dbdata:/var/lib/postgresql/data"] + volumes: dbdata: верхнего уровня (имя переживает docker compose down — без -v; docker compose down -v — удаление — знать разницу как таблицу умножения); driver — local по умолчанию (файлы хоста); драйверы-плагины — сетевые/облачные (NFS/EBS-класс — том переживает смену хоста — для самохостных кластеров; K8s-мир — свои сущности PV/PVC — K8s-хранение в контексте ресурсов); volume из образа (инструкция VOLUME в Dockerfile — объявление «здесь данные» — эффект: изменения в этот путь после объявления игнорируются сборкой, при запуске без явного тома создаётся анонимный — источник граблей №1 — образы БД так делают — всегда монтировать именованный том явно); доступ из нескольких контейнеров (один том на несколько контейнеров — возможен (sidecar-бэкапер читает данные БД — паттерн ниже), но конкурентная запись без координации — порча (две БД на один каталог — гарантированная катастрофа — «том на двоих» только для read-сценариев/одного писателя); производительность — local-том ≈ нативная ФС (bind на macOS/Windows в VM — заметно медленнее (9p/gRPC-FUSE — файлы через гипервизор — hot-reload с тысячью файлов тормозит — на маках код держат в томе с синхронизацией (mutagen/virtiofs) — боль известная); tmpfs — память (размер ограничен RAM — --tmpfs /tmp:size=100m — лимит обязателен, иначе съест память).
Данные в практике: права, бэкап, перенос
Права: контейнер от uid 1000 (не-root) + том, принадлежащий root — отказ записи; решения: user: в compose с соответствием UID хоста (dev), инициализация прав в entrypoint (root-старт → chown → setuid — паттерн официальных образов), :z/:Z на bind при SELinux (RHEL-класс — метки контекста — без них «permission denied» при всех верных uid); проверка — docker run --rm -v vol:/data alpine ls -lan /data (кто владелец — до старта приложения — экономит час гаданий). Бэкап тома (паттерн sidecar-контейнера): docker run --rm -v dbdata:/data -v $(pwd)/backup:/backup alpine tar czf /backup/dbdata-$(date +%F).tgz -C /data . — архив на хост → дальше по 3-2-1; для БД — не tar горячих файлов (несогласованный снимок — восстановление может не сойтись), а логический дамп изнутри (pg_dump через exec — Postgres-бэкапы) — том-снапшот допустим при остановленной БД или LVM/файлсистемных снапшотах; восстановление — новый том + распаковка/pg_restore + проверка (непроверенный бэкап — не бэкап — учебное восстановление раз в квартал); перенос между хостами — архив тома / rsync каталога (при остановленном контейнере) / для БД — dump-restore или логическая репликация (онлайн); миграция bind → volume — скопировать данные в том, переключить монтирование, проверить, потом чистить bind (порядок с проверкой — не наоборот); размер/очистка — docker system df -v (тома по размерам — «кто съел 80 ГБ» — обычно логи/БД-файлы в томах — дисковая диагностика); логи контейнеров — отдельно (json-лог в /var/lib/docker/containers — не в томе — ротация logging-driver'ом — «диск кончился от логов» — max-size/max-file в daemon.json или compose).
Практический сценарий: «после обновления исчезла база» и наведение порядка
Исходник: dev-сервер команды — docker-compose стек (Postgres + Redis + Gitea-класс), после docker compose down && up -d с новым образом — «база пустая»; разбор: compose-файл монтировал /var/lib/postgresql/data без имени (- /var/lib/postgresql/data — анонимный том) — down удалил контейнер с его анонимным томом, up создал новый пустой; данные физически живы в безымянном томе-сироте (docker volume ls — local 9f3a... — восстановлено подключением сироты — повезло). Наведение порядка (день): (1) все состояния — именованные тома в compose (dbdata:, redisdata:, giteadata: — down больше не трогает данные; down -v — только осознанно — правило в README команды); (2) права: сервисы от не-root, entrypoint-инициализация прав на томах (проверка ls -lan — диагностика-привычка «посмотреть до, а не гадать после»); (3) бэкап: cron-скрипт — pg_dump через docker exec → gzip → ротация 14 дней + еженедельная копия на второй хост (3-2-1); том Redis — признан несущественным (кеш — redis — appendonly выключен осознанно — восстановление прогревом), том Gitea — tar+дампы репозиториев; (4) учебное восстановление: развернули дамп в чистый стек на другой машине — 22 минуты, проверка контрольных сумм — бэкап признан рабочим (до этого «бэкапы вроде делались» — веры не было); (5) логи: json-file с max-size=50m/max-file=3 в daemon.json (диск dev-сервера рос на 2 ГБ/неделю от логов контейнеров — найдено docker system df -v); (6) мониторинг диска + алерт 85% (метрики) — тома и логи под наблюдением. Итог: данные переживают любые деплои/пересоздания, бэкап проверен восстановлением, «исчезнувшая база» как класс инцидентов закрыта; вывод в чек-лист инфраструктуры: «состояние — только именованные тома, down -v — только руками и с подтверждением, бэкап — с ежеквартальной проверкой восстановления».
FAQ
Volume против bind mount — когда что? Volume — состояние, которым владеет приложение (БД/очереди/индексы): управляется Docker, переносится драйверами, не зависит от путей хоста, безопаснее по правам; bind mount — разработка (код с hot-reload), конфиг-файлы хоста (nginx.conf, .env — конфиг-практика), интеграция с хостовыми инструментами (сокет docker.sock — с осторожностью — root-эквивалент); «БД на bind mount в проде» — работает, но права/SELinux/переносимость — лишние боли без выигрыша; «код в volume в dev» — наоборот, теряет hot-reload (файлы в томе не видны редактору хоста — кроме синхронизаций).
Почему данные «пропали» после docker compose down?
Два сценария: down -v (том удалён флагом — данные уничтожены — восстанавливать из бэкапа) или монтирование было анонимным/не объявленным в верхнеуровневых volumes (том жил при контейнере — умер с ним); проверка судьбы — docker volume ls -a (сироты видны — шанс на спасение есть, пока не сработал prune); профилактика — именованные тома в compose-файле + никогда не запускать down -v/system prune --volumes без ясного намерения (alias с подтверждением — дешёвая страховка); «docker system prune -a --volumes» в туториалах — команда-убийца данных — читать флаги, не копировать вслепую.
Как правильно бэкапить БД в контейнере?
Не снапшотом горячих файлов (tar по работающему каталогу данных — несогласованно — восстановление лотерея); правильно: логический дамп движка (pg_dump -Fc через docker exec — Postgres; mysqldump/mongodump — свои утилиты) по cron → ротация → копия на другой носитель/хост (3-2-1); для больших БД — снапшот на уровне ФС/LVM при pg_backup_start/stop (согласованность без простоя) или непрерывная архивация WAL (PITR — точка восстановления произвольная); проверка — ежеквартальное учебное восстановление в чистый контейнер с замером времени (RTO — SLO-мышление применимо к восстановлению).
Нужны ли volumes в Kubernetes? Там своя модель: emptyDir (эфемерный, как том без персистентности), hostPath (bind-аналог — редко и осторожно), PersistentVolume/Claim (персистентность через storage-классы — облачные диски/NFS — K8s-ресурсы рядом про запросы/лимиты) — декларативно, переезжает с подом по правилам класса; докерные volumes в K8s не участвуют (рантайм может быть containerd — докер-специфика исчезла); принцип тот же: состояние — в персистентные сущности с бэкапами ( Velero/снапшоты-классы — свои инструменты), контейнер — вычислитель; «БД в K8s на emptyDir» — данные до первого перепланирования — антипаттерн уровня «анонимный том для прода».
Источники: документация Docker (volumes, bind mounts, compose volumes) — ссылки в шапке.