Главная/Подготовка среды/Урок

1.6. Rootless Docker

Цели

После этого материала вы сможете:

  • объяснить, чем rootless mode отличается от обычного и что именно он даёт;
  • подготовить систему: uidmap, диапазоны subuid и subgid;
  • установить и запустить rootless Docker через dockerd-rootless-setuptool.sh;
  • переключаться между системным и rootless daemon через DOCKER_HOST;
  • назвать ограничения rootless mode и оценить, приемлемы ли они для вашей задачи;
  • опубликовать порт ниже 1024 в rootless mode;
  • полностью удалить Docker вместе с данными.

Предварительные знания

Ключевые термины

ТерминОбъяснение
rootless modeРежим, в котором daemon и containers работают от обычного пользователя без прав root
user namespaceNamespace, отображающий UID внутри на другие UID снаружи. Основа rootless mode
subuid / subgidДиапазоны UID и GID, выделенные пользователю для использования в user namespace
newuidmap / newgidmapУтилиты из пакета uidmap, настраивающие отображение UID. Единственные компоненты с повышенными правами
RootlessKitКомпонент, создающий user namespace и пробрасывающий порты в rootless mode
slirp4netns / pastaРеализации сетевого стека в пространстве пользователя
lingeringРежим systemd, при котором пользовательские службы работают без активной сессии

Теория

Что даёт rootless mode

В обычном режиме dockerd работает от root. Любой, кто может отправить ему команду, получает права root на машине — это разбиралось в предыдущем уроке.

В rootless mode daemon работает от вашего пользователя. Внутри container процесс может считать себя root (UID 0), но снаружи, с точки зрения ядра host, это ваш обычный UID или один из выделенных вам subordinate UID.

text
        Обычный режим                       Rootless режим

  ┌─────────────────────────┐        ┌─────────────────────────┐
  │ container: UID 0 (root) │        │ container: UID 0 (root) │
  └───────────┬─────────────┘        └───────────┬─────────────┘
              │                                  │  user namespace
              ▼ тот же UID                       ▼  отображает UID
  ┌─────────────────────────┐        ┌─────────────────────────┐
  │  host: UID 0 (root)     │        │  host: UID 165536       │
  │  полные права           │        │  обычный пользователь   │
  └─────────────────────────┘        └─────────────────────────┘

Практическое следствие: побег из container в rootless mode даёт права обычного пользователя, а не root. Это качественное, а не количественное улучшение.

Как это работает

Ядро Linux позволяет непривилегированному пользователю создать user namespace, где он будет иметь UID 0. Но внутри такого namespace доступен только один UID — его собственный. Для containers нужен диапазон: приложениям требуются разные UID.

Диапазон выделяется заранее и записывается в /etc/subuid и /etc/subgid. Утилиты newuidmap и newgidmap (они установлены с битом setuid) настраивают отображение по этим файлам. Это единственные компоненты схемы, требующие повышенных прав, и они делают ровно одну строго ограниченную операцию.

Дальше RootlessKit создаёт namespaces, а dockerd работает уже внутри них.

Что теряется

Rootless mode — компромисс. Часть возможностей ядра действительно требует root, и обойти это нельзя.

ОграничениеПодробности
Storage driversПоддерживаются overlay2 (ядро 5.11+), fuse-overlayfs (ядро 4.18+), btrfs (ядро 4.18+) и vfs. vfs работает везде, но неэффективен: копирует слои целиком
CgroupsОграничение ресурсов (--cpus, --memory, --pids-limit) работает только на cgroup v2 с systemd. Обычно по умолчанию делегируются только контроллеры memory и pids; для cpu и io нужна дополнительная настройка делегирования
AppArmorНе поддерживается
Checkpoint / restoreНе поддерживается
Overlay networkНе поддерживается (актуально для Swarm)
SCTP-портыПубликация не поддерживается
Порты ниже 1024По умолчанию недоступны; требуется дополнительная настройка (см. ниже)
СетьРаботает в пространстве пользователя через slirp4netns или pasta, что медленнее, чем сетевой стек ядра. По умолчанию исходный IP-адрес клиента не сохраняется
Доступ к устройствам hostОграничен правами вашего пользователя

Когда rootless оправдан

СитуацияОценка
Общий сервер разработки с несколькими пользователямиОправдан. Главный сценарий применения
Выполнение недоверенного кода (CI-раннеры)Оправдан, хотя одного rootless недостаточно — см. раздел 12
Личная рабочая машинаПо желанию. Выигрыш есть, но ограничения могут мешать
Production-сервис с требованиями к производительности сетиОбычно не подходит из-за сетевого стека в пространстве пользователя
Изучение материала этого курсаНе рекомендуется как основной режим: часть упражнений (разделы 07, 08, 17) даёт другой результат

