Установка проекта с помощью 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