--- title: "Debian 12: настройка Docker-хоста для production" date: 2026-05-22 draft: false description: "Пошаговая настройка Debian 12 как Docker-хоста для production: установка Docker, настройка daemon, UFW + Docker интеграция, изоляция контейнеров, docker.sock, ресурсные ограничения." summary: "Пошаговая настройка Debian 12 как Docker-хоста для production: установка Docker, настройка daemon, UFW + Docker интеграция, изоляция контейнеров, docker.sock, ресурсные ограничения." tags: ["debian-12", "docker", "security", "hardening", "ufw", "production", "containers", "linux"] categories: ["Безопасность"] series: ["Linux Hardening"] series_order: 2 --- # Debian 12: настройка Docker-хоста для production Статья предполагает что базовая настройка уже выполнена: SSH на порту 2222, UFW включён, fail2ban настроен, sysctl применён. Если нет - сначала: {{< article link="/posts/debian-12-base-hardening/">}} Здесь ставим Docker и закрываем типичные дыры: daemon без избыточных привилегий, UFW который не обходится Docker, изоляция контейнеров по сетям. Плюс разбираем docker.sock - самое опасное место на Docker-хосте. ## Исходные данные - Debian 12 с выполненным базовым hardening - Минимум 2GB RAM, 20GB диска Проверь версию: ```bash lsb_release -a ``` ``` Distributor ID: Debian Description: Debian GNU/Linux 12 (bookworm) Release: 12 Codename: bookworm ``` Все команды выполняются с `sudo` если не указано иное. ## Установка Docker ### Удаление старых версий Если был установлен Docker из репозиториев Debian - удали. Версия там устаревшая: ```bash sudo apt remove -y docker docker-engine docker.io containerd runc docker-compose docker-doc podman-docker ``` ### Установка из официального репозитория >Команды установки актуальны на момент написания статьи. Docker меняет процедуру добавления репозитория - перед установкой сверяйся с официальной документацией: docs.docker.com/engine/install/debian ```bash # Установить зависимости sudo apt update sudo apt install -y ca-certificates curl # Создать директорию для ключей sudo install -m 0755 -d /etc/apt/keyrings # Добавить GPG ключ Docker sudo curl -fsSL https://download.docker.com/linux/debian/gpg \ -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc # Добавить репозиторий sudo tee /etc/apt/sources.list.d/docker.sources <Этот раздел - справочный. Конкретные настройки применяются при разворачивании каждого сервиса. Docker создаёт свои iptables правила и **обходит UFW**. Порт опубликованный через `-p 80:80` становится доступен снаружи даже если UFW его не разрешал. Это не баг - это архитектурное решение Docker. Два подхода к решению: ### Подход 1 - рекомендуемый: порты на localhost Публикуй порты только на localhost, снаружи открывай через reverse proxy (nginx, Traefik): ```bash # Плохо - порт открыт на всех интерфейсах, обходит UFW docker run -p 80:80 nginx # Хорошо - порт только на localhost docker run -p 127.0.0.1:80:80 nginx ``` В docker-compose.yml: ```yaml ports: - "127.0.0.1:8080:80" ``` Reverse proxy слушает на `0.0.0.0:443` и проксирует на `127.0.0.1:8080`. UFW разрешает только 443. Docker UFW не обходит - нечего обходить. Для большинства production сетапов достаточно этого подхода - он проще и надёжнее. ### Подход 2: DOCKER-USER chain Docker оставляет пустую цепочку `DOCKER-USER` для пользовательских правил. Правила в ней применяются до правил Docker. Нужен когда по каким-то причинам нельзя использовать reverse proxy: ```bash sudo nano /etc/ufw/after.rules ``` Добавь в конец файла: ``` # BEGIN UFW AND DOCKER *filter :DOCKER-USER - [0:0] # Разрешить трафик из локальных сетей -A DOCKER-USER -s 10.0.0.0/8 -j RETURN -A DOCKER-USER -s 172.16.0.0/12 -j RETURN -A DOCKER-USER -s 192.168.0.0/16 -j RETURN # Применить правила UFW к трафику идущему в контейнеры -A DOCKER-USER -j ufw-user-forward # Блокировать новые соединения снаружи к частным подсетям -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16 -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8 -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12 -A DOCKER-USER -j RETURN COMMIT # END UFW AND DOCKER ``` Перезагрузи UFW: ```bash sudo ufw reload ``` ## Сетевая изоляция контейнеров >Этот раздел - справочный. Сети создаются при разворачивании каждого сервиса. Вместо глобального `icc: false` - изоляция через сети. По умолчанию все контейнеры запущенные без явной сети попадают в `bridge` сеть и видят друг друга. Создавай отдельную сеть для каждого приложения: ```bash # Создать изолированную сеть для приложения docker network create --driver bridge app_network # Контейнеры видят только соседей по той же сети docker run -d --name app --network app_network myapp docker run -d --name db --network app_network postgres ``` Контейнеры из разных сетей не могут общаться - это и есть правильная изоляция без глобального `icc: false`. В docker-compose.yml сети создаются автоматически для каждого стека. Явно задавай сети если нужна дополнительная изоляция внутри стека. ## Безопасность образов ### Сканирование уязвимостей - Trivy ```bash # Добавить репозиторий Trivy wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | \ gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] \ https://aquasecurity.github.io/trivy-repo/deb \ $(lsb_release -sc) main" | \ sudo tee /etc/apt/sources.list.d/trivy.list > /dev/null sudo apt update sudo apt install -y trivy ``` Проверь образ: ```bash trivy image nginx:alpine ``` Вывод: ```bash admin@vps01:~$ trivy image nginx:alpine 2026-06-09T12:22:41+03:00 INFO [vulndb] Need to update DB 2026-06-09T12:22:41+03:00 INFO [vulndb] Downloading vulnerability DB... 2026-06-09T12:22:41+03:00 INFO [vulndb] Downloading artifact... repo="mirror.gcr.io/aquasec/trivy-db:2" 95.58 MiB / 95.58 MiB [---------------------------------------------------------------------] 100.00% 763.08 KiB p/s 2m8s 2026-06-09T12:24:52+03:00 INFO [vulndb] Artifact successfully downloaded repo="mirror.gcr.io/aquasec/trivy-db:2" 2026-06-09T12:24:52+03:00 INFO [vuln] Vulnerability scanning is enabled 2026-06-09T12:24:52+03:00 INFO [secret] Secret scanning is enabled 2026-06-09T12:24:52+03:00 INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning 2026-06-09T12:24:52+03:00 INFO [secret] Please see https://trivy.dev/docs/v0.71/guide/scanner/secret#recommendation for faster secret detection 2026-06-09T12:25:11+03:00 INFO Detected OS family="alpine" version="3.23.4" 2026-06-09T12:25:11+03:00 INFO [alpine] Detecting vulnerabilities... os_version="3.23" repository="3.23" pkg_num=71 2026-06-09T12:25:11+03:00 INFO Number of language-specific files num=0 Report Summary ┌──────────────────────────────┬────────┬─────────────────┬─────────┐ │ Target │ Type │ Vulnerabilities │ Secrets │ ├──────────────────────────────┼────────┼─────────────────┼─────────┤ │ nginx:alpine (alpine 3.23.4) │ alpine │ 1 │ - │ └──────────────────────────────┴────────┴─────────────────┴─────────┘ Legend: - '-': Not scanned - '0': Clean (no security findings detected) nginx:alpine (alpine 3.23.4) Total: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 1, CRITICAL: 0) ┌─────────┬───────────────┬──────────┬────────┬───────────────────┬───────────────┬─────────────────────────────────────────────────┐ │ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ ├─────────┼───────────────┼──────────┼────────┼───────────────────┼───────────────┼─────────────────────────────────────────────────┤ │ libxml2 │ CVE-2026-6732 │ HIGH │ fixed │ 2.13.9-r0 │ 2.13.9-r1 │ libxml2: libxml2: Denial of Service via crafted │ │ │ │ │ │ │ │ XSD-validated document │ │ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-6732 │ └─────────┴───────────────┴──────────┴────────┴───────────────────┴───────────────┴─────────────────────────────────────────────────┘ ``` >Status `fixed` - исправление уже есть, обнови образ >Status `affected` - исправления нет, принимай решение осознанно Правило для production: образы с CRITICAL уязвимостями не идут в production. HIGH - анализировать и принимать решение. Сканировать перед каждым деплоем: ```bash # Выйти с ненулевым кодом если есть CRITICAL/HIGH - удобно для CI trivy image --exit-code 1 --severity CRITICAL,HIGH nginx:alpine ``` ### Аудит событий Docker Auditd фиксирует все обращения к Docker на уровне ядра: запуск контейнеров, изменения конфигурации, доступ к docker.sock. Когда что-то пойдёт не так - это первое место где искать что произошло и когда. ```bash sudo apt install -y auditd ``` Создай правила: ```bash sudo nano /etc/audit/rules.d/docker.rules ``` ``` -w /usr/bin/docker -p rwxa -k docker -w /var/lib/docker -p rwxa -k docker -w /etc/docker -p rwxa -k docker -w /var/run/docker.sock -p rwxa -k docker ``` ```bash sudo systemctl enable auditd sudo systemctl restart auditd ``` Просматривай события: ```bash sudo ausearch -k docker | tail -20 ``` ### Правила выбора образов >Этот раздел - справочный. Применяй при выборе базового образа для своих Dockerfile. Alpine-based или distroless - меньше attack surface: ```dockerfile # 200MB лишних пакетов и уязвимостей FROM ubuntu:24.04 # 5MB, минимум уязвимостей FROM alpine:3.20 # Ещё лучше - distroless (нет shell, нет пакетного менеджера) FROM gcr.io/distroless/static-debian12 ``` Всегда указывай конкретный тег - никогда `latest`: ```bash # Плохо - непредсказуемо, сломается при следующем релизе образа docker pull nginx:latest # Хорошо - предсказуемо docker pull nginx:1.27-alpine ``` ## Изоляция контейнеров >Этот раздел - справочный. ### Capabilities Контейнер по умолчанию имеет набор Linux capabilities которые ему не нужны. Убирай всё лишнее: ```bash # Убрать все capabilities, добавить только нужные docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx ``` Минимальный набор для большинства веб-сервисов: `NET_BIND_SERVICE` (порты < 1024). Если сервис не биндит привилегированные порты - вообще ничего добавлять не нужно, только `--cap-drop=ALL`. ### Read-only filesystem ```bash # Контейнер не может писать в FS кроме явно указанных точек docker run --read-only --tmpfs /tmp --tmpfs /var/run nginx ``` В docker-compose.yml: ```yaml read_only: true tmpfs: - /tmp - /var/run ``` Не все образы поддерживают read-only без дополнительной настройки - смотри документацию образа. ### Seccomp Docker применяет seccomp профиль по умолчанию - блокирует ~44 опасных syscall. Проверь что профиль применяется: ```bash docker info | grep seccomp # Security Options: ... seccomp ``` ### Не запускай контейнеры с --privileged ```bash # НИКОГДА в production docker run --privileged nginx ``` `--privileged` даёт контейнеру доступ ко всем устройствам хоста и отключает все механизмы изоляции. Если видишь `--privileged` в чужом docker-compose - это красный флаг. ## Ресурсные ограничения >Этот раздел - справочный. Без ограничений один контейнер может съесть всю память или CPU хоста и положить остальные сервисы. >OOM (Out Of Memory) killer - механизм ядра Linux: когда процесс превышает лимит памяти, ядро принудительно его завершает. Код завершения контейнера 137 (128 + сигнал 9) означает что сработал OOM killer, а не штатное завершение. ```bash # Максимум 512MB RAM - если превысит, OOM (Out Of Memory) killer завершит контейнер docker run -m 512m --memory-reservation 256m nginx # Максимум 0.5 ядра docker run --cpus="0.5" nginx # Защита от fork bomb - контейнер не создает бесконечное количество процессов docker run --pids-limit 200 nginx ``` Если контейнер неожиданно останавливается - первым делом проверяй OOM (Out Of Memory): ```bash dmesg | grep -i "oom\|killed process" docker inspect CONTAINER --format='{{.State.OOMKilled}}' ``` ## Docker Compose с настройками безопасности >Этот раздел - справочный. Шаблон `docker-compose.yml` для production: сети, лимиты, изоляция: ```yaml services: web: image: nginx:1.27-alpine container_name: web restart: unless-stopped # Порт только на localhost - reverse proxy снаружи ports: - "127.0.0.1:8080:80" # Ресурсные ограничения deploy: resources: limits: cpus: "0.5" memory: 256M reservations: memory: 128M # Изоляция cap_drop: - ALL cap_add: - NET_BIND_SERVICE read_only: true tmpfs: - /tmp - /var/cache/nginx - /var/run security_opt: - no-new-privileges:true pids_limit: 100 logging: driver: "json-file" options: max-size: "50m" max-file: "3" networks: - frontend db: image: postgres:16-alpine container_name: db restart: unless-stopped environment: POSTGRES_PASSWORD_FILE: /run/secrets/db_password volumes: - db_data:/var/lib/postgresql/data deploy: resources: limits: cpus: "1.0" memory: 512M cap_drop: - ALL cap_add: - SETUID - SETGID - DAC_OVERRIDE - CHOWN security_opt: - no-new-privileges:true pids_limit: 200 logging: driver: "json-file" options: max-size: "50m" max-file: "3" # Только backend сеть - не доступен снаружи networks: - backend app: image: myapp:1.2.3 container_name: app restart: unless-stopped # Подключена к обеим сетям - мост между web и db networks: - frontend - backend depends_on: - db cap_drop: - ALL security_opt: - no-new-privileges:true pids_limit: 100 logging: driver: "json-file" options: max-size: "50m" max-file: "3" networks: frontend: driver: bridge backend: driver: bridge internal: true # backend сеть без доступа к интернету volumes: db_data: secrets: db_password: file: ./secrets/db_password.txt ``` `internal: true` для backend сети - контейнеры в ней не имеют доступа к внешним сетям. База данных не должна ходить в интернет. ## Регулярные задачи >Этот раздел - справочный. ### Проверка контейнеров с docker.sock Проверь запущенные контейнеры у которых docker.sock смонтирован: ```bash # Контейнеры с docker.sock = контейнеры с root-доступом к хосту docker ps --format '{{.Names}}' | xargs -I {} docker inspect {} \ --format '{{.Name}}: {{range .Mounts}}{{if eq .Source "/var/run/docker.sock"}}DOCKER_SOCK_MOUNTED{{end}}{{end}}' \ 2>/dev/null | grep DOCKER_SOCK_MOUNTED ``` Запускай после добавления каждого нового сервиса. Пустой вывод - норма. Никогда не монтируй docker.sock в контейнер без явной необходимости. Если сервис требует docker.sock (Portainer, Watchtower, Traefik с автообнаружением) - осознавай что это сервис с root-доступом к хосту. ### Очистка Docker накапливает мусор: остановленные контейнеры, неиспользуемые образы, dangling volumes: ```bash # Показать что занимает место docker system df # Удалить всё неиспользуемое (остановленные контейнеры, dangling образы, неиспользуемые сети) docker system prune -f # Удалить всё включая неиспользуемые образы docker system prune -a -f # Удалить неиспользуемые volumes (ОСТОРОЖНО - данные удалятся безвозвратно) docker volume prune -f ``` Автоматизируй через cron: ```bash sudo crontab -e ``` ``` # Еженедельная очистка - каждое воскресенье в 4:00 0 4 * * 0 docker system prune -f >> /var/log/docker-prune.log 2>&1 ``` ### Мониторинг ресурсов ```bash # Статистика всех запущенных контейнеров в реальном времени docker stats # Один снимок без потока docker stats --no-stream # Конкретный контейнер docker stats web --no-stream ``` ## Troubleshooting ### Контейнер не видит DNS **Симптом:** `curl: Could not resolve host: example.com` внутри контейнера. **Причина:** DNS не передан контейнеру. **Решение:** Добавь DNS в daemon.json: ```json { "dns": ["8.8.8.8", "8.8.4.4"] } ``` ```bash sudo systemctl restart docker ``` --- ### Docker обходит UFW **Симптом:** Порт опубликованный через `-p 80:80` доступен снаружи несмотря на UFW deny. **Причина:** Docker пишет напрямую в iptables, обходя UFW. **Решение:** Использовать `127.0.0.1:80:80` вместо `80:80` - подход 1 из раздела UFW + Docker выше. --- ### Permission denied на bind mount **Симптом:** Контейнер падает с `Permission denied` при обращении к примонтированной директории. **Причина:** UID процесса в контейнере не совпадает с владельцем файлов на хосте. **Решение:** Проверь под каким UID работает процесс в контейнере: ```bash docker run --rm IMAGE id ``` Установи правильного владельца на хосте: ```bash sudo chown -R UID:GID /path/to/mount ``` --- ### OOM killer убивает контейнер **Симптом:** Контейнер неожиданно останавливается, `docker ps` показывает `Exited (137)`. **Причина:** Превышен memory limit, ядро завершило процесс. **Диагностика:** ```bash # Проверить OOM события dmesg | grep -i "oom\|killed process" # Статус контейнера docker inspect CONTAINER --format='{{.State.OOMKilled}}' ``` **Решение:** Увеличить `memory` limit или оптимизировать приложение. --- ### Диск забит логами контейнеров **Симптом:** `df -h` показывает 100% на `/var/lib/docker`. **Диагностика:** ```bash # Найти самые большие лог файлы sudo find /var/lib/docker/containers/ -name "*.log" -exec du -sh {} \; | sort -rh | head -10 ``` **Экстренное решение:** ```bash # Обрезать лог конкретного контейнера (не удаляет файл) sudo truncate -s 0 /var/lib/docker/containers/CONTAINER_ID/CONTAINER_ID-json.log ``` **Постоянное решение:** `max-size` и `max-file` в daemon.json - уже настроено выше. ## Финальная проверка ```bash # Статус daemon sudo systemctl status docker # Конфигурация docker info | grep -E "Storage Driver|Logging Driver|Live Restore|no-new-privileges" # Открытые порты ss -tulnp | grep docker # Размер данных Docker docker system df # Контейнеры с docker.sock docker ps -q | xargs docker inspect --format '{{.Name}}: {{range .Mounts}}{{if eq .Source "/var/run/docker.sock"}}SOCK{{end}}{{end}}' 2>/dev/null | grep SOCK ``` ## Что дальше Docker-хост готов к production нагрузке. Следующий шаг - разворачиваем сервисы поверх этой базы. - Peertube на VPS - следующая статья серии: собственный видеохостинг на этом Docker-хосте - Reverse proxy - Traefik или nginx перед контейнерами - Мониторинг - Prometheus + cAdvisor + Grafana - Централизованные логи - Loki + Promtail **Стек этой статьи:** Debian 12 (Bookworm) · Docker CE · UFW · auditd · Trivy · docker-compose