Установка проекта с помощью Docker

Пошаговая инструкция по безопасному развертыванию Docker‑проекта на продакшн‑сервере с использованием Rootless Docker.

Цель — обеспечить полную изоляцию: даже если Docker‑пользователь или контейнер будут скомпрометированы, атакующий не получит root‑доступ к хосту и не сможет воздействовать на другие проекты.

Предполагается, что базовая настройка сервера уже выполнена по инструкции по безопасности. У вас есть именной sudo‑пользователь для SSH и вы подключены к серверу.


Почему Rootless Docker

Классический Docker‑демон работает от root. Любой, кто может отправлять ему команды (через группу docker или через sudo docker), фактически имеет полный root‑доступ к хосту:

# Через группу docker — root на хосте: docker run -v /:/host -it alpine chroot /host # Через sudo docker — тот же результат: sudo docker run -v /:/host -it alpine chroot /host

Ни usermod -aG docker, ни sudoers с ограничением по пути к бинарнику не решают эту проблему — команда docker сама по себе является инструментом с root‑возможностями.

Rootless Docker запускает демон от обычного пользователя. Все контейнеры работают в пользовательском namespace. Даже root внутри контейнера маппится на непривилегированный UID на хосте. Побег из контейнера даёт максимум права сервисного пользователя — не root.


Шаг 1. Подготовка хоста

Все команды в этом шаге выполняются от вашего sudo‑пользователя.

1.1. Требования

  • Ubuntu 22.04+ (cgroups v2 включены по умолчанию)

Проверяем cgroups v2:

stat -fc %T /sys/fs/cgroup # Должно вернуть: cgroup2fs

1.2. Установка зависимостей

sudo apt-get update sudo apt-get install -y uidmap dbus-user-session systemd-container
  • uidmap — утилиты newuidmap/newgidmap для user namespaces.

  • dbus-user-session — D-Bus сессия для пользовательских systemd‑сервисов.

  • systemd-container — утилита machinectl для корректного входа в сессию сервисного пользователя.


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

Docker нужно ставить из официального репозитория Docker, а не из пакетов Ubuntu (docker.io). Есть два способа.

Способ A — через apt (рекомендуется для продакшна)

# Удаляем старые/конфликтующие пакеты sudo apt-get remove -y docker.io docker-compose docker-compose-v2 \ docker-doc podman-docker containerd runc 2>/dev/null || true # Добавляем GPG‑ключ Docker sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc # Добавляем репозиторий echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \ https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # Устанавливаем Docker Engine + rootless‑компоненты sudo apt-get update sudo apt-get install -y \ docker-ce \ docker-ce-cli \ containerd.io \ docker-buildx-plugin \ docker-compose-plugin \ docker-ce-rootless-extras

Способ B — через convenience‑скрипт

curl -fsSL https://get.docker.com -o get-docker.sh sudo bash get-docker.sh sudo apt-get install -y docker-ce-rootless-extras

Convenience‑скрипт удобен для быстрого старта, но Docker не рекомендует его для продакшна. Способ A даёт больше контроля.

2.1. Отключаем системный (rootful) Docker‑демон

Нам нужны только бинарники Docker, а не системный демон. Отключаем его:

sudo systemctl disable --now docker.service docker.socket

Шаг 3. Создание сервисного пользователя

В примерах ниже используется имя superbot — это имя взято для примера. Замените его на реальное имя вашего проекта.

Каждый проект получает своего системного пользователя. Это обеспечивает изоляцию: каждый пользователь запускает свой собственный экземпляр Docker‑демона в своём user namespace.

sudo adduser --disabled-password --gecos "" superbot

Запрещаем интерактивный вход и SSH:

sudo usermod -s /usr/sbin/nologin superbot

Принципы:

  • пользователь не имеет пароля;

  • пользователь не использует SSH напрямую;

  • пользователь не имеет sudo;

  • пользователь не добавлен в группу docker.

3.1. Проверяем subordinate UID/GID

При создании через adduser диапазоны обычно назначаются автоматически:

grep superbot /etc/subuid # superbot:100000:65536 grep superbot /etc/subgid # superbot:100000:65536

Если записей нет — добавляем вручную (диапазоны не должны пересекаться между пользователями):

sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 superbot

3.2. Включаем linger

Чтобы пользовательские systemd‑сервисы работали без активной сессии (Docker‑демон не останавливался при выходе):

sudo loginctl enable-linger superbot

Шаг 4. Настройка Rootless Docker для пользователя

4.1. Переключаемся в сервисного пользователя

Для корректной работы systemd и XDG_RUNTIME_DIR необходимо использовать machinectl:

sudo machinectl shell superbot@.host /bin/bash

Важно: мы указываем .host (локальная машина) и явно /bin/bash, потому что login shell пользователя — /usr/sbin/nologin, и без этого machinectl откажет в доступе.

Почему **machinectl**, а не **sudo -u**? Команда machinectl shell создаёт полноценную systemd‑сессию пользователя с правильным XDG_RUNTIME_DIR. Без этого systemctl --user и Docker‑демон не будут работать. Команда sudo -u superbot -i bash не работает с nologin и не создаёт systemd‑сессию, поэтому мы используем machinectl везде — и для настройки, и для повседневной работы.