Рекомендация курса. Пройдите курс на обычной установке, а rootless настройте параллельно и попереключайтесь между ними. Оба daemon могут сосуществовать: у них разные сокеты и разные каталоги данных.


Внутренний механизм

Диапазоны subuid и subgid

Файл /etc/subuid содержит строки вида:

text
evg:100000:65536

Читается так: пользователю evg выделены 65 536 подчинённых UID начиная со 100000, то есть диапазон 100000–165535.

Отображение внутри user namespace выглядит следующим образом:

UID в containerUID на host
0 (root)1000 (ваш UID)
1100000
2100001
......
65536165535

Практическое следствие для раздела 07: файл, созданный внутри container от UID 1000, будет принадлежать на host UID 100999. Это меняет всю логику работы с правами доступа при bind mount.

Требование — не менее 65 536 UID и GID на пользователя. Современные дистрибутивы создают эти записи автоматически при добавлении пользователя.

Два сокета

Rootless daemon слушает другой socket:

text
Обычный:   /var/run/docker.sock
Rootless:  /run/user/<UID>/docker.sock

Переменная DOCKER_HOST указывает CLI, к какому daemon подключаться. Это позволяет держать оба и переключаться между ними одной командой.

Каталоги данных тоже разные: rootless использует ~/.local/share/docker. Образы, containers и volumes у двух daemon не общие.

Lingering

По умолчанию systemd завершает пользовательские службы при выходе пользователя из системы. Для сервера это неприемлемо: containers остановятся, как только вы закроете SSH-сессию.

Команда loginctl enable-linger включает режим, при котором пользовательские службы работают независимо от сессий и запускаются при загрузке системы.


Команды и примеры

Шаг 1. Проверка предпосылок

bash
# наличие утилит отображения UID
which newuidmap newgidmap
text
/usr/bin/newuidmap
/usr/bin/newgidmap

Если команд нет:

bash
sudo apt install -y uidmap

Проверка диапазонов:

bash
grep "^$USER:" /etc/subuid /etc/subgid
text
/etc/subuid:evg:100000:65536
/etc/subgid:evg:100000:65536

Требуется не менее 65 536 в каждом файле. Если записей нет, добавьте:

bash
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 "$USER"

Проверка cgroup v2 (нужна для ограничения ресурсов):

bash
stat -fc %T /sys/fs/cgroup/
text
cgroup2fs

Шаг 2. Установка

Если системный Docker установлен и вы хотите использовать только rootless, отключите системный daemon:

bash
sudo systemctl disable --now docker.service docker.socket
sudo rm -f /var/run/docker.sock

Для параллельной работы этот шаг пропускается.

Запуск установщика без sudo:

bash
dockerd-rootless-setuptool.sh install

Что делает: проверяет предпосылки, создаёт пользовательский unit systemd docker.service, запускает его и печатает переменные окружения, которые нужно установить.

Ожидаемый вывод в конце:

text
[INFO] Installed docker.service successfully.
[INFO] To control docker.service, run: `systemctl --user (start|stop|restart) docker.service`
[INFO] To run docker.service on system startup, run: `sudo loginctl enable-linger evg`

[INFO] Make sure the following environment variable(s) are set (or add them to ~/.bashrc):
export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix:///run/user/1000/docker.sock

Если скрипт не найден, установите пакет с дополнительными компонентами:

bash
sudo apt install -y docker-ce-rootless-extras

Шаг 3. Настройка окружения

bash
echo 'export DOCKER_HOST=unix:///run/user/'"$(id -u)"'/docker.sock' >> ~/.bashrc
export DOCKER_HOST="unix:///run/user/$(id -u)/docker.sock"

Для оболочки fish файл конфигурации другой (~/.config/fish/config.fish), синтаксис — set -x DOCKER_HOST ....

Шаг 4. Автозапуск

bash
sudo loginctl enable-linger "$USER"
systemctl --user enable docker

Что делает: первая команда разрешает пользовательским службам работать без активной сессии, вторая включает автозапуск daemon.

Шаг 5. Проверка

bash
docker info --format '{{.SecurityOptions}}'
text
[name=seccomp,profile=builtin name=rootless name=cgroupns]

Наличие name=rootless — подтверждение, что вы работаете с rootless daemon.

Проверка отображения UID:

bash
docker run --rm alpine id
text
uid=0(root) gid=0(root) groups=0(root),1(bin),...

Внутри container — root. А снаружи, на host:

