Docker volumes: как использовать тома правильно для скорости, надёжности и контроля доступа

Как правильно использовать Docker volumes и bind mounts, почему это влияет на производительность и надёжность, и как ограничивать доступ к данным

Тома в Docker часто воспринимают как простую вещь: “нужно сохранить данные — подключили volume”. Но на практике от выбора между writable layer, named volume, bind mount и tmpfs зависят производительность, надёжность, backup, переносимость и даже безопасность контейнера.

Отдельная проблема в том, что storage в контейнерном мире быстро копит технический мусор. Если не следить за логами, temporary-файлами и местами записи, через несколько месяцев можно обнаружить не только “данные сервиса”, но и десятки гигабайт container logs, которые спокойно съели диск хоста.

Эта статья — не про все варианты storage в Docker, а про практический baseline: где использовать volume, где bind mount, как разделять данные по назначению, как ограничивать доступ и как не превратить Docker-хост в склад случайных файлов.

В статье

Почему вообще нельзя писать всё внутрь контейнера

У любого контейнера есть writable layer — слой, куда приложение пишет во время работы. Для чего-то мелкого это работает, но у такого подхода есть сразу несколько проблем:

  1. Данные живут в жизненном цикле контейнера.
  2. Производительность записи обычно хуже, чем при работе через volume.
  3. Backup и restore становятся менее прозрачными.
  4. Легко потерять данные при пересоздании контейнера.

Практическое правило простое:

  • 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

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 растёт.

Обычно причины три:

  1. приложение пишет во writable layer в каталог, который не вынесен в volume;
  2. растут container logs;
  3. копятся временные файлы, кэши или артефакты внутри контейнера.

Шаг 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 больше не копятся на диске хоста.

Как мыслить в таких случаях

Если контейнер растёт, полезно задавать себе очень прикладные вопросы:

  1. Это данные приложения или просто логи?
  2. Эти данные должны переживать пересоздание контейнера?
  3. Это постоянные данные или временный мусор?
  4. Этот путь должен жить в 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-схема выбрана плохо.

Типичные ошибки

  1. Держать данные приложения только во writable layer контейнера.
  2. Использовать bind mount там, где логичнее named volume.
  3. Давать контейнеру доступ на запись к конфигу, который должен быть только для чтения.
  4. Складывать данные, кэш, временные файлы и конфиг в один общий mount.
  5. Запускать контейнер от root и не думать о владельцах каталогов.
  6. Не ограничивать размер логов Docker.
  7. Считать, что “volume есть” автоматически означает “backup продуман”.

Короткий baseline

Если нужен очень короткий набор правил, то он такой:

  • stateful данные — в volume или bind mount, но не только во writable layer;
  • конфиг — отдельно и чаще всего read-only;
  • временные файлы — в tmpfs, если это оправдано;
  • логи Docker — с ограничением размера;
  • контейнер — по возможности non-root;
  • backup и restore — продуманы до проблем, а не после.

Тома в Docker — это не просто “где бы сохранить данные”. Это один из базовых инструментов надёжности и эксплуатационной дисциплины. Правильный выбор mount-стратегии уменьшает риск потери данных, снижает шум на хосте и делает поведение сервиса намного предсказуемее.

Документация и первоисточники

Если хочется проверить детали по официальным материалам Docker, полезно смотреть сюда:

Обсуждение в Telegram

Присоединиться →

Комментарии