4.2. Запускаем установку rootless‑режима

Находясь в сессии пользователя superbot:

dockerd-rootless-setuptool.sh install

Скрипт создаст systemd user service. Активируем автозапуск:

systemctl --user enable docker

4.3. Проверяем

docker info 2>/dev/null | grep -i rootless # rootless: true docker run --rm hello-world

Если всё работает — выходим из сессии:

exit

Шаг 5. Клонирование проекта

Переключаемся в сервисного пользователя:

sudo machinectl shell superbot@.host /bin/bash

5.1. Генерируем SSH‑ключ для доступа к репозиторию

ssh-keygen -t ed25519 -C "superbot@$(hostname)" -N "" cat ~/.ssh/id_ed25519.pub

Скопируйте публичный ключ и добавьте его в репозиторий как Deploy Key:

  • GitHub: Settings → Deploy keys → Add deploy key

  • GitLab: Settings → Repository → Deploy keys

Достаточно доступа read-only. Deploy Key привязан к одному репозиторию, а не ко всему аккаунту — если сервисный пользователь будет скомпрометирован, атакующий получит доступ только к этому репо.

5.2. Клонируем проект

git clone git@github.com:your-org/your-project.git ~/project cd ~/project

5.3. Создание файла окружения .env

cp .env.example .env chmod 600 .env

Файл .env часто содержит токены, ключи и секреты — он должен быть доступен только владельцу.


Шаг 6. Запуск проекта

Если вы вышли из сессии — входим снова:

sudo machinectl shell superbot@.host /bin/bash

Запускаем:

cd ~/project docker compose up -d

Никакого **sudo** перед docker. Демон работает от самого пользователя.

Остановка:

docker compose down

Пересборка:

docker compose down docker compose up -d --build

Логи:

docker compose logs -f

Шаг 7. Настройка docker-compose.yml для продакшна

7.1. Привязка портов к localhost

Не открывайте контейнеры наружу напрямую:

# Неправильно: ports: - "8000:8000" # Правильно: ports: - "127.0.0.1:8000:8000"

Внешний доступ — только через reverse‑proxy (Caddy, nginx).

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

Без ограничений один скомпрометированный контейнер может положить хост через DoS:

services: app: deploy: resources: limits: memory: 512M cpus: "1.0"

7.3. Hardening контейнеров

services: app: security_opt: - no-new-privileges:true read_only: true tmpfs: - /tmp - /var/tmp
  • no-new-privileges — запрещает suid/sgid внутри контейнера.

  • read_only — файловая система контейнера только для чтения.

  • tmpfs — явно указанные точки для записи.

7.4. Ротация логов

services: app: logging: driver: json-file options: max-size: "10m" max-file: "3"

7.5. Полный пример docker-compose.yml

services: app: build: ./src ports: - "127.0.0.1:8000:8000" env_file: - .env security_opt: - no-new-privileges:true read_only: true tmpfs: - /tmp deploy: resources: limits: memory: 512M cpus: "1.0" logging: driver: json-file options: max-size: "10m" max-file: "3" restart: unless-stopped

Особенности Rootless‑режима

Привилегированные порты

В rootless‑режиме контейнеры не могут привязываться к портам ниже 1024.

Способ 1 (рекомендуемый): Обратный прокси (Caddy, nginx) на хосте слушает 80/443 и проксирует на высокий порт контейнера.

Способ 2: Понизить границу привилегированных портов:

sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80 # Для постоянного применения: echo "net.ipv4.ip_unprivileged_port_start=80" | sudo tee /etc/sysctl.d/99-unprivileged-ports.conf

Хранилище

Образы и данные хранятся в ~/.local/share/docker/ и не шарятся между пользователями. Это увеличивает расход диска, но обеспечивает полную изоляцию. Планируйте минимум 20 ГБ свободного места в домашней директории сервисного пользователя.


Несколько проектов на одном сервере

Для каждого нового проекта повторяете шаги 3–6 с другим именем пользователя:

ops (SSH + sudo) — ваш вход на сервер ├── superbot — rootless Docker, проект 1 ├── webapp — rootless Docker, проект 2 └── api — rootless Docker, проект 3

Каждый пользователь запускает свой экземпляр dockerd. Проекты физически не видят контейнеры друг друга — это не просто разные compose‑файлы в одном демоне, а полностью независимые процессы.


Рекомендуемый рабочий флоу

# 1. Заходим на сервер ssh -p 2222 ops@server # 2. Переключаемся в пользователя проекта sudo machinectl shell superbot@.host /bin/bash # 3. Работаем с проектом cd ~/project docker compose up -d docker compose logs -f exit # 4. Для системных задач — root sudo -i

Итог: что даёт эта схема

Угроза

Защита

Побег из контейнера

Атакующий получает UID сервисного пользователя, не root

Компрометация сервисного пользователя

Нет sudo, нет SSH, нет доступа к другим проектам

DoS через контейнер

Ограничения CPU/RAM через cgroups v2

Эскалация привилегий внутри контейнера

no-new-privileges, read_only

Заполнение диска логами

Ротация через logging driver

Доступ к сервису извне

Только через reverse‑proxy, порты на 127.0.0.1