bash
docker run -d --name uidtest alpine sleep 60
ps -o user,pid,cmd -p "$(docker inspect uidtest --format '{{.State.Pid}}')"
text
USER         PID CMD
evg       184213 sleep 60

Процесс принадлежит вашему пользователю, а не root. Это и есть суть rootless mode.

Уборка:

bash
docker rm -f uidtest

Управление службой

bash
systemctl --user status docker
systemctl --user restart docker
systemctl --user stop docker

Обратите внимание на --user: это пользовательский unit, а не системный. sudo systemctl здесь работать не будет.

Логи:

bash
journalctl --user -u docker -n 50

Переключение между daemon

bash
# rootless
export DOCKER_HOST="unix:///run/user/$(id -u)/docker.sock"
docker info --format '{{.Name}} rootless={{.SecurityOptions}}'

# системный
unset DOCKER_HOST
docker info --format '{{.Name}}'

Удобные функции для ~/.bashrc:

bash
docker-rootless() { export DOCKER_HOST="unix:///run/user/$(id -u)/docker.sock"; echo "rootless"; }
docker-system()   { unset DOCKER_HOST; echo "system"; }

Ещё один способ — Docker contexts:

bash
docker context create rootless \
    --docker "host=unix:///run/user/$(id -u)/docker.sock"
docker context use rootless
docker context use default
docker context ls

Contexts предпочтительнее переменной окружения: настройка сохраняется между сессиями и видна в docker context ls.

Публикация портов ниже 1024

По умолчанию в rootless mode нельзя опубликовать порт ниже 1024:

bash
docker run -d -p 80:80 nginx
text
docker: Error response from daemon: driver failed programming external
connectivity on endpoint ...: bind: permission denied

Есть два способа. Оба требуют однократного вмешательства с sudo.

Способ 1: capability для RootlessKit.

bash
sudo setcap cap_net_bind_service=ep "$(which rootlesskit)"
systemctl --user restart docker

Что делает: даёт бинарному файлу rootlesskit право привязываться к привилегированным портам. Ограничено одной программой и одной возможностью.

Способ 2: системная настройка.

bash
echo 'net.ipv4.ip_unprivileged_port_start=0' | \
    sudo tee /etc/sysctl.d/99-rootless.conf
sudo sysctl --system

Что делает: разрешает всем непривилегированным процессам системы привязываться к любым портам. Шире по действию, поэтому первый способ предпочтительнее.

Второй способ снимает системное ограничение целиком. На многопользовательской машине это означает, что любой пользователь сможет занять порт 80 или 443. Оценивайте последствия.

Проверка:

bash
docker run -d --name web -p 80:80 nginx
curl -s -o /dev/null -w '%{http_code}\n' http://localhost/
docker rm -f web
text
200

Ограничение ресурсов

По умолчанию делегируются не все контроллеры cgroup:

bash
cat "/sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/cgroup.controllers"
text
memory pids

Значит --memory и --pids-limit работают, а --cpus — нет. Чтобы делегировать контроллеры cpu и io:

bash
sudo mkdir -p /etc/systemd/system/user@.service.d
sudo tee /etc/systemd/system/user@.service.d/delegate.conf > /dev/null <<'EOF'
[Service]
Delegate=cpu cpuset io memory pids
EOF
sudo systemctl daemon-reload

Затем перезагрузите систему или переоткройте сессию.

Проверка:

bash
docker run --rm --memory=64m --cpus=0.5 alpine \
    sh -c 'cat /sys/fs/cgroup/memory.max'
text
67108864

67 108 864 байт = 64 MiB. Лимит применился.


Полное удаление Docker

Раздел применим и к обычной, и к rootless установке. Выполняйте шаги по порядку.

Шаг 1. Инвентаризация

Перед удалением. Следующие шаги удаляют образы, containers и volumes безвозвратно. Восстановление возможно только из резервных копий или из registry. Сначала посмотрите, что у вас есть.

bash
docker system df
docker volume ls
docker ps -a

Если среди volumes есть данные, которые нужны, сделайте backup — процедура описана в разделе 07. Образы, которые вы не хотите пересобирать, опубликуйте в registry или сохраните:

bash
docker save -o backup-images.tar image1:tag image2:tag

Шаг 2. Удаление rootless-установки

bash
dockerd-rootless-setuptool.sh uninstall
rootlesskit rm -rf ~/.local/share/docker

Первая команда останавливает и удаляет пользовательский unit. Вторая удаляет каталог данных (через rootlesskit, потому что часть файлов принадлежит подчинённым UID и обычным rm не удаляется).

