
Docker без root внутри контейнера: USER, capabilities и read-only
Запускаем приложение непривилегированным пользователем, убираем capabilities, запрещаем повышение прав и проверяем запись только в разрешённые каталоги.
Содержание
Пользователь root внутри контейнера не равен root на хосте, но обычному веб-приложению его полномочия всё равно не нужны. Пространства имён отделяют видимые процессы и сеть, seccomp запрещает часть системных вызовов, а capabilities делят полномочия root на отдельные разрешения. Ошибка в приложении опаснее, если процесс может менять системные файлы, управлять сетью или записывать в широкий каталог хоста.
Какие ограничения нужны обычному приложению
Создайте в образе отдельного пользователя с постоянными UID и GID — числовыми номерами пользователя и группы, по которым Linux проверяет права к файлам. Передайте ему только нужные файлы и задайте USER. При запуске запретите запись в корневую файловую систему, оставьте отдельный каталог для данных, уберите лишние capabilities и запретите повышение прав. Затем проверьте фактический UID и допустимые операции записи.
Не путайте пользователя приложения и режим Docker
Процесс приложения без root
Инструкция Dockerfile USER или параметр Compose user выбирает UID/GID основного процесса. Docker daemon при обычной установке всё ещё работает с полномочиями root на host.
Переназначение пользовательских номеров
UID контейнера сопоставляется с другим непривилегированным диапазоном на хосте. Служба Docker остаётся root, но номера владельцев процессов и файлов получают дополнительное преобразование.
Docker без root на хосте
И служба Docker, и контейнеры запускаются без root внутри отдельного пространства пользовательских номеров. Это самостоятельный вариант установки со своими требованиями и ограничениями. Его нельзя заменить одной строкой USER, но он также не отменяет отдельного пользователя приложения.
В этой статье настраивается первый уровень: он переносим между обычным и rootless Docker и даёт понятный результат для каждого образа.
Создайте пользователя в Dockerfile
Создайте Dockerfile в каталоге приложения. Пример использует Alpine и добавляет группу и пользователя с номером 10001; затем копирует файлы с тем же владельцем и переключает основной процесс на него:
FROM node:24-alpine
RUN addgroup -S -g 10001 app \
&& adduser -S -D -H -u 10001 -G app app
WORKDIR /app
COPY --chown=10001:10001 package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --chown=10001:10001 server.js ./
USER 10001:10001
EXPOSE 3000
CMD ["node", "server.js"]
Постоянный числовой UID упрощает согласование прав volumes между версиями образа. COPY --chown не оставляет код принадлежащим root, однако приложению вовсе не обязательно иметь право менять собственный код. В более строгом варианте код остаётся root-owned и доступен пользователю только на чтение, а запись разрешена отдельному каталогу данных.
Не выполняйте package manager во время старта контейнера. Установка зависимостей требует дополнительных прав и сети; её место в контролируемой сборке образа.
Проверьте пользователя образа
docker build -t example-app:nonroot .
docker image inspect example-app:nonroot \
--format 'configured-user={{.Config.User}}'
docker run --rm example-app:nonroot id
Ожидается UID и GID 10001, а не 0. Затем запустите приложение на локальном порту:
docker run -d --name nonroot-demo \
-p 127.0.0.1:3000:3000 \
example-app:nonroot
docker exec nonroot-demo id
docker top nonroot-demo -eo pid,user,group,args
Публикуйте высокий внутренний порт, чтобы приложению не требовалась способность bind к привилегированному порту. Внешние 80/443 может принимать reverse proxy.
Ограничьте runtime через Compose
services:
web:
image: example-app:nonroot
user: "10001:10001"
read_only: true
tmpfs:
- /tmp:size=64m,mode=1777
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
ports:
- "127.0.0.1:3000:3000"
user дублирует защитное ожидание runtime и не даёт случайно запустить процесс как root из-за изменения образа. read_only запрещает запись в root filesystem; /tmp предоставлен отдельно. Его размер учитывается в memory limit контейнера, поэтому не делайте tmpfs безразмерным.
cap_drop: ALL убирает Linux capabilities. Если приложение действительно требует одну из них, сначала подтвердите отказ на тесте, затем добавьте только конкретную capability через cap_add. Не возвращайте ALL и не используйте privileged: true.
no-new-privileges запрещает процессу получить дополнительные права через setuid/setgid binary и сходные механизмы. Это дополнительный барьер, а не замена исправным permissions и seccomp/AppArmor.
Подготовьте каталог данных
Для bind mount создайте точный каталог на host с ожидаемым владельцем:
sudo install -d -o 10001 -g 10001 -m 0750 /srv/example/data
Compose long syntax явно показывает границу записи:
services:
web:
volumes:
- type: bind
source: /srv/example/data
target: /app/data
Не меняйте владельца всего /srv, /var/lib/docker или домашнего каталога. Если образ использует named volume, изучите его штатную процедуру начальной инициализации: некоторые образы кратко запускаются как root, готовят каталог и затем сбрасывают права. Самодельный entrypoint должен завершаться ошибкой, если подготовка не удалась, и передавать сигналы основному процессу через exec.
Проверьте запреты, а не только успешный запуск
docker compose config
docker compose up -d --force-recreate web
docker compose exec web id
docker compose exec web sh -c 'touch /app/should-fail'
docker compose exec web sh -c 'touch /tmp/should-work && rm /tmp/should-work'
docker compose exec web sh -c 'touch /app/data/should-work && rm /app/data/should-work'
Первая попытка записи должна завершиться ошибкой из-за read-only filesystem. /tmp и каталог данных должны принимать запись. Если приложение не содержит shell, выполните проверки отдельным тестовым target образа или через его встроенную диагностическую команду; не добавляйте shell в production только ради exec.
Посмотрите применённые настройки:
docker inspect "$(docker compose ps -q web)" --format \
'user={{.Config.User}} readonly={{.HostConfig.ReadonlyRootfs}} caps_drop={{json .HostConfig.CapDrop}} security={{json .HostConfig.SecurityOpt}}'
Не выдавайте доступ к Docker socket
Mount /var/run/docker.sock позволяет клиенту управлять Docker API: запускать привилегированные контейнеры, подключать host paths и читать конфигурацию других экземпляров. Для обычного приложения это полномочия уровня управления узлом, даже если его процесс имеет UID 10001.
Если инструменту действительно требуется Docker API, вынесите его в отдельный доверенный компонент, ограничьте API-поверхность и оцените альтернативу без socket. Не подключайте socket «для удобства» сборки или просмотра соседних контейнеров.
Частые ошибки
USER appзадан до установки пакетов, а следующие шаги сборки требуют root;- UID меняется между версиями и перестаёт совпадать с владельцем данных;
read_onlyвключён без отдельного/tmpили каталога runtime-файлов;- приложение слушает порт ниже 1024 и ради этого получает лишние права;
chmod -R 777скрывает неверного владельца;privileged: trueиспользуется как средство диагностики;- rootless daemon ошибочно считают заменой
USERвнутри образа; - Docker socket подключён процессу, который принимает внешние запросы.
Как откатывать ограничения
Если приложение не запускается, сохраните docker compose logs, inspect и точный отказ доступа. Снимайте ограничения по одному на тестовом экземпляре: сначала предоставьте нужный writable path, затем проверьте владельца и только после этого оценивайте capability.
Верните предыдущий проверенный образ или Compose-файл и пересоздайте сервис. Не оставляйте временный user: root, privileged или широкий writable mount после диагностики.
Основы безопасного Dockerfile описаны в материале о сборке образа. Права постоянных каталогов разобраны в статье о volumes и bind mounts, а границы контейнерной изоляции — в объяснении модели Docker.
Источники
Самопроверка
Проверьте, что материал усвоен
Ответьте на все вопросы. Результат сохранится только в этом браузере и будет учтён в статистике прочитанных материалов.
Разбираем коротко
Частые вопросы
Достаточно ли инструкции USER для полной безопасности контейнера?
Нет. USER уменьшает права процесса, но остаются mounts, capabilities, kernel, network и доступные секреты. Нужны дополнительные ограничения и обновляемый доверенный образ.
Одинаковы ли non-root процесс и rootless Docker?
Нет. USER или параметр user меняет учётную запись процесса внутри контейнера. Rootless mode запускает без root ещё и Docker daemon в пользовательском namespace.
Почему нельзя монтировать docker.sock в обычное приложение?
Доступ к Docker API позволяет управлять контейнерами и mounts host и фактически даёт очень широкие полномочия на узле. Такой socket нельзя считать обычным файлом интеграции.


