Тома в Docker часто воспринимают как простую вещь: “нужно сохранить данные — подключили volume”. Но на практике от выбора между writable layer, named volume, bind mount и tmpfs зависят производительность, надёжность, backup, переносимость и даже безопасность контейнера.
Отдельная проблема в том, что storage в контейнерном мире быстро копит технический мусор. Если не следить за логами, temporary-файлами и местами записи, через несколько месяцев можно обнаружить не только “данные сервиса”, но и десятки гигабайт container logs, которые спокойно съели диск хоста.
Эта статья — не про все варианты storage в Docker, а про практический baseline: где использовать volume, где bind mount, как разделять данные по назначению, как ограничивать доступ и как не превратить Docker-хост в склад случайных файлов.
В статье
- Почему нельзя писать всё внутрь контейнера
- Named volume, bind mount и
tmpfs - Как storage влияет на производительность
- Почему логи забивают диск хоста
- Как искать лишнюю запись внутри контейнера
- Как ограничивать доступ к данным
- Production baseline и backup
Почему вообще нельзя писать всё внутрь контейнера
У любого контейнера есть writable layer — слой, куда приложение пишет во время работы. Для чего-то мелкого это работает, но у такого подхода есть сразу несколько проблем:
- Данные живут в жизненном цикле контейнера.
- Производительность записи обычно хуже, чем при работе через volume.
- Backup и restore становятся менее прозрачными.
- Легко потерять данные при пересоздании контейнера.
Практическое правило простое:
- stateful данные не должны жить только во writable layer;
- временные данные лучше выносить в
tmpfsили чистить явно; - конфигурацию лучше подключать отдельно;
- логи нужно держать под контролем как отдельную тему.
Что есть в Docker на практике
Для большинства сервисов реально нужны три варианта:
flowchart TD
C["Контейнер"] --> WL["Writable Layer\n⚠ временный, привязан к контейнеру\nмедленная запись через storage driver"]
C --> NV["Named Volume\n✓ persistent, управляется Docker\nхорошая производительность"]
C --> BM["Bind Mount\n✓ прямой путь на хосте\nудобно для конфигов и dev"]
C --> TF["tmpfs\n⚡ только в RAM\nне пишет на диск, данные теряются"]
style WL fill:#f5d5d5,stroke:#c44e4e
style NV fill:#c9e4c5,stroke:#5b8a5e
style BM fill:#d5e5f5,stroke:#4a7ab5
style TF fill:#f9f3e3,stroke:#8b7355
1. Named volumes
Это тома, которыми управляет сам Docker. Они подходят, когда нужно:
- хранить данные между перезапусками контейнера;
- уменьшить зависимость от структуры каталогов на хосте;
- упростить backup и миграцию;
- получить более предсказуемую работу storage для приложения.
2. Bind mounts
Это прямое подключение каталога или файла с хоста в контейнер. Их удобно использовать, когда:
- нужно видеть файлы с хоста;
- требуется монтировать конфигурацию;
- вы хотите сохранить артефакты прямо в файловой системе хоста;
- у вас dev-сценарий с исходниками или локальными файлами.
3. tmpfs
Это mount в памяти. Он полезен для:
- временных файлов;
- кэшей, которые не нужно сохранять;
- чувствительных временных данных;
- снижения лишней записи на диск.
Где volume действительно лучше writable layer
Практическое отличие не только в “данные не пропадут”.
Docker официально рекомендует volumes как базовый механизм для persistent data, и на это есть хорошие причины:
- volume не завязан на жизненный цикл конкретного контейнера;
- volume проще переносить и бэкапить;
- volume обычно быстрее writable layer;
- volume чище отделяет данные приложения от состояния контейнера.
Если приложение пишет заметный объём данных, разница становится не теоретической, а вполне операционной.
Где это особенно заметно
- базы данных;
- object storage;
- очереди и брокеры;
- каталоги с загружаемыми файлами;
- кэши, которые нужно хранить между рестартами;
- CI-сервисы и build-кэши.
Named volume или bind mount
Это самый частый практический выбор.
Когда лучше named volume
Берите named volume, если:
- приложение хранит свои рабочие данные;
- не нужно регулярно лазить в каталог руками с хоста;
- важны переносимость и более чистая модель эксплуатации;
- хочется, чтобы Docker сам управлял жизненным циклом тома.
Пример:
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: secret # в production — через .env или secrets
volumes:
# Named volume: данные БД переживают пересоздание контейнера
- pg_data:/var/lib/postgresql/data
volumes:
pg_data:Когда лучше bind mount
Берите bind mount, если:
- нужен конкретный путь на хосте;
- контейнер должен читать локальный конфиг;
- вы хотите хранить файлы рядом с остальной инфраструктурой хоста;
- это dev-сценарий с исходниками или шаблонами.
Пример:
services:
angie:
image: docker.io/library/nginx:alpine
volumes:
- type: bind
source: ./angie/nginx.conf
target: /etc/nginx/nginx.conf
read_only: true # конфиг только для чтения — контейнер не сможет его изменитьГлавный компромисс здесь такой:
- named volume лучше подходит для данных приложения;
- bind mount лучше подходит для файлов, которые должны оставаться “видимыми” и управляемыми с хоста.
Короткая нотация тоже нормальна
Во многих примерах bind mount показывают в короткой форме через -v:
# Короткая нотация: хост_путь:контейнер_путь:режим
docker run --rm \
-v /path/to/local/html:/usr/share/nginx/html:ro \
nginx:alpineЗдесь:
/path/to/local/html— каталог на хосте;/usr/share/nginx/html— путь внутри контейнера;:ro— mount только для чтения.
Такая запись удобна для быстрых запусков и разовых команд.
Но у неё есть ограничение: когда mount’ов становится больше, появляются дополнительные опции и хочется явности, длинный синтаксис обычно читается лучше.
То есть практическое правило такое:
- для коротких
docker runсценариев-vвполне уместен; - для production-конфигурации и более сложных случаев лучше использовать длинную форму или Compose YAML.
Производительность: почему storage-выбор реально важен
Docker writable layer использует storage driver и union filesystem. Это удобно для контейнерной модели, но не лучший вариант для интенсивной записи.
В практическом порядке почти всегда действует такая иерархия:
- tmpfs — для временных данных и максимальной скорости без записи на диск;
- volume — хороший baseline для persistent data;
- writable layer — худший вариант для серьёзной записи;
- bind mount — зависит от хоста, файловой системы и сценария, но часто тоже хороший выбор, если нужен прямой доступ к данным.
Поэтому хороший вопрос не “умеет ли контейнер писать сюда”, а “куда ему вообще стоит писать”.
Простой ориентир
Если каталог:
- должен переживать пересоздание контейнера — это volume или bind mount;
- нужен только на время работы — это кандидат на
tmpfs; - содержит конфиг — это чаще bind mount, обычно
read-only; - генерирует много записи — не оставляйте это только во writable layer.
Логи контейнеров: тот самый тихий убийца диска
Это отдельный, но очень важный сюжет. Логи контейнера — это обычно не volume приложения, а лог-драйвер Docker. По умолчанию Docker часто пишет их через json-file.
Именно поэтому через долгое время можно обнаружить не только “данные сервиса”, но и огромные файлы логов на хосте. Если контейнер шумный, а ограничений нет, лог легко дорастает до десятков гигабайт.
Что с этим делать
Минимум — ограничивать размер логов.
На уровне Docker daemon:
{
"log-driver": "local",
"log-opts": {
"max-size": "10m", // ротация при достижении 10 МБ
"max-file": "5" // хранить не более 5 файлов
}
}Если используете json-file, тоже задавайте ротацию:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "5"
}
}На уровне Compose для отдельного сервиса:
services:
api:
image: example/api:latest
logging:
driver: local
options:
max-size: "10m"
max-file: "5"Практический вывод:
- volume решает persistence данных приложения;
- лог-драйвер и его лимиты решают storage-гигиену самого Docker-хоста;
- игнорировать второе так же опасно, как и первое.
Как диагностировать: “тома вроде есть, а контейнер всё равно растёт”
Это очень частый практический сценарий. Кажется, что persistent data уже вынесены в volumes, но через какое-то время диск хоста всё равно забивается, а размер контейнера или данных Docker растёт.
Обычно причины три:
- приложение пишет во writable layer в каталог, который не вынесен в volume;
- растут container logs;
- копятся временные файлы, кэши или артефакты внутри контейнера.
Шаг 1. Посмотреть общую картину по Docker
Начинать удобно с общей оценки:
# Сколько места занимают образы, контейнеры, volumes и build cache
docker system df
# Размер writable layer у каждого контейнера
docker ps --sizeЧто это даёт:
docker system dfпоказывает, сколько места занимают образы, контейнеры, volumes и build cache;docker ps --sizeпоказывает размер writable layer у конкретных контейнеров.
Если у контейнера заметно растёт именно writable size, это уже хороший сигнал, что приложение пишет куда-то внутрь контейнера, а не только в volume.
Шаг 2. Проверить логи контейнера
Перед тем как идти внутрь файловой системы контейнера, стоит исключить самую частую причину:
docker inspect myapp --format '{{.LogPath}}'Дальше уже на хосте можно посмотреть размер файла логов:
du -h /var/lib/docker/containers/<container-id>/<container-id>-json.logЕсли здесь гигабайты, причина уже найдена: это не данные приложения, а отсутствие нормальной ротации логов.
Шаг 3. Найти, куда именно приложение пишет внутри контейнера
Если проблема не в логах, следующий шаг — посмотреть крупные каталоги внутри самого контейнера:
# Топ-30 самых тяжёлых каталогов внутри контейнера
docker exec -it myapp sh -c 'du -xh / 2>/dev/null | sort -h | tail -n 30'Для более прицельной проверки часто достаточно пройтись по типичным местам:
docker exec -it myapp sh -c 'du -sh /tmp /var/tmp /var/log /var/cache /app /data 2>/dev/null'Обычно именно здесь и всплывают неожиданные кандидаты:
/tmp/var/log/var/cache/app/uploads/data- каталоги с экспортами, отчётами или временными архивами
Шаг 4. Сопоставить найденный путь с mount’ами контейнера
Когда подозрительный каталог найден, полезно проверить, вынесен ли он вообще наружу:
# Проверяем, какие пути вынесены наружу — если каталога нет в списке,
# данные живут только во writable layer
docker inspect myapp --format '{{json .Mounts}}'Если каталог, например /app/uploads, отсутствует среди mount’ов, а приложение активно туда пишет, значит эти данные живут только во writable layer контейнера.
Именно это и есть типичный сигнал: каталог нужно вывести в отдельный volume или bind mount.
Практический пример
Представим, что сервис пишет загружаемые файлы в /app/uploads, а отчёты складывает в /tmp/reports.
После проверки видно:
- writable layer растёт;
- логи не виноваты;
- внутри контейнера разрастается
/app/uploads; - этот путь не смонтирован отдельно.
Плохой вариант:
services:
app:
image: example/app:latestЛучше так:
services:
app:
image: example/app:latest
volumes:
- app_uploads:/app/uploads # persistent данные — в named volume
tmpfs:
- /tmp # временные файлы в RAM, не копятся на диске
volumes:
app_uploads:Здесь:
- persistent uploads вынесены в named volume;
- временные файлы в
/tmpбольше не копятся на диске хоста.
Как мыслить в таких случаях
Если контейнер растёт, полезно задавать себе очень прикладные вопросы:
- Это данные приложения или просто логи?
- Эти данные должны переживать пересоздание контейнера?
- Это постоянные данные или временный мусор?
- Этот путь должен жить в volume, bind mount или
tmpfs?
Обычно решение после диагностики выглядит так:
- persistent data — в отдельный volume;
- конфиг — в read-only bind mount;
- временные файлы — в
tmpfsили под явную очистку; - логи — под ротацию и лимиты.
Как ограничивать права доступа к томам
Тема прав доступа обычно всплывает позже, чем должна. Пока всё работает, кажется, что “container и так видит каталог”. Но на практике правильнее сразу думать, кому и зачем нужен доступ.
Есть несколько рабочих приёмов.
1. Монтировать read-only, где запись не нужна
Если контейнеру нужно только читать конфиг, сертификаты, статику или шаблоны, mount должен быть read-only.
Пример:
services:
app:
image: example/app:latest
volumes:
- type: bind
source: ./config/app.yaml
target: /etc/app/app.yaml
read_only: trueЭто простое ограничение, но оно сразу убирает целый класс случайных и вредных записей.
2. Разделять тома по назначению
Не стоит складывать всё в один большой mount /data.
Лучше разделять:
- данные приложения;
- конфиг;
- временные файлы;
- загружаемые пользователем файлы;
- кэши.
Пример:
services:
app:
image: example/app:latest
volumes:
- app_data:/var/lib/app
- type: bind
source: ./config
target: /etc/app
read_only: true
- type: tmpfs
target: /tmp
volumes:
app_data:Это и безопаснее, и проще для backup.
3. Запускать контейнер не от root
Если контейнер работает под непривилегированным пользователем, права на volume и bind mount становятся важной частью модели безопасности.
Пример:
FROM alpine:3.23
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
# --chown меняет владельца при копировании, без отдельного RUN chown
COPY --chown=app:app server /usr/local/bin/server
USER app
CMD ["server"]И дальше уже volume или bind mount нужно согласовать по владельцу и правам.
4. Для bind mount контролировать владельца и режимы на хосте
Bind mount — это прямой доступ к файлам хоста. Значит, ограничения прав должны жить не только в Compose, но и в самой файловой системе хоста:
- правильный
uid/gid; - режимы
chmod; - при необходимости ACL;
- отдельные каталоги под конкретные сервисы.
Если контейнеру не нужен доступ ко всему каталогу проекта, не давайте ему доступ ко всему каталогу проекта.
5. Не путать named volume и host-level контроль
У named volume меньше прямого host-level удобства. Это плюс для изоляции, но иногда минус для ручного контроля.
Если вам критично управлять файлами как обычными объектами в filesystem хоста, bind mount может быть практичнее. Если важнее именно контейнерная модель данных, named volume чаще предпочтительнее.
Пример разумной схемы для production-сервиса
services:
api:
image: example/api:latest
user: "10001:10001" # запуск под непривилегированным UID/GID
volumes:
- api_data:/var/lib/api # persistent данные приложения
- type: bind
source: ./config
target: /etc/api
read_only: true # конфиг только на чтение
- type: tmpfs
target: /tmp # временные файлы не копятся на диске
logging:
driver: local
options:
max-size: "10m"
max-file: "5"
volumes:
api_data:Здесь уже есть нормальный baseline:
- persistent data в отдельном volume;
- конфиг только на чтение;
- временные файлы не копятся на диске;
- логи ограничены по размеру;
- контейнер работает не от
root.
Backup и restore: думать нужно заранее
Volume полезен не сам по себе, а как часть понятной процедуры восстановления.
Хорошие вопросы:
- где физически лежат данные;
- как вы делаете backup volume;
- как восстановить сервис на другом хосте;
- что будет с версией приложения при restore;
- нужно ли отдельно бэкапить конфиг из bind mount.
Если вы используете named volumes, продумайте backup отдельно от контейнера. Если используете bind mounts, убедитесь, что они входят в обычную схему резервного копирования хоста.
И снова практическое правило:
Если через полгода вам трудно объяснить, как восстановить сервис с данными, storage-схема выбрана плохо.
Типичные ошибки
- Держать данные приложения только во writable layer контейнера.
- Использовать bind mount там, где логичнее named volume.
- Давать контейнеру доступ на запись к конфигу, который должен быть только для чтения.
- Складывать данные, кэш, временные файлы и конфиг в один общий mount.
- Запускать контейнер от
rootи не думать о владельцах каталогов. - Не ограничивать размер логов Docker.
- Считать, что “volume есть” автоматически означает “backup продуман”.
Короткий baseline
Если нужен очень короткий набор правил, то он такой:
- stateful данные — в volume или bind mount, но не только во writable layer;
- конфиг — отдельно и чаще всего
read-only; - временные файлы — в
tmpfs, если это оправдано; - логи Docker — с ограничением размера;
- контейнер — по возможности non-root;
- backup и restore — продуманы до проблем, а не после.
Тома в Docker — это не просто “где бы сохранить данные”. Это один из базовых инструментов надёжности и эксплуатационной дисциплины. Правильный выбор mount-стратегии уменьшает риск потери данных, снижает шум на хосте и делает поведение сервиса намного предсказуемее.
Документация и первоисточники
Если хочется проверить детали по официальным материалам Docker, полезно смотреть сюда:
Комментарии