Отключить lingering, если он больше не нужен:

bash
sudo loginctl disable-linger "$USER"

Шаг 3. Остановка системной службы

bash
sudo systemctl stop docker.socket docker.service containerd.service
sudo systemctl disable docker.socket docker.service

Шаг 4. Удаление пакетов

bash
sudo apt purge -y docker-ce docker-ce-cli containerd.io \
                  docker-buildx-plugin docker-compose-plugin \
                  docker-ce-rootless-extras
sudo apt autoremove -y

purge удаляет и конфигурационные файлы пакетов, в отличие от remove.

Шаг 5. Удаление данных

Опасная команда. Удаляет все образы, containers, volumes, сети и build cache без возможности восстановления. Убедитесь, что шаг 1 выполнен.

bash
sudo rm -rf /var/lib/docker
sudo rm -rf /var/lib/containerd
sudo rm -rf /etc/docker
rm -rf ~/.docker

Что где лежит:

ПутьСодержимое
/var/lib/dockerОбразы, containers, volumes, сети, build cache
/var/lib/containerdДанные containerd (при containerd image store — образы)
/etc/dockerdaemon.json, сертификаты
~/.dockerКонфигурация CLI, credentials, contexts

Шаг 6. Удаление репозитория и группы

bash
sudo rm -f /etc/apt/sources.list.d/docker.sources
sudo rm -f /etc/apt/keyrings/docker.asc
sudo apt update

sudo groupdel docker

Шаг 7. Проверка

bash
which docker || echo "docker удалён"
ls /var/lib/docker 2>/dev/null || echo "данные удалены"
getent group docker || echo "группа удалена"

Практическое упражнение

Задание. Настройте rootless Docker параллельно с системным, создайте context для переключения и докажите тремя проверками, что перед вами именно rootless daemon.

Требования к результату:

  1. Оба daemon работают одновременно.
  2. Переключение выполняется одной командой (docker context use).
  3. Доказательства rootless: наличие name=rootless в security options, принадлежность процесса container вашему пользователю, отличие пути к каталогу данных.

Подсказки

Подсказка 1

Установка rootless не требует остановки системного daemon, если вы не собираетесь использовать rootless как единственный. Шаг с systemctl disable --now docker пропускается.

Подсказка 2

PID процесса container извлекается так:

bash
docker inspect <name> --format '{{.State.Pid}}'

Владельца процесса покажет ps -o user= -p <pid>.

Подсказка 3

Каталог данных виден в docker info --format '{{.DockerRootDir}}'. Сравните значения для двух contexts.

Решение

Сначала выполните задание самостоятельно.

Показать решение
bash
#!/usr/bin/env bash
# compare-daemons.sh — сравнение системного и rootless daemon.
set -euo pipefail

ROOTLESS_HOST="unix:///run/user/$(id -u)/docker.sock"

# Создать context, если его ещё нет
if ! docker context inspect rootless > /dev/null 2>&1; then
    docker context create rootless --docker "host=$ROOTLESS_HOST"
fi

show() {
    local ctx="$1"
    echo "=== Context: $ctx ==="
    docker --context "$ctx" info \
        --format '  Security:  {{.SecurityOptions}}'
    docker --context "$ctx" info \
        --format '  Data root: {{.DockerRootDir}}'
    docker --context "$ctx" info \
        --format '  Storage:   {{.Driver}}'

    local name="cmp-$ctx-$$"
    docker --context "$ctx" run -d --name "$name" alpine sleep 30 > /dev/null
    local pid
    pid="$(docker --context "$ctx" inspect "$name" --format '{{.State.Pid}}')"
    printf '  Владелец процесса container: %s (PID %s)\n' \
        "$(ps -o user= -p "$pid" | tr -d ' ')" "$pid"
    docker --context "$ctx" rm -f "$name" > /dev/null
    echo
}

show default
show rootless

Ожидаемый вывод:

text
=== Context: default ===
  Security:  [name=seccomp,profile=builtin name=cgroupns]
  Data root: /var/lib/docker
  Storage:   overlayfs
  Владелец процесса container: root (PID 184001)

=== Context: rootless ===
  Security:  [name=seccomp,profile=builtin name=rootless name=cgroupns]
  Data root: /home/пользователь/.local/share/docker
  Storage:   overlayfs
  Владелец процесса container: evg (PID 184213)

Три различия видны сразу: name=rootless в security options, разные каталоги данных, разные владельцы процесса.

Проверка результата

