Тома, bind mount и tmpfs: три способа сбежать из записываемого слоя
Именованные тома — управляемое Docker устойчивое хранилище под /var/lib/docker/volumes; bind mount вставляет путь хоста напрямую; tmpfs живёт в RAM. Каждый выбирает свой компромисс по переносимости, семантике UID и задержке записи — и ошибка теряет данные или ломает права.
Миграция выглядела чистой: перевести контейнер Postgres с bind mount на именованный том к продакшен-катоверу. Команда заменила -v /data/pg:/var/lib/postgresql/data на -v pgdata:/var/lib/postgresql/data, передеплоила и наблюдала, как Postgres инициализирует совершенно новую пустую базу — восьминедельные прод-данные всё ещё лежали в /data/pg на хосте, теперь не смонтированные никуда, а приложение радостно отдавало пустую схему и принимало новые записи. Сорок минут никто не замечал, потому что приложение не падало; оно просто потеряло всю историю аккаунта клиента. Глубже резануло неделей позже на втором хосте: тот же именованный том, но добавили bind mount исходников для hot-reload, контейнер работал под UID 1000, а файлы хоста принадлежали UID 0 — каждая запись падала с permission denied, и фреймворк проглатывал ошибки как «файл не найден». Три механизма хранения, три разные формы отказа, одна корневая причина: команда трактовала -v как одну фичу, когда это три.
Именованные тома: хранилищем владеет Docker
Именованный том — это хранилище, которое движок создаёт и которым управляет. docker volume create pgdata материализует директорию в /var/lib/docker/volumes/pgdata/_data, и монтирование её в /var/lib/postgresql/data заставляет контейнер писать прямо в эту директорию хоста, полностью обходя драйвер хранилища overlay2. Этот обход — вся история про производительность: записи идут напрямую в ФС хоста, а не через union-монтирование, поэтому нет налога copy-up и лишнего слоя для ядра. Для нагруженной на запись задачи вроде базы том нужен не только ради долговечности; он ощутимо быстрее записываемого слоя и без файловых затыков copy-up.
$ docker volume inspect pgdata --format '{{ .Mountpoint }}'
/var/lib/docker/volumes/pgdata/_data
# Том переживает контейнер; данные живут дольше любого rm/пересоздания:
$ docker run -d -v pgdata:/var/lib/postgresql/data postgres:16
$ docker rm -f $(docker ps -lq) # контейнера нет
$ docker run -d -v pgdata:/var/lib/postgresql/data postgres:16 # данные на местеТома переносимы и удобны для бэкапа: поскольку Docker владеет путём и жизненным циклом, вы бэкапите, мигрируете и переносите их между хостами и между Linux- и Windows-контейнерами, не заботясь о раскладке директорий хоста. У них есть важное поведение при первом запуске: когда вы монтируете пустой именованный том поверх директории контейнера, которую заполнил образ, Docker копирует содержимое образа в том при первом использовании — так что монтирование пустого pgdata поверх свежего образа Postgres сохраняет seed-файлы образа. (Эта копия происходит только для пустых именованных томов, никогда для bind mount и никогда, если в томе уже есть данные — что и есть ловушка из истории: другой пустой том инициализировал чистую базу.)
Bind mount: сырой путь хоста, по правилам хоста
Bind mount отображает конкретный путь хоста прямо в контейнер: -v /data/pg:/var/lib/postgresql/data или явный --mount type=bind,source=/data/pg,target=.... Нет управляемой Docker директории и нет копии при первом запуске — что лежит на пути хоста, то и видит контейнер, немедленно и двунаправленно. Это правильный инструмент для dev-hot-reload (смонтировать дерево исходников, редактировать на хосте, контейнер видит) и для проброса конфиг-файлов хоста. Но вы наследуете семантику ФС хоста целиком, и острый угол — владение UID/GID.
Права Linux-файлов числовые. У контейнера свой /etc/passwd, но ядро применяет права по числу, а не по имени. Если директория хоста принадлежит UID 0, а процесс контейнера работает под UID 1000 (как и должен закалённый образ), каждая запись получает EACCES — permission denied — независимо от того, как «выглядит» имя пользователя внутри контейнера. Для обычного bind mount нет автоматического ремаппинга UID (rootless Docker и user namespaces это меняют, но демон по умолчанию — нет), так что директорию хоста нужно chown-нуть на UID, под которым контейнер реально работает, либо контейнер должен работать под UID, владеющим файлами хоста. Это самый частый отказ bind mount в проде, и фреймворки регулярно маскируют его под ошибку отсутствующего файла.
Контейнер под UID 1000 делает bind mount директории хоста, принадлежащей root (UID 0), и записи падают с permission denied. В чём на самом деле дело?
tmpfs: хранилище, которое есть RAM
Прежде чем тянуться за томом или bind mount для токенов сессий или временного каталога сортировки, спроси себя: должны ли эти байты пережить рестарт? Если нет — tmpfs правильный выбор. Монтирование tmpfs не устойчивое и не на диске — это ФС в памяти. --tmpfs /tmp или --mount type=tmpfs,destination=/tmp,tmpfs-size=64m даёт контейнеру быструю черновую область, которая никогда не касается ни драйвера хранилища, ни диска, и исчезает при остановке контейнера (она ещё эфемернее записываемого слоя, который хотя бы переживает stop/start). Применяют для двух вещей: секреты и чувствительные данные, которые никогда не хочется писать на диск, и высокооборотный черновик (файлы сессий, временные данные рендера, спил сортировки), где нужна скорость и нулевая долговечность. Подвох в том, что tmpfs потребляет RAM хоста: без tmpfs-size tmpfs может расти, пока не исчерпает память и OOM killer не убьёт контейнер, так что прод-tmpfs всегда должен задавать явный лимит размера.
Вы монтируете пустой именованный том поверх /var/lib/postgresql/data на свежем образе Postgres, а на втором деплое делаете bind mount пустой директории хоста в тот же путь. Что станет с seed-файлами образа в каждом случае?
▸Почему это работает
Почему пустой именованный том копирует содержимое образа, а bind mount — нет? Потому что именованный том — управляемый Docker объект без прежней идентичности — Docker может безопасно засеять его из образа, чтобы контейнер стартовал в нормальном состоянии, и это то, что делает «просто добавь том» рабочим для stateful-образов. Bind mount указывает на путь хоста, который уже что-то значит для оператора; молча перезаписать его содержимым образа было бы разрушительно и неожиданно. Асимметрия — намеренная граница владения: Docker инициализирует то, чем владеет, и никогда не трогает то, чем владеете вы.
- 01Сравни именованные тома и bind mount по жизненному циклу, производительности, переносимости, поведению при первом запуске и модели прав.
- 02Когда тянуться за tmpfs вместо тома или bind mount, и в чём операционный риск?
Записываемый слой — это место, где состояние жить не должно, и Docker даёт три способа выхода, каждый со своим компромиссом. Именованный том — хранилище, которым владеет движок: он лежит в /var/lib/docker/volumes/<name>/_data, переживает любое удаление или пересоздание контейнера, переносим и легко бэкапится, потому что Docker абстрагирует директорию хоста, и пишет прямо в ФС хоста, минуя copy-up overlay2 — для базы это и долговечность, и реальная скорость. Его тихая суперсила — копия при первом запуске: смонтируй пустой именованный том поверх директории, которую заполнил образ, и Docker засеет том из образа, поэтому свежий том на Postgres всё ещё держит инициализированный кластер. Bind mount — противоположная философия: он вставляет конкретный путь хоста прямо в контейнер, двунаправленно, без управляемого объекта и без копии при первом запуске — идеален для dev-hot-reload и проброса конфига хоста, но правила ФС хоста вы наследуете целиком. Повторяющаяся прод-рана — владение UID: ядро применяет права по числу, ремаппинга по умолчанию нет, так что контейнер под не-root UID не может писать в директорию хоста под root, и фреймворки обожают выдавать получившийся EACCES за отсутствующий файл. tmpfs — третий вариант: RAM-ФС, которая никогда не касается диска, идеальна для секретов и высокооборотного черновика, исчезает в момент остановки контейнера и опасна без лимита tmpfs-size, потому что ест память хоста, пока не сработает OOM killer. Senior-рефлекс — называть механизм, а не флаг: тома для устойчивого состояния, bind mount для dev и конфига хоста с проверкой владения, tmpfs для эфемерного или чувствительного черновика с лимитом размера. Теперь, когда видишь флаг -v в Dockerfile или Compose-файле, первый вопрос не «это устойчиво?», а «какой из трёх механизмов здесь, и совпадает ли UID?»
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.