Файл исчез после пересоздания контейнера, база поднялась пустой, а приложение внезапно получило Permission denied. Обычно причина не в Docker «вообще», а в том, что каталог подключили не тем способом или не проверили владельца файлов.
У Compose есть два основных варианта постоянного хранения: именованный volume и bind mount. Они похожи в YAML, но решают разные задачи.
Короткое правило выбора
Именованный volume подходит, когда данными в основном управляет контейнер:
- каталог данных MariaDB или PostgreSQL;
- очередь, кэш с сохранением на диск;
- загруженные файлы приложения, если их не требуется постоянно править с хоста;
- данные Uptime Kuma, Grafana и других готовых сервисов.
Bind mount нужен, когда хост и контейнер должны видеть один и тот же конкретный путь:
- исходный код во время разработки;
- конфигурация Nginx, которую редактируют в репозитории;
- каталог импорта или экспорта;
- сертификат или другой файл, которым управляет хост.
Не выбирайте bind mount только потому, что его содержимое проще увидеть через проводник. Для базы прямое редактирование файлов с хоста не требуется и чаще добавляет проблемы с правами, производительностью и переносом проекта.
Как выглядит именованный volume
services:
db:
image: mariadb:lts
volumes:
- db_data:/var/lib/mysql
volumes:
db_data:
Docker создаёт и обслуживает хранилище db_data. Контейнер можно удалить и собрать заново, а volume останется. Фактическое имя обычно получает префикс проекта, например shop_db_data.
Посмотреть точное имя и точку подключения можно так:
docker compose config --volumes
docker volume ls
docker volume inspect shop_db_data
Путь из Mountpoint — внутренняя реализация Docker. Не стоит править файлы базы там вручную. Для выгрузки MariaDB используйте mariadb-dump, а для копии обычных файлов — временный контейнер или штатный экспорт приложения.
Как выглядит bind mount
services:
web:
image: nginx:1.29-alpine
volumes:
- type: bind
source: ./nginx/default.conf
target: /etc/nginx/conf.d/default.conf
read_only: true
Здесь ./nginx/default.conf существует на хосте рядом с проектом. Изменение файла сразу видно контейнеру. read_only: true запрещает сервису переписывать конфигурацию.
Длинная форма полезнее короткой, когда конфигурацию будут поддерживать несколько человек: в ней явно видны источник, назначение и режим чтения.
Для каталога с исходниками запись обычно нужна:
services:
app:
build: .
volumes:
- type: bind
source: ./src
target: /app/src
Почему «файлы пропали»
Проверьте ситуацию по порядку.
Контейнер писал не в подключённый путь
Приложение сохраняло данные в /app/storage, а volume подключён к /var/lib/app. После пересоздания слой старого контейнера исчез, потому что нужный каталог не был постоянным.
Посмотрите реальные mounts:
docker inspect "$(docker compose ps -q app)" \
--format '{{json .Mounts}}'
Затем уточните путь данных в документации образа и настройках приложения.
Изменилось имя Compose-проекта
Имена volumes зависят от имени проекта. Если каталог переименовали или запустили Compose с другим -p, новый стек может создать пустой volume с другим префиксом.
docker volume ls
docker compose ls
Старый volume, скорее всего, всё ещё существует. Не удаляйте похожие volumes до проверки docker volume inspect.
Если хранилище должно иметь постоянное внешнее имя, объявите это явно:
volumes:
db_data:
name: devconnect_mariadb_data
Такое имя удобно, но становится общей договорённостью: два проекта с одним именем подключат одни и те же данные.
Выполнили down -v
Обычный docker compose down удаляет контейнеры и сети проекта, но сохраняет объявленные именованные volumes. Команда с -v удаляет и их:
docker compose down -v
Поэтому -v подходит для сознательного сброса тестового окружения, а не для рядового обновления. Перед ним полезно вывести volumes через docker compose config --volumes и иметь проверенную копию важных данных.
Как разбирать Permission denied
Не начинайте с chmod -R 777. Он даёт запись всем локальным пользователям и процессам, маскирует причину и оставляет небезопасные права после исправления контейнера.
Сначала узнайте, от какого пользователя работает процесс:
docker compose exec app id
docker compose exec app sh -lc 'id && stat -c "%u:%g %a %n" /app/storage'
На хосте сравните владельца bind mount:
stat -c '%u:%g %a %n' ./storage
Дальше выберите нормальное решение:
- передайте контейнеру ожидаемые UID/GID, если образ это поддерживает;
- один раз назначьте каталог правильному владельцу;
- оставьте конфигурацию
read_only, если приложению не нужна запись;
- для SELinux используйте корректную метку тома (
:z или :Z) для выделенного каталога, а не отключайте SELinux целиком;
- не запускайте приложение от root только ради доступа к файлам.
На Docker Desktop bind mount проходит через виртуальную машину. Производительность большого дерева с тысячами мелких файлов может отличаться от Linux. Зависимости и данные базы обычно разумнее держать в volume, а с хоста подключать только редактируемый код.
Практичная схема для веб-проекта
services:
app:
build: .
volumes:
- type: bind
source: ./src
target: /app/src
- app_cache:/app/var/cache
- uploads:/app/public/uploads
db:
image: mariadb:lts
volumes:
- db_data:/var/lib/mysql
volumes:
app_cache:
uploads:
db_data:
Исходники остаются удобными для разработки. Кэш, загрузки и база не зависят от случайного удаления контейнера. Для production исходники обычно копируют в образ на этапе сборки, а bind mount оставляют только там, где действительно нужен внешний файл.
Официальные источники