Короткий ответ
Named volume и bind сохраняют числовые UID/GID. Официальный Postgres в Debian-образе часто 999, nginx 101/33, Node-образы — 1000 или node. Если в compose стоит user: "1000:1000", а файлы в app_data принадлежат 999, получите EACCES. Лечение: согласовать user: / USER образа с chown на томе, либо наоборот. Не chmod -R 777, не privileged, не root «навсегда» (контейнер от root).
Сначала убедитесь, что том вообще смонтирован: volume не монтируется.
Симптомы и как отличить
- лог:
Permission denied: '/var/lib/postgresql/data'илиEACCES: permission denied, open '/var/lib/app/data.db'; - контейнер сразу Exited с кодом 1;
lsиз контейнера видит файлы,touchнет;- bind с хоста: файлы
admin:admin700, контейнер uid 999.
| ls на томе | Процесс | Действие |
|---|---|---|
| root:root 755, пусто | non-root | chown под гостя |
| 999:999 | user 1000 | выровнять user |
| 777 | любой | откатить, это не фикс |
Возможные причины
user:в compose скопировали с Node на Postgres.- Том создался от root при первом запуске без USER, потом добавили non-root.
- Bind
/opt/app/dataпринадлежитadmin1000, гость 33 (www-data). read_only: trueбез tmpfs — выглядит как права.- NFS root_squash: uid 0 внутри становится
nobody. - userns-remap на демоне (редко на Ubuntu по умолчанию) сдвигает все UID.
- Редко: immutable attr
chattr +iна хосте.
Диагностика
Контейнер app-web-1, volume app_data, путь внутри /var/lib/app.
1. Кто процесс и что на файле
docker compose -f /opt/app/compose.yaml exec web id
docker compose exec web ls -lan /var/lib/app
docker inspect app-web-1 --format 'user={{.Config.User}} readonly={{.HostConfig.ReadonlyRootfs}}'-n обязателен: имена в контейнере не равны именам хоста.
2. Хостовый inode тома
mp=$(docker volume inspect app_data --format '{{.Mountpoint}}')
echo "$mp"
sudo ls -lan "$mp"Сверьте числа с id гостя.
3. Bind
ls -lan /opt/app/data4. userns
docker info --format '{{.SecurityOptions}}'
grep userns /etc/docker/daemon.json || trueРешение
Сценарий A. Первый запуск, том root:root, нужен uid 999
Окно:
cd /opt/app
docker compose stop web
mp=$(docker volume inspect app_data --format '{{.Mountpoint}}')
sudo chown -R 999:999 "$mp"
docker compose up -d web
docker compose exec web id
docker compose exec web touch /var/lib/app/.write-test && docker compose exec web rm /var/lib/app/.write-testUID возьмите из образа (docker compose run --rm --entrypoint id web), не из форума.
Сценарий B. Выровнять compose user под существующие данные
Данные уже 1000:1000:
services:
web:
user: "1000:1000"Если бинарь образа обязан быть 999 (postgres) — не меняйте user на 1000, меняйте chown на 999. Postgres не стартует от произвольного uid без подготовки.
Сценарий C. Bind и хостовый admin
sudo chown -R 33:33 /opt/app/dataЛибо добавьте на хосте группу и ACL, если нужно писать и admin, и контейнеру. chmod 777 не используйте.
Сценарий D. read_only корень
read_only: true
tmpfs:
- /tmp
- /var/runДанные — только на app_data. EACCES на /tmp без tmpfs — не UID тома.
Сценарий E. Миграция с root-процесса на non-root
- Бэкап volume.
- stop.
- chown.
user:в YAML.- start.
- Проверка записи.
Не оставляйте entrypoint chmod -R 777 как «совместимость».
На Engine 27.x с cgroup v2 смена user: не меняет владельца уже лежащих inode. Первый контейнер от root создаёт PGDATA 70 root, второй с user: 999 не стартует. Это не баг overlay: это POSIX. Окно: stop, chown -R 999:999 на Mountpoint, start. Не делайте chown из entrypoint на каждый запуск на терабайтном томе — это часы IO и риск гонки.
userns-remap в daemon.json сдвигает все UID на хосте (например +165536). Тогда ls на Mountpoint показывает 165535+999, а внутри контейнера по-прежнему 999. Если remap включили после создания app_data, получите кашу. Не включайте remap в ночь на проде с живыми томами. Проверка: docker info SecurityOptions содержит name=userns, плюс каталог /var/lib/docker/165536.165536 вместо обычного tree.
Для bind из домашнего каталога admin (uid 1000) и образа nginx (www-data 33) ACL setfacl -m u:33:rwx на хосте иногда удобнее массового chown, если людям нужен git-checkout в тот же путь. На named volume в /var/lib/docker/volumes ACL возможны, но хуже сопровождаются бэкапом. Запрет 777 остаётся: это маскирует ошибку uid и открывает запись всем процессам хоста, у которых есть traverse к Mountpoint (root всегда, плюс ошибочные bind).
Как проверить, что проблема устранена
docker compose exec web id
docker compose exec web sh -c 'touch /var/lib/app/.perm-ok && rm /var/lib/app/.perm-ok && echo write_ok'
docker compose logs --tail 30 webНет EACCES в новых логах, сервис Running. На хосте ls -lan Mountpoint показывает тот же uid, что id гостя.
Если не помогло
operation not permittedна chmod внутри — нет CAP_FOWNER, и не надо: chown с хоста.- Только некоторые файлы 600 root — entrypoint инициализации от root оставил хвост. stop, chown -R.
- Overlay верхний слой, не volume: приложение пишет не в mount. Inspect Mounts vs путь в конфиге.
- NFS squash: uid mapping на NAS, не docker user.
- AppArmor DENIED path — не unix uid, журнал.
Профилактика
- В образе и compose один и тот же числовой UID, задокументированный в README стека.
- init-контейнер с известным chown хуже, чем сразу правильный USER; если init — только на пустой том.
- Бэкап
app_dataдо массового chown. - Запрет 777 в CI grep.
- Non-root как цель hardening, но после согласования тома.
FAQ
Почему 999, а не postgres?
На хосте пользователя postgres может не быть. Числа портативны. Имена в ls с хоста обманывают.
user: "0:0" как быстрый фикс?
Работает и отменяет смысл non-root. Только аварийно, с тикетом на откат.
Можно ли ACL вместо chown?
Да на bind. На named volume в /var/lib/docker ACL возможны, но проще один uid процесса.
Docker Desktop file sharing vs Engine на Ubuntu
Эта статья про Engine на сервере. Права Desktop/WSL — другой UID-маппинг, не копируйте советы.
chown 1000 на postgres data?
Сломаете официальный entrypoint. Для postgres держите uid образа (часто 999).