Learning Platform
Глоссарий Troubleshooting
Урок 10.05 · 24 мин
Средний
dockervolumespermissionsuidentrypoint

Permissions: UID mismatch, chown в entrypoint, типичные ошибки

Если ты хоть раз видел сообщение «data directory "/var/lib/postgresql/data" has wrong ownership» — добро пожаловать в самый частый класс багов volume и bind mount. Корень проблемы — POSIX-права не зависят от Docker, а UID/GID процессов в контейнере не обязаны совпадать с UID хоста. Этот урок — про то, как этот mismatch возникает, как правильные образы его обходят через entrypoint-скрипт, и какие приёмы доступны тебе самому.


Пользователи и группы — UID, GID и две текстовые базы данных chmod: octal и symbolic notation, umask

Чем отличаются bind и volume по правам

Аспектbind mountnamed volume
Owner/перм. при mountНаследуются с host pathСоздаются Docker, обычно root:root
Изменения прав в контейнереМеняют host-файл напрямуюМеняют файл в Docker-managed папке
Готовность к owner-чисткеХост должен подготовить заранееКонтейнер может chown при init
Поведение при пустом mountВидишь содержимое host-папкиПустой при первом mount
Особо: empty volume + образ-contentBind перекрывает то, что было в образеEmpty named volume копирует содержимое из образа на первый mount

Последний пункт критичен и часто удивляет. Если ты делаешь:

docker run -v pgdata:/var/lib/postgresql/data postgres:17

— и pgdata пустой и новый, Docker сначала скопирует содержимое директории /var/lib/postgresql/data из образа в volume. У Postgres-образа эта папка обычно пустая, но у других — нет. Например, у Airflow в /opt/airflow лежат файлы из образа. Если ты примонтируешь туда named volume — на первый mount туда скопируется содержимое.

С bind такого нет: bind просто перекрывает, host-папка показывается «как есть».

Empty named volume vs bind mount при первом mount
Образ содержит /opt/data/app.confВ Dockerfile: COPY app.conf /opt/data/app.conf. При сборке файл попадает в layer образа
docker run -v vol:/opt/dataVolume пустой и новый. На первый mount Docker копирует содержимое /opt/data из образа в volume — теперь app.conf лежит в volume
docker run -v /host/data:/opt/dataHost-папка содержит свои файлы (или пустая). Bind просто перекрывает /opt/data — файл app.conf из образа становится недоступен
Содержимое = hostВ контейнере видны только файлы host-папки. app.conf из образа скрыт под mount-перекрытием

UID mismatch — анатомия

Сценарий:

# На Linux я — UID 1000.
mkdir -p ./pgdata-bind
docker run -d --name pg \
  -e POSTGRES_PASSWORD=secret \
  -v $(pwd)/pgdata-bind:/var/lib/postgresql/data \
  postgres:17

docker logs pg
# initdb: error: could not change permissions of directory "/var/lib/postgresql/data"
# ... data directory has wrong ownership

Что произошло:

  1. Host-папка pgdata-bind принадлежит мне (UID 1000:GID 1000).
  2. Postgres-образ внутри запускает процесс от пользователя postgres (UID 999).
  3. initdb хочет писать в /var/lib/postgresql/data, но папка — 1000:1000, а процесс — 999:999. Запись отказывает.

С named volume Postgres-образ сам исправляет — на пустой volume entrypoint делает chown postgres:postgres /var/lib/postgresql/data. С bind mount entrypoint не лезет в host-папку, потому что это потенциально опасно (что если ты примонтировал /?).

Решения:

  1. Подготовить host-папку с нужным UID:

    sudo chown -R 999:999 ./pgdata-bind

    Минус: на каждой машине надо помнить, плюс это «магическое число» 999.

  2. Запустить контейнер с тем же UID, что хост-папка:

    docker run -d -u $(id -u):$(id -g) \
      -e POSTGRES_PASSWORD=secret \
      -v $(pwd)/pgdata-bind:/var/lib/postgresql/data \
      postgres:17

    Минус: Postgres ожидает UID postgres, а тут будет другой. Иногда срабатывает, иногда — permission denied на внутренние файлы.

  3. Использовать named volume вместо bind. Самый правильный путь для Postgres data: volume управляется Docker, entrypoint выставит нужный owner.

