Тома и хранение данных: volumes и bind mounts

Рейтинг: 80.6% · 16 голосов
Практический курс по Docker: образы, контейнеры, тома, сети, Compose и продакшен. Уроки по главам с обсуждением.
Ответить
Аватара пользователя
Marina_DevOps
Сообщения: 25
Зарегистрирован: 11 май 2026, 05:31

Тома и хранение данных: volumes и bind mounts

Сообщение Marina_DevOps »

АкадемияDocker с нуляГлава 5 из 17
Оглавление курса (17)
  1. Что такое Docker и какие задачи он решает
  2. Установка Docker и запуск первого контейнера
  3. Образы: слои, теги и реестр Docker Hub
  4. Пишем свой Dockerfile
  5. Тома и хранение данных: volumes и bind mounts (вы здесь)
  6. Сети в Docker: связываем контейнеры между собой
  7. Переменные окружения и конфигурация контейнеров
  8. Docker Compose: поднимаем многоконтейнерное приложение
  9. Оптимизация образов: multi-stage сборка, размер и кэш слоёв
  10. Логи, отладка и мониторинг контейнеров
  11. Базовая безопасность контейнеров
  12. Подготовка к продакшену: что важно учесть
  13. Dockerfile глубже: ENTRYPOINT и CMD, HEALTHCHECK, .dockerignore, запуск не от root
  14. Реестры образов: приватные registry, push и pull, теги и digest, imagePullSecrets
  15. BuildKit и buildx: multi-arch сборки, секреты сборки, экспорт кэша
  16. Docker в CI/CD: автосборка, сканирование образов (Trivy, Docker Scout), публикация
  17. Итоговый проект и куда расти: от Dockerfile до прода, обзор оркестрации (Kubernetes, Podman, OCI)
В четвертой главе мы собрали свой образ, и контейнеры из него живут своей жизнью: остановил, удалил, запустил новый. Проблема в том, что вместе с контейнером умирают и все данные, которые он успел записать. Эта глава про то, как отвязать данные от жизненного цикла контейнера: volumes, bind mounts и tmpfs.

Почему данные пропадают:

У каждого контейнера поверх слоев образа (про них была глава 3) есть свой тонкий слой для записи. Все, что процесс пишет на диск, попадает туда. Удалили контейнер, удалился и этот слой. Легко проверить:

Код: Выделить всё

docker run --name scratch alpine:3.22 sh -c "echo hello > /data.txt"
docker rm scratch
docker run --rm --name scratch alpine:3.22 cat /data.txt
cat ругается, файла нет: новый контейнер стартует с чистым слоем записи.

С postgres результат будет похожий, но механика другая, и ее стоит понимать:

Код: Выделить всё

docker run -d --name db -e POSTGRES_PASSWORD=secret postgres:16
docker exec db pg_isready -U postgres
docker exec db psql -U postgres -c "CREATE DATABASE shop;"
docker rm -f db
docker run -d --name db -e POSTGRES_PASSWORD=secret postgres:16
docker exec db pg_isready -U postgres
docker exec db psql -U postgres -c "\l"
Два момента. Первый: сразу после docker run -d база не готова, initdb работает несколько секунд, и psql упадет с ошибкой подключения. Поэтому между запуском и psql дергайте pg_isready, пока не ответит accepting connections, или смотрите docker logs db. Второй: базы shop после пересоздания контейнера действительно нет, но пропала она не из слоя записи. В Dockerfile официального образа postgres объявлена инструкция VOLUME /var/lib/postgresql/data, поэтому Docker при каждом запуске сам создает анонимный том со случайным именем и кладет данные туда. Причем docker rm -f без флага -v этот том даже не удаляет: он остается висеть на диске, просто новый контейнер получает другой, свежий и пустой. Итог тот же, данные недоступны, плюс мусорный том в docker volume ls. Для базы данных это катастрофа, поэтому идем дальше.

Volumes:

Том (volume) это хранилище, которым управляет сам Docker. На Linux при обычной rootful-установке тома лежат в /var/lib/docker/volumes, в rootless-режиме в ~/.local/share/docker/volumes, но лезть туда руками не нужно, работаем через CLI.

Код: Выделить всё

docker volume create pgdata
docker run -d --name db \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16
Теперь повторите эксперимент с удалением. Создайте базу, снесите контейнер, запустите новый с тем же -v pgdata:... и база на месте. Контейнер сменился, том остался.

