Эволюция Docker-конфигураций: от разрозненных контейнеров к управляемым стекам
Оглавление
Раньше каждый сервис жил в отдельном репозитории с минимальным docker-compose.yml:
# Старый подход: garrysmod-server/
services:
garrysmod-server:
image: ceifa/garrysmod:latest
ports:
- "27015:27015"
volumes:
- ./garrysmod:/data
restart: unless-stoppedПроблемы:
- ❌ Нет контроля ресурсов - один сервис мог «съесть» всю память
- ❌ Нет health checks - упавший сервис не детектировался
- ❌ Нет лог-ротации - логи росли до заполнения диска
- ❌ Нет изоляции сетей - все сервисы в
defaultсети - ❌ Обновление каждого сервиса вручную - высокий риск человеческой ошибки
Новый подход: стеки с явными контрактами#
Теперь связанные сервисы группируются в единый репозиторий с общей конфигурацией:
grafana-stack/
├── compose.yaml # Единый стек: grafana, loki, oncall-*
├── stack.env # Общие переменные (вынесены из кода)
├── loki/
│ ├── Dockerfile # Кастомная сборка при необходимости
│ └── loki.yml # Конфиг приложения
└── README.md # Документация: порты, зависимости, деплойКлючевые изменения#
1. Ресурсные лимиты (предотвращение «голодания»)#
services:
minecraft-server:
cpus: "2.0" # Жёсткий лимит CPU
mem_limit: 3g # Жёсткий лимит памяти
pids_limit: 300 # Защита от fork-бомбЗачем:
- Предотвращает ситуацию, когда один сервис блокирует остальные
- Позволяет точно планировать нагрузку на хост
- Упрощает диагностику: если сервис упирается в лимит - это видно сразу
2. Безопасность по умолчанию#
services:
grafana:
security_opt:
- no-new-privileges:true # Запрет escalation привилегий
- seccomp:unconfined # Только где действительно нужно (например, для systemd внутри)Почему no-new-privileges:
- Контейнер не может получить больше прав, чем у него есть при старте
- Защита от уязвимостей, эксплуатирующих setuid-бинарники
- Практически нулевой оверхед
3. Наблюдаемость: логи и health checks#
services:
grafana:
logging:
driver: json-file
options:
max-file: "3" # Хранить только 3 ротированных файла
max-size: 10m # Каждый файл - максимум 10 МБ
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"]
interval: 15s # Проверка каждые 15 секунд
timeout: 5s # Таймаут на ответ
retries: 3 # 3 неудачи = unhealthy
start_period: 30s # Не считать ошибки во время стартаРезультат:
- Логи не заполняют диск (ротация + лимиты)
- Оркестратор видит состояние сервиса (
docker psпоказывает(healthy)) - Можно настроить алертинг на
unhealthyстатус
4. Сетевая изоляция и service discovery#
networks:
prometheus:
external: true # Общая сеть для всех сервисов, экспортирующих метрики
name: prometheus
traefik:
external: true # Общая сеть для публичных сервисов за прокси
name: traefik
services:
grafana:
networks: [prometheus, traefik] # Видит и метрики, и веб-трафик
loki:
networks: [prometheus] # Только метрики, нет публичного доступаПреимущества:
- Сервисы находят друг друга по имени (
http://loki:3100) - Публичный доступ только через те сервисы, которые явно подключены к
traefik - Легко добавить новый сервис в мониторинг: подключил к сети
prometheus- и он уже виден
5. Корректная остановка и сигналы#
services:
minecraft-server:
stop_signal: SIGTERM # Сначала вежливая просьба остановиться
stop_grace_period: 60s # Ждать до 60 секунд перед SIGKILL
stdin_open: true # Для интерактивных консолей
tty: trueЗачем:
- Предотвращает потерю данных при рестарте (сервер успевает сохранить мир)
- Позволяет приложениям корректно закрыть соединения, сбросить кэши
SIGTERM+ grace period - стандарт для production-деплоя
6. Конфигурация через .env, а не хардкод#
# compose.yaml
services:
grafana:
env_file: [stack.env] # Вынос чувствительных данных
# stack.env (в .gitignore)
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASS}
DOMAIN=potatoenergy.ruПлюсы:
- Один файл с секретами - легче ротировать, легче аудировать
- Нет паролей в репозитории
- Легко деплоить на разные окружения (dev/stage/prod) с разными
.env
Сравнительная таблица#
| Критерий | Старый подход | Новый подход |
|---|---|---|
| Группировка | 1 сервис = 1 репо | Связанные сервисы = 1 стек |
| Ресурсы | Нет лимитов | cpus, mem_limit, pids_limit |
| Безопасность | По умолчанию Docker | no-new-privileges, явный seccomp |
| Логи | Рост до заполнения диска | Ротация: max-file, max-size |
| Health | Нет | Health check с интервалом/таймаутом |
| Сети | Все в default | Явные внешние сети (prometheus, traefik) |
| Остановка | Мгновенный SIGKILL | SIGTERM + stop_grace_period |
| Конфиг | Хардкод в compose | .env файлы, вынесенные из репо |
Эволюция в цифрах#
| Метрика | Было | Стало |
|---|---|---|
| Среднее время деплоя стека | ~15 мин (ручное обновление 5 сервисов) | ~3 мин (один docker compose up -d) |
| Потребление памяти (хост) | Непредсказуемо, частые OOM | Стабильно, лимиты гарантируют изоляцию |
| Время восстановления после сбоя | Зависит от ручного вмешательства | Авто-рестарт + health check детектирует проблему |
| Аудит конфигурации | Нужно смотреть 10+ репозиториев | Один compose.yaml на стек |
Практические рекомендации#
При миграции со старого подхода#
- Начните с одного стека (например, мониторинг: grafana + loki + prometheus)
- Добавляйте лимиты постепенно: сначала
mem_limit, потомcpus, потомpids_limit - Тестируйте health checks локально перед деплоем:
docker compose up --abort-on-container-exit - Выносите секреты в
.envи добавляйте его в.gitignoreдо первого коммита - Документируйте внешние сети: какая сеть за что отвечает, какие сервисы к ней подключаются
Чеклист для нового сервиса#
- Ресурсные лимиты:
cpus,mem_limit,pids_limit - Security:
no-new-privileges:true, явныйseccompпри необходимости - Логи:
json-fileдрайвер сmax-file/max-size - Health check: осмысленный
test, адекватныеinterval/timeout - Сети: явное подключение к внешним сетям (
prometheus,traefik) - Остановка:
stop_signal: SIGTERM,stop_grace_periodдля долгоживущих сервисов - Конфиг: чувствительные данные в
env_file, не в коде
Ссылки#
- 🐳 Docker Compose Reference
- 🔐 Docker Security Best Practices
- 📊 Healthcheck Documentation
- 🧰 itzg Docker Images - примеры грамотных образов
- 🐙 Репозитории стеков
Здесь пока нет статей.