WARNING

Не пытайся монтировать Postgres data через bind mount на macOS. Помимо UID-проблем там ещё и I/O через VM-FS — fsync медленный, durability гарантии слабее. Для Postgres data — named volume, всегда.


Стандартный паттерн: entrypoint + chown + gosu

Если ты строишь свой образ, в котором нужен mountable data path, классический рецепт:

FROM debian:12-slim

RUN apt-get update && apt-get install -y --no-install-recommends gosu \
  && rm -rf /var/lib/apt/lists/* \
  && groupadd -r app && useradd -r -g app -u 1001 app

RUN mkdir -p /data && chown -R app:app /data
VOLUME /data

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
CMD ["my-application"]

entrypoint.sh:

#!/bin/sh
set -e

# Если /data не принадлежит app — поправим. На bind mount этот chown реально перепишет host-папку.
chown -R app:app /data || true

# drop privileges и запустим основной процесс
exec gosu app "$@"

gosu — это утилита, которая делает setuid + exec в стиле su, но безопаснее и без TTY-шенаниганов su. Если gosu нет, можно su-exec (alpine) или runuser -u app -- "$@" (более новые ядра).

Логика:

  1. Контейнер стартует от root (потому что нужно сделать chown на mount path).
  2. chown поправит owner на mount path — на bind mount это перепишет host UID, на named volume — на volume UID.
  3. gosu app "$@" переключается на app user и стартует основное приложение.

Так делают официальные образы Postgres, Redis, Airflow.


Linux vs macOS поведение

СценарийLinuxmacOS Docker Desktop / OrbStack
Bind mount, файл создан в контейнере под UID 1000На хосте owner = 1000На хосте обычно owner = твой macOS-юзер (UID 501)
Bind mount, host-папка под UID 501, контейнер хочет writeablePermission denied если процесс != 501Обычно works — VM сглаживает
Postgres data в bind mountТребует точного UID 999 на host-папкеЧасто работает «из коробки», но медленно
Named volume под Postgres dataРаботает out-of-boxРаботает out-of-box
TIP

На macOS bind-mount-permissions менее строги — VM-driver сглаживает. На Linux всё буквально. Поэтому код, написанный на маке, может упасть на CI-Linux с UID-ошибкой. Тестируй на Linux прежде, чем считать pipeline production-ready.


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

Ошибка 1: «data directory has wrong ownership»

initdb: error: could not change permissions of directory "/var/lib/postgresql/data" to 0700

Причина: bind mount с UID хоста, Postgres ждёт UID 999.

Лечение: перейти на named volume, либо chown -R 999:999 ./pgdata-bind перед запуском.

Ошибка 2: «permission denied» при попытке записать в /app

PermissionError: [Errno 13] Permission denied: '/app/output.csv'

Причина: контейнер запущен от user app (UID 1001), bind mount имеет host UID 1000, в host-папке нет write permission для UID 1001.

Лечение:

  • быстро: chmod 777 ./output-dir (быстро, но плохо для prod);
  • правильно: chown 1001:1001 ./output-dir;
  • идеально: build-arg UID в образе совпадает с хостом, useradd -u $UID совпадает.

Ошибка 3: named volume содержит мусор после первого старта

ls /opt/airflow внутри контейнера показывает кучу всего из образа

Причина: ты примонтировал named volume на путь, где в образе уже что-то лежало. Docker скопировал содержимое образа в volume при первом mount, оно там осталось.

Лечение: удалить volume (docker volume rm) и пересоздать. Если нужно сохранить часть — выбраться внутрь volume через docker run --rm -v vol:/v alpine sh и удалить нужные файлы.

Ошибка 4: bind mount пустой, ожидался файл из образа

docker run -v $(pwd)/conf:/etc/app -- ну и где /etc/app/default.conf?

Причина: bind mount перекрывает то, что было в образе. Файл default.conf лежал в образе по пути /etc/app/default.conf, но host-папка ./conf пустая — после mount в /etc/app видна только пустая host-папка.

Лечение: сделать COPY на хост перед запуском, либо монтировать конкретный файл, а не папку:

docker run -v $(pwd)/conf/myapp.conf:/etc/app/myapp.conf:ro ...

Попробуй сам

# 1. Воспроизведи UID mismatch на bind mount (на Linux).
mkdir -p /tmp/uid-demo
docker run --rm -u 1500:1500 \
  -v /tmp/uid-demo:/data \
  alpine sh -c 'echo "from 1500" > /data/file.txt; ls -ln /data/'

ls -ln /tmp/uid-demo
# Файл принадлежит UID 1500. Если ты не 1500 на хосте, тронуть его без sudo не сможешь.

# Cleanup (нужен sudo на Linux):
sudo rm -rf /tmp/uid-demo

# 2. Чистый запуск Postgres с named volume — никаких UID-проблем.
docker run -d --name pg-clean \
  -e POSTGRES_PASSWORD=secret \
  -v pg-clean-data:/var/lib/postgresql/data \
  postgres:17

sleep 5
docker logs pg-clean | head -20
# Видишь "database system is ready to accept connections" — никаких ошибок прав.

# Заглянем в volume — кто owner?
docker run --rm -v pg-clean-data:/d alpine ls -ln /d | head -5
# postgres = 999 внутри alpine. Postgres-entrypoint выставил.

docker rm -f pg-clean
docker volume rm pg-clean-data

# 3. Empty named volume копирует содержимое образа.
# Запустим nginx, у которого в /usr/share/nginx/html лежит index.html по умолчанию.
docker run -d --name nginx-vol \
  -v nginx-html:/usr/share/nginx/html \
  -p 8080:80 \
  nginx:1.27-alpine

# Volume теперь содержит default index.html, скопированный из образа.
docker run --rm -v nginx-html:/v alpine ls /v
# 50x.html  index.html

docker rm -f nginx-vol
docker volume rm nginx-html
NOTE

Best practice: для своих образов всегда документируй ожидаемый UID. В README пиши «образ запускается от UID 1001, для bind mount на Linux подготовьте папку через chown 1001 …». Лучше — добавь build-arg для UID, чтобы пользователь мог собрать образ под свой хост.

На этом разбор volumes завершён. В следующем модуле — сети контейнеров: bridge, host, none, overlay; service discovery через DNS; почему Postgres внутри compose доступен как postgres:5432, а не localhost:5432.


Проверка знанийKnowledge check
Команда docker run -v $(pwd)/data:/var/lib/postgresql/data postgres:17 на Linux падает с ошибкой initdb: could not change permissions of directory ... data directory has wrong ownership . Объясни механику ошибки и предложи три различных решения с trade-off'ами.
ОтветAnswer
Механика: host-папка $(pwd)/data принадлежит host-пользователю (UID 1000). Внутри postgres-образа entrypoint запускает initdb от user "postgres" (UID 999). initdb пытается chown/chmod data dir, но UID 1000 — это не он, а bind mount держит host-права буквально. Postgres отказывается работать с data dir, где owner неправильный — это его собственная защита. Три решения: (1) chown -R 999:999 ./data на хосте перед docker run — быстро, но магическое число и нужно повторять на каждой машине; (2) перейти на named volume вместо bind — Docker создаёт volume под root, entrypoint Postgres делает chown на пустом mount при первом запуске; самый чистый вариант для data; (3) собрать кастомный postgres-образ с build-arg UID=1000 и пересоздать user postgres с этим UID — даёт совпадение с хостом, но образ становится "под мою машину" и не portable. Trade-off: (1) — простой, но хрупкий; (2) — production-grade, но bind-удобства live-edit нет; (3) — гибкий, но требует maintain-собственный-образ.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. На Linux команда docker run -v $(pwd)/data:/var/lib/postgresql/data postgres:17 падает с 'data directory has wrong ownership'. В чём корень и какое самое чистое решение?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5