Короткий ответ

Сначала отличите named volume app_data от bind (/opt/app/data:/var/lib/app). Ошибка mount — это source на хосте, тип, readonly, а на хостах с SELinux ещё метки :z/:Z. Ubuntu 22.04/24.04 по умолчанию AppArmor, не SELinux: :z там обычно безвреден, но копипаста из RHEL без понимания путает. Пустой каталог в контейнере часто значит, что Docker создал директорию вместо отсутствующего файла-bind или вы смотрите не тот volume. Не лечите docker volume rm app_data.

Права UID внутри уже смонтированного тома — соседняя статья: права на volume.

Симптомы и как отличить

  • Error response from daemon: invalid mount config for type "bind": bind source path does not exist;
  • контейнер стартует, /var/lib/app пустой, на хосте данные в другом пути;
  • stat /opt/app/nginx.conf на хосте — файл, в контейнере по пути — directory;
  • на Rocky/RHEL: AVC, на Ubuntu этого нет.
Тип в inspectSourceРиск rm
volume/var/lib/docker/volumes/app_data/_dataпотеря данных
bindпуть хоста как естьrm контейнера не удалит хост
tmpfsпамятьнет персиста

Возможные причины

  1. Относительный bind ./data от другого cwd, не от /opt/app.
  2. Файл-источник не существовал — Docker создал каталог.
  3. Путаница volumes: - app_data:/data vs ./app_data:/data (named vs bind).
  4. long syntax type: bind без bind.create_host_path на 27.x при отсутствии пути — ошибка.
  5. SELinux (не Ubuntu) без :z на bind.
  6. NFS-путь хоста не смонтирован, bind на пустой mountpoint.
  7. Редко: consistency/propagation для shared mounts.

Диагностика

1. Что реально смонтировано

cd /opt/app
docker compose config --volumes
docker inspect app-web-1 --format '{{json .Mounts}}' | python3 -m json.tool

Смотрите Type, Source, Destination, RW, Propagation.

2. Source на хосте

ls -ld /opt/app/data /opt/app/nginx.conf /var/lib/docker/volumes/app_data/_data 2>&1
stat /opt/app/nginx.conf 2>&1

Если хостовый nginx.conf — directory, вы уже в ловушке bind-файла. Не копируйте туда бинарник поверх, пока не разберётесь.

3. Имя volume

docker volume ls | grep app
docker volume inspect app_data
docker volume inspect app_app_data

Compose без name: app_data создаёт app_app_data (проект + ключ). YAML ссылается на ключ app_data, API — на полное имя. Путаница «volume не тот».

4. SELinux vs Ubuntu

getenforce 2>/dev/null || echo no_selinux
aa-status 2>/dev/null | head

getenforce нет или Disabled — не вините :z. AVC на Ubuntu не появятся; будет AppArmor DENIED в journal.

Решение

Сценарий A. Bind path does not exist

Создайте правильный тип объекта:

sudo mkdir -p /opt/app/data
# файл, не каталог:
sudo test -f /opt/app/nginx.conf || sudo install -m 644 /dev/null /opt/app/nginx.conf

Потом recreate. Если Docker уже создал директорию вместо файла:

sudo rmdir /opt/app/nginx.conf  # только если пустая директория-ошибка
sudo install -m 644 /opt/app/nginx.conf.real /opt/app/nginx.conf

Не rm -rf каталога с данными.

Сценарий B. Нужен named volume app_data

services:
  web:
    volumes:
      - app_data:/var/lib/app
volumes:
  app_data:
    name: app_data

name: фиксирует имя без префикса проекта — удобно для бэкапа. Создание:

docker compose up -d
docker volume inspect app_data --format '{{.Mountpoint}}'

Пустой новый volume — норма, пока приложение не запишет. Не копируйте в Mountpoint в обход, если контейнер запущен.

Сценарий C. Смотрите не тот каталог

Данные лежат в bind /opt/app/data, а вы inspect named. Выровняйте YAML. Миграция bind→named — копирование в окно, см. резерв volumes.

Сценарий D. SELinux (RHEL-совместимый хост)

Bind:

volumes:
  - /opt/app/data:/var/lib/app:Z

:z — shared label между контейнерами, :Z — private. На Ubuntu это не лечение Permission denied от UID.

Сценарий E. NFS mountpoint пустой

Сначала findmnt /srv/nfs/app, потом bind. Docker смонтирует пустой dir и замаскирует поздний nfs mount (классика). Порядок: сеть/NFS, затем docker.

На Ubuntu 22.04/24.04 bind на NFS-mountpoint, который ещё не смонтировался, даёт «тихий» пустой каталог: Docker не проверяет, что за ФС под путём. После mount -a данные NFS появляются на хосте, но контейнер продолжает видеть тот каталог, который был в момент docker start — это тот же inode mountpoint, уже с содержимым, если mount был поверх. Если же docker стартовал раньше autofs и записал свои файлы в локальный dir, поздний NFS скроет их. Порядок в systemd: RequiresMountsFor= на unit docker или drop-in, который After=remote-fs.target, плюс не класть app_data на автомонтируемый путь без зависимостей.

Относительные bind в compose считаются от файла compose в современных Compose v2, но systemd WorkingDirectory= для обёртки docker compose up всё ещё путает людей: они запускают те же YAML из /root руками и получают другой ./data. Держите абсолютные пути в проде. Проверка: docker inspectSource должен быть каноническим /opt/app/data, не /root/data.

Не копируйте содержимое named volume через cp на живом Postgres. Для миграции bind↔named используйте stop и tar, как в процедуре резервирования. consistency: delegated в YAML на Linux Engine не меняет семантики и не лечит «не монтируется».

Как проверить, что проблема устранена

docker inspect app-web-1 --format '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}} rw={{.RW}}{{"\n"}}{{end}}'
docker compose exec web ls -la /var/lib/app | head
touch /tmp/aaadmin-vol-test
# только если это bind тестовый каталог, не прод-БД:
docker compose exec web test -d /var/lib/app && echo mount_ok

Файл, созданный приложением, виден на Source хоста (для bind) или в volume inspect Mountpoint (для named, осторожно с consistency).

Если не помогло

  • Mount ок, запись нет — UID/GID, не тип volume.
  • read-only file system:ro в YAML или XFS mount usero.
  • Windows-путь в YAML на Linux (C:\) — не тот compose.
  • subpath в Compose: проверьте версию плагина; старый v2 без subpath даст странные ошибки.
  • Контейнер сразу выходит из-за пустого конфига — после фикса mount читайте stderr.

Профилактика

  • В репозитории README: named vs bind, абсолютные пути.
  • name: app_data для продакшен-данных.
  • Запрет относительных bind в systemd WorkingDirectory/opt/app.
  • Чеклист: файл vs директория для конфигов.
  • Бэкап volume до любых down -v.

FAQ

Почему volume пустой после up?

Новый named volume. Старые данные могли быть в другом имени (app_app_data). Inspect ls.

bind быстрее named?

Не аргумент для прода. Named проще бэкапить штатно и переживает смену пути проекта.

Нужен ли :delegated?

Это Docker Desktop/macOS. На Ubuntu Engine 27 игнорируйте копипасту.

Можно ли rm контейнера, том останется?

Да для named. Bind вообще не в Docker lifecycle. down -v — нет, том умрёт.

AppArmor vs :z

Разные MAC. На Ubuntu DENIED смотрите journal AppArmor, не SELinux-метки.