22 KiB
title, date, draft, description, summary, tags, categories, series, series_order
| title | date | draft | description | summary | tags | categories | series | series_order | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Ubuntu 24.04: настройка Docker-хоста для production | 2026-07-29 | true | Пошаговая настройка Ubuntu 24.04 LTS как Docker-хоста для production: установка Docker, hardening daemon, UFW + Docker интеграция, systemd-resolved и DNS, изоляция контейнеров, docker.sock. | Пошаговая настройка Ubuntu 24.04 LTS как Docker-хоста для production: установка Docker, hardening daemon, UFW + Docker интеграция, systemd-resolved и DNS, изоляция контейнеров, docker.sock. |
|
|
|
4 |
Ubuntu 24.04: настройка Docker-хоста для production
Статья предполагает что базовый hardening уже сделан по статье: SSH на нестандартном порту, UFW включён, fail2ban настроен. Если нет - сначала туда. {{< article link="/posts/ubuntu-24-base-hardening/">}}
В этой статье - Docker и всё что нужно чтобы контейнеры не стали дырой в безопасности хоста.
Что получишь на выходе:
- Docker установлен из официального репозитория (не snap)
- daemon.json настроен безопасно - без userns-remap и icc:false по умолчанию
- UFW и Docker работают вместе
- systemd-resolved и DNS в контейнерах - рабочее решение
- docker.sock - понимание риска и контроль доступа
- Контейнеры изолированы по сетям, ограничены по ресурсам
- Образы проверяются на уязвимости
Исходные данные
- Ubuntu 24.04 LTS с выполненным базовым hardening
- Минимум 2GB RAM, 20GB диска
- Пользователь с sudo
Проверь версию:
lsb_release -a
Distributor ID: Ubuntu
Description: Ubuntu 24.04.4 LTS
Release: 24.04
Codename: noble
[!info] ВАЖНО! Все команды выполняются с
sudoесли не указано иное. Если работаешь от root -sudoможно опустить.
Установка Docker
[!danger] ОЧЕНЬ ВАЖНО! Не используй snap версию в production!
В Ubuntu можно поставить Docker через snap (sudo snap install docker).
Snap-версия Docker работает в собственном confinement-окружении со своими путями (/var/snap/docker/), хуже интегрируется с системным iptables/UFW, и обновляется по графику snap, а не когда нужно тебе. Используй официальный репозиторий Docker - там стандартный путь /var/lib/docker, предсказуемое поведение, полный контроль над версией.
Проверь что snap-версии нет:
snap list | grep docker
Если есть - удали:
sudo snap remove docker
Удаление старых версий
sudo apt remove -y docker docker-engine docker.io containerd runc
Установка из официального репозитория
Зависимости
sudo apt install -y ca-certificates curl gnupg lsb-release
Создать директорию для ключей
sudo install -m 0755 -d /etc/apt/keyrings
Добавить GPG ключ Docker
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
Добавить репозиторий
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Обновить индекс и установить
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Проверь версию:
docker --version
Ожидаем:
# Docker version 29.x.x, build xxxxxxx
Проверь daemon:
sudo systemctl status docker
Ожидаем:
● docker.service - Docker Application Container Engine
Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; preset: enabled)
Active: active (running) since Wed 2026-07-29 MSK; 3h 38min ago
TriggeredBy: ● docker.socket
Docs: https://docs.docker.com
Main PID: 182611 (dockerd)
Tasks: 8
Memory: 23.4M (peak: 24.0M)
CPU: 2.661s
CGroup: /system.slice/docker.service
└─182611 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
Добавление пользователя в группу docker
sudo usermod -aG docker $USER
Выйди и зайди снова (Выполни newgrp docker для текущей сессии без перелогина). Проверь:
docker run --rm hello-world
docker.sock - главный риск
Прежде чем настраивать daemon - разберёмся с самым опасным местом.
/var/run/docker.sock - Unix сокет Docker daemon. Любой процесс с доступом к нему имеет полный root на хосте. Через один вызов API можно смонтировать корень хоста и получить туда доступ:
# Пример атаки - одна команда и ты root на хосте
docker run --rm -v /:/host alpine chroot /host
Правила работы с docker.sock:
[!info] ВАЖНО Никогда не монтируй docker.sock в контейнер без явной необходимости.
Если сервис требует его (Portainer, Watchtower, Traefik с автообнаружением) - этот сервис имеет root-доступ к хосту.
Проверь права на сокет:
ls -la /var/run/docker.sock
srw-rw---- 1 root docker 0 Jul 29 11:38 /var/run/docker.sock
Проверь кто в группе docker:
getent group docker
Только доверенные пользователи должны быть в этой группе - фактически это равно правам root.
Проверь какие контейнеры монтируют сокет:
docker ps -q | 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
Hardening daemon.json
sudo nano /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
},
"live-restore": true,
"userland-proxy": false,
"no-new-privileges": true,
"storage-driver": "overlay2",
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 65536,
"Soft": 65536
}
}
}
Что и почему:
log-driver + лимиты - без них логи контейнеров забивают /var/lib/docker/containers/ за дни на активном сервисе.
live-restore: true - контейнеры продолжают работать пока daemon перезапускается.
userland-proxy: false - iptables вместо userland proxy, быстрее, меньше процессов.
no-new-privileges: true - контейнеры не поднимают привилегии через setuid/setgid бинарники.
storage-driver: overlay2 - зафиксирован явно, хотя на Ubuntu 24.04 он и так используется по умолчанию.
Почему нет userns-remap
Root в контейнере мапится на непривилегированный UID на хосте (например 100000). Звучит безопасно, но ломает bind mounts: файлы на хосте принадлежат UID 1000, контейнер видит UID 100000 и получает Permission denied. Каждый сервис с записью на хост требует chown -R 100000:100000, и пересчёт нужен для каждого нового сервиса.
[!info] ВАЖНО Включай только если осознанно готов управлять UID маппингом для каждого тома.
Почему нет icc:false
Глобальный icc: false ломает общение контейнеров внутри одного docker-compose стека - Compose создаёт свою bridge сеть, и без icc контейнеры в ней не видят друг друга без явных --link.
Правильная изоляция - через отдельные сети Docker, не через глобальный флаг daemon. Раздел ниже.
Примени:
sudo systemctl restart docker
Проверь:
docker info | grep -E "Storage Driver|Logging Driver|live-restore"
sysctl для Docker-хоста
sudo nano /etc/sysctl.d/99-docker.conf
# IP forwarding - обязательно для Docker
net.ipv4.ip_forward = 1
# IPv6 forwarding - только если используешь IPv6 в контейнерах
# net.ipv6.conf.all.forwarding = 1
# Лимиты inotify
fs.inotify.max_user_instances = 512
fs.inotify.max_user_watches = 524288
# Для Java приложений (Elasticsearch и подобных) в контейнерах
vm.max_map_count = 262144
sudo sysctl -p /etc/sysctl.d/99-docker.conf
systemd-resolved и DNS в контейнерах
Это самая частая проблема на Ubuntu специфично.
Ubuntu 24.04 использует systemd-resolved - DNS резолвер слушает на 127.0.0.53. Контейнер не может достать этот адрес - 127.0.0.53 это loopback хоста, контейнер находится в своём network namespace и видит свой собственный loopback, не хостовый.
Симптом:
docker run --rm alpine nslookup google.com
# ;; connection timed out; no servers could be reached
Решение 1 (быстрое, для отдельных контейнеров):
docker run --dns 8.8.8.8 --dns 8.8.4.4 alpine nslookup google.com
Решение 2 (для всех контейнеров через daemon.json):
sudo nano /etc/docker/daemon.json
Добавь:
{
"dns": ["8.8.8.8", "8.8.4.4"]
}
sudo systemctl restart docker
Решение 3 (если нужен внутренний DNS, не публичный):
Узнай реальный upstream DNS на хосте:
resolvectl status | grep "DNS Server"
Используй именно его IP (не 127.0.0.53) в daemon.json.
Docker Compose поддерживает то же самое на уровне сервиса:
services:
app:
dns:
- 8.8.8.8
- 8.8.4.4
UFW + Docker интеграция
Та же проблема что и на любом дистрибутиве - Docker пишет свои iptables правила напрямую, обходя UFW.
Подход 1 (рекомендуемый): порты только на localhost
# Плохо - обходит UFW
docker run -p 80:80 nginx
# Хорошо
docker run -p 127.0.0.1:80:80 nginx
Reverse proxy на хосте слушает 0.0.0.0:443, UFW разрешает только 443, контейнеры недоступны напрямую.
Подход 2: DOCKER-USER chain
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
-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
sudo ufw reload
Для большинства случаев достаточно подхода 1.
Сетевая изоляция контейнеров
Вместо глобального icc: false - отдельные сети под каждое приложение:
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
Контейнеры из разных сетей не видят друг друга. В docker-compose сети создаются автоматически для каждого стека - это и есть рабочая изоляция.
Безопасность образов
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
trivy image nginx:alpine
Перед деплоем:
trivy image --exit-code 1 --severity CRITICAL,HIGH nginx:alpine
Правило: CRITICAL уязвимости - образ не идёт в production.
Выбор образов
# Большой attack surface
FROM ubuntu:24.04
# Минимальный
FROM alpine:3.20
# Ещё меньше - нет shell, нет пакетного менеджера
FROM gcr.io/distroless/static-debian12
Конкретные теги, не latest:
docker pull nginx:1.27-alpine
Изоляция контейнеров
Capabilities
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
Read-only filesystem
docker run --read-only --tmpfs /tmp --tmpfs /var/run nginx
Seccomp
Docker применяет дефолтный seccomp профиль автоматически:
docker info | grep seccomp
Никогда не использовать --privileged
# НИКОГДА в production - отключает всю изоляцию
docker run --privileged nginx
Ресурсные ограничения
# Memory с soft limit
docker run -m 512m --memory-reservation 256m nginx
# CPU
docker run --cpus="0.5" nginx
# PID limit - защита от fork bomb
docker run --pids-limit 200 nginx
Проверка OOM:
dmesg | grep -i "oom\|killed process"
docker inspect CONTAINER --format='{{.State.OOMKilled}}'
Docker Compose с hardening
services:
web:
image: nginx:1.27-alpine
container_name: web
restart: unless-stopped
ports:
- "127.0.0.1:8080:80"
dns:
- 8.8.8.8
- 8.8.4.4
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"
networks:
- backend
app:
image: myapp:1.2.3
container_name: app
restart: unless-stopped
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
volumes:
db_data:
secrets:
db_password:
file: ./secrets/db_password.txt
internal: true для backend - база данных без доступа к интернету.
Регулярные задачи обслуживания
# Что занимает место
docker system df
# Очистка неиспользуемого
docker system prune -f
# Полная очистка включая неиспользуемые образы
docker system prune -a -f
# Volumes - ОСТОРОЖНО, удаляет данные безвозвратно
docker volume prune -f
Cron:
sudo crontab -e
0 4 * * 0 docker system prune -f >> /var/log/docker-prune.log 2>&1
Метрики:
docker stats --no-stream
Аудит через auditd
sudo apt install -y auditd
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
sudo systemctl enable auditd
sudo systemctl restart auditd
sudo ausearch -k docker | tail -20
Troubleshooting
DNS не работает в контейнере
См. раздел "systemd-resolved и DNS в контейнерах" выше - это специфика Ubuntu, самая частая проблема на этом дистрибутиве.
Docker обходит UFW
Причина: Docker пишет напрямую в iptables.
Решение: 127.0.0.1:port:port или DOCKER-USER chain.
Permission denied на bind mount
Причина: UID процесса в контейнере не совпадает с владельцем файлов на хосте.
docker run --rm IMAGE id
sudo chown -R UID:GID /path/to/mount
OOM killer убивает контейнер
dmesg | grep -i "oom\|killed process"
docker inspect CONTAINER --format='{{.State.OOMKilled}}'
Увеличить memory limit или оптимизировать приложение.
newgrp docker не сработал после usermod
Симптом: docker: permission denied несмотря на usermod -aG docker $USER.
Причина: Группа применяется только к новым сессиям. Текущая сессия не знает о изменении.
Решение:
newgrp docker
Или просто выйти и зайти заново по SSH.
Диск забит логами
sudo find /var/lib/docker/containers/ -name "*.log" -exec du -sh {} \; | sort -rh | head -10
Экстренно:
sudo truncate -s 0 /var/lib/docker/containers/CONTAINER_ID/CONTAINER_ID-json.log
Постоянно - max-size/max-file в daemon.json (уже настроено).
Финальная проверка
sudo systemctl status docker
docker info | grep -E "Storage Driver|Logging Driver|live-restore"
docker run --rm alpine nslookup google.com
ss -tulnp | grep docker
docker system df
Отличия от Debian 12
| Аспект | Debian 12 | Ubuntu 24.04 |
|---|---|---|
| Snap версия Docker | Не существует | Существует, избегать |
| DNS resolver | Обычно прямой /etc/resolv.conf |
systemd-resolved (127.0.0.53), требует явной настройки DNS в Docker |
| Группа docker | newgrp docker работает |
newgrp docker работает, та же команда |
| UFW | Нужно устанавливать | Предустановлен |
| GPG ключ репозитория Docker | download.docker.com/linux/debian/gpg |
download.docker.com/linux/ubuntu/gpg |
Что дальше
Docker-хост готов к production нагрузке.
- Reverse proxy - Traefik или nginx
- Мониторинг - Prometheus + cAdvisor + Grafana
- Централизованные логи - Loki + Promtail
- Секреты - HashiCorp Vault или Docker Secrets
Стек этой статьи: Ubuntu 24.04 LTS (Noble Numbat) · Docker CE · UFW · systemd-resolved · auditd · Trivy · docker-compose