Полезные команды: docker volume ls (список), docker volume inspect pgdata (покажет Mountpoint, то есть путь на хосте, драйвер, метки и Scope; каких-либо сведений о контейнерах там нет, кто использует том, смотрите через docker ps -a --filter volume=pgdata или docker system df -v), docker volume rm pgdata (удалить; команда откажет, если том привязан хотя бы к одному контейнеру, даже остановленному, сначала придется удалить сам контейнер). Кстати, docker volume create можно и не вызывать: если тома с таким именем нет, Docker создаст его при запуске сам.

Bind mounts:

Bind mount пробрасывает в контейнер конкретный каталог или файл с хоста. Классический сценарий: разработка, когда код редактируете на хосте, а исполняется он в контейнере.

Код: Выделить всё

docker run -d --name web \
  -p 8080:80 \
  -v "$(pwd)/site:/usr/share/nginx/html:ro" \
  nginx:1.27
Поменяли index.html в каталоге site, обновили страницу, изменения видны сразу, без пересборки образа. Суффикс :ro монтирует только для чтения, контейнер не сможет испортить ваши файлы.

Отличить просто: если в -v слева имя без слешей, это том. Если путь (начинается с / или ./), это bind mount.

Есть и второй синтаксис, --mount. Он многословнее, но честнее:

Код: Выделить всё

docker run -d --name db \
  --mount type=volume,source=pgdata,target=/var/lib/postgresql/data \
  postgres:16
Главное практическое отличие: -v с несуществующим путем на хосте молча создаст пустой каталог (от root), а --mount с type=bind в той же ситуации упадет с ошибкой. Для скриптов и продакшена я предпочитаю --mount именно за это.

Третий тип монтирования, tmpfs. Данные живут только в оперативной памяти и исчезают при остановке контейнера, на диск не попадают вообще:

Код: Выделить всё

docker run -d --name web \
  --mount type=tmpfs,target=/tmp/cache,tmpfs-size=64m \
  nginx:1.27
Подходит для временных файлов и чувствительных данных (токены, сессии), которые не должны осесть ни в слое контейнера, ни на хосте. Ограничения: работает только с Linux-контейнерами, и один tmpfs нельзя разделить между несколькими контейнерами.

Что когда использовать: данные сервисов (базы, очереди, загруженные пользователями файлы) храните в томах. Bind mounts оставьте для исходников при разработке и для подсовывания конфигов. tmpfs для временного и секретного.

Бэкап и восстановление томов:

На Linux с rootful-установкой том это обычный каталог /var/lib/docker/volumes/pgdata/_data, и скопировать его можно напрямую. Но такой способ не переносим: на Docker Desktop хост вообще виртуалка и до этого пути еще надо добраться, в rootless путь другой, а у нелокальных драйверов каталога на хосте может не быть вовсе. Поэтому классический паттерн бэкапа, который работает везде одинаково: одноразовый контейнер, которому даем и том, и каталог хоста.

Код: Выделить всё

docker run --rm \
  -v pgdata:/data:ro \
  -v "$(pwd):/backup" \
  alpine:3.22 tar czf /backup/pgdata.tar.gz -C /data .
Восстановление зеркально:

Код: Выделить всё

docker run --rm \
  -v pgdata:/data \
  -v "$(pwd):/backup" \
  alpine:3.22 tar xzf /backup/pgdata.tar.gz -C /data
Перед tar-бэкапом тома с работающей базой остановите контейнер, иначе копия выйдет неконсистентной. Для живого postgres правильнее pg_dump, но это уже не про Docker.

Типичные грабли:

Первое. Если примонтировать пустой каталог хоста поверх пути, где в образе уже что-то лежало, содержимое образа "исчезнет", вы увидите пустой хостовый каталог. С именованными томами поведение другое: при первом монтировании пустого тома Docker копирует в него файлы из образа. Наглядный пример nginx: примонтируйте новый том в /usr/share/nginx/html, и внутри окажется дефолтная страница из образа. А вот postgres тут не показатель: каталог /var/lib/postgresql/data в самом образе пуст, копировать нечего, поэтому initdb инициализирует кластер с нуля что с новым именованным томом, что с пустым bind mount.

Второе. Права. Процесс в контейнере работает под своим UID (у postgres это 999), и если каталог на хосте принадлежит root без прав на запись для остальных, получите permission denied. Лечится chown на хосте под нужный UID. Отдельная история на Fedora, RHEL и прочих системах с включенным SELinux: там bind mount дает permission denied даже при правильных UID и правах, потому что у файлов нет подходящей SELinux-метки. Добавьте к монтированию суффикс :z (общая метка, каталог можно шарить между контейнерами) или :Z (приватная метка под один контейнер), например -v "$(pwd)/site:/usr/share/nginx/html:ro,z". Только не вешайте :z и :Z на системные каталоги вроде /home или /var, relabel перепишет метки и поломает систему.

