Перейти к основному содержимому

Эволюция 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
БезопасностьПо умолчанию Dockerno-new-privileges, явный seccomp
ЛогиРост до заполнения дискаРотация: max-file, max-size
HealthНетHealth check с интервалом/таймаутом
СетиВсе в defaultЯвные внешние сети (prometheus, traefik)
ОстановкаМгновенный SIGKILLSIGTERM + stop_grace_period
КонфигХардкод в compose.env файлы, вынесенные из репо

Эволюция в цифрах
#

МетрикаБылоСтало
Среднее время деплоя стека~15 мин (ручное обновление 5 сервисов)~3 мин (один docker compose up -d)
Потребление памяти (хост)Непредсказуемо, частые OOMСтабильно, лимиты гарантируют изоляцию
Время восстановления после сбояЗависит от ручного вмешательстваАвто-рестарт + health check детектирует проблему
Аудит конфигурацииНужно смотреть 10+ репозиториевОдин compose.yaml на стек

Практические рекомендации
#

При миграции со старого подхода
#

  1. Начните с одного стека (например, мониторинг: grafana + loki + prometheus)
  2. Добавляйте лимиты постепенно: сначала mem_limit, потом cpus, потом pids_limit
  3. Тестируйте health checks локально перед деплоем: docker compose up --abort-on-container-exit
  4. Выносите секреты в .env и добавляйте его в .gitignore до первого коммита
  5. Документируйте внешние сети: какая сеть за что отвечает, какие сервисы к ней подключаются

Чеклист для нового сервиса
#

  • Ресурсные лимиты: 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, не в коде

Ссылки
#

Здесь пока нет статей.