bash
docker context ls
text
NAME       DESCRIPTION                               DOCKER ENDPOINT
default *  Current DOCKER_HOST based configuration   unix:///var/run/docker.sock
rootless                                             unix:///run/user/1000/docker.sock
bash
docker --context rootless info --format '{{.SecurityOptions}}' | grep -o rootless
text
rootless

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

ОшибкаПричинаИсправление
dockerd-rootless-setuptool.sh: command not foundНе установлен пакет с дополнительными компонентамиsudo apt install docker-ce-rootless-extras
Установщик запущен через sudoСкрипт должен работать от обычного пользователяЗапустить без sudo
could not find newuidmapНе установлен пакет uidmapsudo apt install uidmap
/etc/subuid не содержит записи для пользователяПользователь создан без выделения диапазоновsudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER
Containers останавливаются после выхода из SSHНе включён lingeringsudo loginctl enable-linger $USER
docker подключается к системному daemon вместо rootlessНе установлена DOCKER_HOST или не выбран contextdocker context use rootless
--cpus не работаетКонтроллер cpu не делегирован пользователюНастроить Delegate= в drop-in для user@.service
bind: permission denied для порта 80Порты ниже 1024 недоступны по умолчаниюsetcap на rootlesskit или net.ipv4.ip_unprivileged_port_start=0
Образы «пропали» после перехода на rootlessУ двух daemon разные каталоги данныхПеренести через docker save / docker load
rm -rf ~/.local/share/docker не удаляет часть файловФайлы принадлежат подчинённым UIDrootlesskit rm -rf ~/.local/share/docker

Контрольные вопросы

На понимание:

  1. Почему процесс, считающий себя root внутри container, не имеет прав root на host в rootless mode?
  2. Зачем нужны файлы /etc/subuid и /etc/subgid?
  3. Почему newuidmap установлен с битом setuid, и почему это не сводит на нет всю схему?
  4. Почему сеть в rootless mode медленнее?
  5. Что произойдёт с работающими containers при выходе пользователя из системы без lingering?

На применение:

  1. Как переключиться между системным и rootless daemon, не редактируя файлы конфигурации?
  2. Как опубликовать порт 443 в rootless mode, ограничив выданные права одной программой?
  3. Как проверить, какие cgroup-контроллеры делегированы вашему пользователю?

На диагностику:

  1. docker run --memory=64m в rootless mode выполняется, но лимит не применяется. Где искать причину?
  2. После установки rootless команда docker images показывает пустой список, хотя раньше образы были. Что произошло?

Краткое резюме

  1. В rootless mode daemon и containers работают от обычного пользователя.
  2. Основа механизма — user namespace, отображающий UID container на подчинённые UID host.
  3. Диапазоны задаются в /etc/subuid и /etc/subgid, требуется не менее 65 536 значений.
  4. Установка выполняется через dockerd-rootless-setuptool.sh install без sudo.
  5. Rootless daemon слушает /run/user/<UID>/docker.sock и хранит данные в ~/.local/share/docker.
  6. Оба daemon могут работать параллельно; переключение — через Docker contexts.
  7. loginctl enable-linger нужен, чтобы containers пережили выход из сессии.
  8. Ограничения: AppArmor, checkpoint, overlay network, SCTP-порты не поддерживаются; сеть работает в пространстве пользователя.
  9. Ограничение ресурсов требует cgroup v2 с systemd; по умолчанию делегируются только memory и pids.
  10. Порты ниже 1024 требуют setcap на rootlesskit либо системной настройки ip_unprivileged_port_start.

Официальные источники

ИсточникСсылкаЧто подтверждает
Rootless modehttps://docs.docker.com/engine/security/rootless/Предпосылки, установка через dockerd-rootless-setuptool.sh, DOCKER_HOST, loginctl enable-linger, список известных ограничений, публикация привилегированных портов, требования к cgroup v2
Rootless mode: tipshttps://docs.docker.com/engine/security/rootless/tips/Делегирование cgroup-контроллеров, дополнительные настройки
Rootless mode: troubleshootinghttps://docs.docker.com/engine/security/rootless/troubleshoot/Диагностика типичных проблем установки
Docker contextshttps://docs.docker.com/engine/manage-resources/contexts/Создание и переключение contexts
Isolate containers with a user namespacehttps://docs.docker.com/engine/security/userns-remap/Альтернативный механизм userns-remap
subuid(5) man pagehttps://man7.org/linux/man-pages/man5/subuid.5.htmlФормат файлов подчинённых UID
user_namespaces(7) man pagehttps://man7.org/linux/man-pages/man7/user_namespaces.7.htmlМеханизм user namespaces в ядре

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий материал → Практические задания
Главное оглавление

Markdown на GitHub ↗