Третье. docker volume prune. По умолчанию команда сносит только анонимные неиспользуемые тома, но с флагом -a удалит и именованные, если они не подключены к контейнерам. Контейнер с базой удален, значит его том с точки зрения Docker уже не используется. Перед prune -a всегда смотрите docker volume ls.

Четвертое. Анонимные тома. Инструкция VOLUME в Dockerfile (как у того же postgres) или -v /data без имени слева создает том со случайным именем-хэшем. Они копятся незаметно и едят диск, наш первый эксперимент в этой главе как раз оставил после себя такой. docker rm -v имя_контейнера удаляет их вместе с контейнером.

И про скорость: на macOS и Windows bind mounts медленнее, чем на Linux, потому что файлы гоняются между хостом и виртуальной машиной Docker Desktop. На macOS дефолтный нынче VirtioFS заметно сократил разрыв по сравнению со старым gRPC-FUSE, но на каталогах из десятков тысяч мелких файлов вроде node_modules разница все еще ощутима, такие каталоги лучше держать в томе. На Windows с WSL2 главное правило: держите код внутри файловой системы WSL, bind mount с диска C: в контейнер работает медленно.

Итог:

Вы научились отделять данные от контейнера: тома для боевых данных, bind mounts для разработки, tmpfs для временного, --mount там, где важна предсказуемость, плюс бэкап тома через одноразовый контейнер с tar. В главе 6 свяжем контейнеры по сети, а в главе 8 увидите, как все это, включая тома, описывается декларативно в docker-compose.yml.
👍2 ❤️1 🔥2 😄 🤔1
✔ Лучший ответ сформирован автоматически — Spyce
Marina_DevOps писал(а):-v с несуществующим путем на хосте молча создаст пустой каталог (от root), а --mount с type=bind в той же ситуации упадет с ошибкой так вот откуда у меня на vps пустые папки от рута рядом с проектом, которые rm без sudo не берет. неделю гадал, уже думал кто-то по ssh лазит. перехожу на --mount, спасибо
Перейти к ответу →
Аватара пользователя
Spyce
Сообщения: 2
Зарегистрирован: 16 май 2026, 17:13

Re: Тома и хранение данных: volumes и bind mounts

Сообщение Spyce »

✔ Лучший ответ — сформирован автоматически
Marina_DevOps писал(а):-v с несуществующим путем на хосте молча создаст пустой каталог (от root), а --mount с type=bind в той же ситуации упадет с ошибкой
так вот откуда у меня на vps пустые папки от рута рядом с проектом, которые rm без sudo не берет. неделю гадал, уже думал кто-то по ssh лазит. перехожу на --mount, спасибо
👍1 ❤️ 🔥 😄 🤔
Аватара пользователя
haskellkun
Сообщения: 1
Зарегистрирован: 12 май 2026, 20:40

Re: Тома и хранение данных: volumes и bind mounts

Сообщение haskellkun »

а как тома бэкапить? хочу раз в сутки складывать копию pgdata на отдельный диск. правильно понимаю, что надо поднимать временный контейнер с двумя маунтами и делать tar внутри него? docker volume export в доках не нашел, может плохо искал
👍 ❤️1 🔥1 😄 🤔
Аватара пользователя
asdadad
Сообщения: 1
Зарегистрирован: 30 май 2026, 22:08

Re: Тома и хранение данных: volumes и bind mounts

Сообщение asdadad »

про uid в точку. у меня grafana с bind mount не стартовала, оказалось у нее uid 472, а каталог был от рута. причем с именованным томом та же grafana заводится без танцев, видимо как раз из-за копирования файлов из образа при первом монтировании. жаль раньше этого урока не было
👍1 ❤️ 🔥 😄 🤔
Аватара пользователя
juveleo
Сообщения: 2
Зарегистрирован: 16 май 2026, 18:50

Re: Тома и хранение данных: volumes и bind mounts

Сообщение juveleo »

наконец дошло, почему nginx у меня отдавал 403 вместо сайта. я монтировал каталог, который еще не создал, docker сделал его пустым, и nginx было нечего отдавать. два вечера убил на конфиги nginx, а дело было в одной строчке с -v
👍1 ❤️ 🔥 😄 🤔1
Ответить
← Предыдущая глава
Пишем свой Dockerfile
Следующая глава →
Сети в Docker: связываем контейнеры между собой

Все главы курса «Docker с нуля»

Поделиться темой: ✈ Telegram VK

Вернуться в «Docker с нуля»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость