Напарник подкинул интересную идею — использовать Sablier для автоматического старта/стопа редко используемых сервисов. Разобрались, внедрили, теперь делюсь опытом и полной настройкой.

Проблема

У нас 15 тестовых окружений, которые работают 24/7, но реально нужны пару часов в неделю. Контейнеры жрут ресурсы просто так:

# Мониторинг показывал
docker stats --no-stream
CONTAINER          CPU %     MEM USAGE / LIMIT   MEM %
dev-frontend       0.01%     128MiB / 2GiB      6.25%
dev-backend        0.02%     256MiB / 4GiB      6.25%
staging-api        0.01%     312MiB / 2GiB      15.25%
# ... ещё 12 контейнеров

# Итого: ~3GB RAM и CPU cycles впустую

Что такое Sablier?

Sablier — это специальный прокси-сервер, который реализует паттерн Scale-to-Zero:

  • Следит за активностью ваших контейнеров через reverse proxy
  • Останавливает их, если никто не обращается определённое время
  • Автоматически запускает при первом запросе
  • Показывает пользователю красивую страницу ожидания во время старта
  • Интегрируется с Traefik, Nginx, Caddy из коробки

Как это работает (пошагово)

sequenceDiagram
    participant User
    participant Proxy as Reverse Proxy
    participant Sablier
    participant Docker
    participant App

    User->>Proxy: GET dev.myapp.com
    Proxy->>Sablier: Проверить статус контейнера
    Sablier->>Docker: docker ps | grep myapp
    Docker->>Sablier: Контейнер остановлен
    Sablier->>Docker: docker start myapp
    Sablier->>User: Страница "Запускаю сервис..."
    Docker->>Sablier: Контейнер готов
    Sablier->>Proxy: Перенаправить на приложение
    Proxy->>App: Проксировать запрос
    App->>User: Ответ приложения
  1. Пользователь заходит на dev.myapp.com
  2. Запрос попадает в reverse proxy (Traefik/Nginx)
  3. Proxy проверяет — работает ли нужный контейнер через Sablier middleware
  4. Если нет — перенаправляет запрос в Sablier
  5. Sablier запускает контейнер через Docker API
  6. Пока контейнер стартует — показывает страницу “Запускаю сервис…”
  7. Как только контейнер готов — перенаправляет пользователя

Установка и базовая настройка

Docker Compose конфигурация

# docker-compose.yml
version: '3.8'

services:
  # Сам Sablier
  sablier:
    image: sablierapp/sablier:1.7.0
    container_name: sablier
    command:
      - start
      - --provider.name=docker
      - --server.host=0.0.0.0
      - --server.port=10000
    volumes:
      # Даём доступ к Docker socket для управления контейнерами
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - web
    ports:
      - "10000:10000"
    environment:
      - SABLIER_THEME=dark
      - SABLIER_SHOW_DETAILS=true
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.sablier.rule=Host(`sablier.local`)"
      - "traefik.http.services.sablier.loadbalancer.server.port=10000"

  # Traefik как reverse proxy
  traefik:
    image: traefik:v3.0
    container_name: traefik
    command:
      - --api.insecure=true
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --providers.file.filename=/etc/traefik/dynamic.yml
      - --entrypoints.web.address=:80
    ports:
      - "80:80"
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro
    networks:
      - web

  # Пример приложения, которое будет останавливаться
  dev-backend:
    image: nginx:alpine
    container_name: dev-backend
    labels:
      # Обычные labels для Traefik
      - "traefik.enable=true"
      - "traefik.http.routers.dev-backend.rule=Host(`dev.app.local`)"
      - "traefik.http.routers.dev-backend.middlewares=sablier-dev-backend"
      
      # Labels для Sablier
      - "sablier.enable=true"
      - "sablier.group=dev-backend"
    networks:
      - web
    # Создаём простую HTML страницу для тестирования
    volumes:
      - ./test-page.html:/usr/share/nginx/html/index.html:ro

networks:
  web:
    external: false

Настройка Traefik middleware

# traefik-dynamic.yml
http:
  middlewares:
    # Middleware для нашего dev окружения
    sablier-dev-backend:
      plugin:
        sablier:
          # Имя группы из label контейнера
          names: dev-backend
          # URL Sablier API
          sablierUrl: http://sablier:10000
          # Время жизни сессии после последнего запроса
          sessionDuration: 30m
          # Имя для отображения на странице ожидания
          displayName: "Dev Backend Environment"
          # Тема страницы загрузки
          theme: dark
          # Показывать технические детали
          showDetails: true
          # Частота обновления страницы ожидания
          refreshFrequency: 2s
          # Таймаут запуска (если контейнер долго стартует)
          blockingTimeout: 120s

# Плагин Sablier для Traefik (если не установлен)
plugins:
  sablier:
    moduleName: github.com/sablierapp/sablier
    version: v1.7.0

Создание тестовой страницы

<!-- test-page.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Dev Backend</title>
    <style>
        body { font-family: Arial; text-align: center; margin-top: 50px; }
        .container { background: #f0f0f0; padding: 20px; border-radius: 10px; display: inline-block; }
    </style>
</head>
<body>
    <div class="container">
        <h1>🚀 Dev Backend Environment</h1>
        <p>Контейнер успешно запущен!</p>
        <p>Время: <span id="time"></span></p>
        <script>
            setInterval(() => {
                document.getElementById('time').textContent = new Date().toLocaleTimeString();
            }, 1000);
        </script>
    </div>
</body>
</html>

Запуск и тестирование

# Запускаем всю инфраструктуру
docker-compose up -d

# Проверяем что всё поднялось
docker ps
CONTAINER ID   IMAGE                    STATUS
abc123         sablierapp/sablier:1.7.0 Up 30 seconds
def456         traefik:v3.0             Up 30 seconds
ghi789         nginx:alpine             Up 30 seconds

# Добавляем домены в /etc/hosts для тестирования
echo "127.0.0.1 dev.app.local sablier.local" >> /etc/hosts

# Тестируем работу
curl -I http://dev.app.local
HTTP/1.1 200 OK

# Ждём 30 минут или останавливаем контейнер вручную
docker stop dev-backend

# Теперь при обращении контейнер автоматически запустится
curl http://dev.app.local
# Увидим страницу загрузки, затем приложение

Продвинутая настройка

Конфигурация для production-like окружения

# docker-compose.prod.yml
version: '3.8'

services:
  sablier:
    image: sablierapp/sablier:1.7.0
    restart: unless-stopped
    command:
      - start
      - --provider.name=docker
      - --server.host=0.0.0.0
      - --server.port=10000
      # Логирование
      - --log.level=info
      - --log.format=json
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - web
    environment:
      # Кастомизация страницы ожидания
      - SABLIER_THEME=dark
      - SABLIER_SHOW_DETAILS=false
      - SABLIER_CUSTOM_THEME_PATH=/themes
      # Безопасность
      - SABLIER_SERVER_BASE_URL=https://sablier.company.com
    volumes:
      - ./custom-themes:/themes:ro
    deploy:
      resources:
        limits:
          memory: 128M
          cpus: '0.1'
        reservations:
          memory: 64M
          cpus: '0.05'

  # Пример реального приложения
  staging-api:
    image: mycompany/api:staging
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/staging
      - REDIS_URL=redis://redis:6379/1
      - LOG_LEVEL=debug
    depends_on:
      - postgres
      - redis
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.staging-api.rule=Host(`staging-api.company.com`)"
      - "traefik.http.routers.staging-api.middlewares=sablier-staging-api,auth"
      - "traefik.http.routers.staging-api.tls=true"
      - "traefik.http.routers.staging-api.tls.certresolver=letsencrypt"
      
      # Sablier конфигурация
      - "sablier.enable=true"
      - "sablier.group=staging-api"
      # Кастомная страница загрузки
      - "sablier.blocking.timeout=60s"
    networks:
      - web
      - backend
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  # База данных - не останавливается
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: staging
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - backend

  redis:
    image: redis:7-alpine
    networks:
      - backend

volumes:
  postgres_data:

networks:
  web:
    external: true
  backend:
    internal: true

Обработка различных сценариев

# traefik-advanced.yml
http:
  middlewares:
    # Быстро стартующие сервисы (фронтенд)
    sablier-frontend:
      plugin:
        sablier:
          names: frontend-dev
          sablierUrl: http://sablier:10000
          sessionDuration: 15m  # Короткая сессия
          displayName: "Frontend Dev"
          blockingTimeout: 30s  # Быстрый старт
          
    # Медленно стартующие сервисы (бекенд с БД)
    sablier-backend:
      plugin:
        sablier:
          names: backend-dev
          sablierUrl: http://sablier:10000
          sessionDuration: 60m   # Длинная сессия
          displayName: "Backend API"
          blockingTimeout: 120s  # Долгий старт
          
    # Группа связанных сервисов
    sablier-microservices:
      plugin:
        sablier:
          names: user-service,payment-service,notification-service
          sablierUrl: http://sablier:10000
          sessionDuration: 45m
          displayName: "Microservices Stack"
          blockingTimeout: 90s

    # Базовая аутентификация
    auth:
      basicAuth:
        users:
          - "dev:$2y$10$..."

Мониторинг и метрики

Настройка логирования

# sablier-config.yml
server:
  host: 0.0.0.0
  port: 10000

provider:
  name: docker
  
logging:
  level: info
  format: json
  
# Экспорт метрик для Prometheus
metrics:
  prometheus:
    enabled: true
    path: /metrics
    port: 9090

Prometheus метрики

# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'sablier'
    static_configs:
      - targets: ['sablier:9090']
    metrics_path: /metrics
    scrape_interval: 30s

Полезные метрики для мониторинга

# Статистика использования
curl http://sablier:10000/api/strategies | jq

# Пример ответа:
{
  "strategies": [
    {
      "name": "dev-backend",
      "status": "stopped",
      "lastActivity": "2025-01-27T10:30:00Z",
      "uptime": "0s",
      "totalRequests": 42
    }
  ]
}

Подводные камни и решения

1. Пулы соединений к БД

Проблема: Приложение может упасть при старте из-за большого пула соединений

# Плохо - БД удивится 20 новым соединениям одновременно
environment:
  - DATABASE_POOL_SIZE=20
  - DATABASE_POOL_MAX_OVERFLOW=10

# Хорошо - для Sablier окружений
environment:
  - DATABASE_POOL_SIZE=2
  - DATABASE_POOL_MAX_OVERFLOW=1
  - DATABASE_POOL_PRE_PING=true
  - DATABASE_POOL_RECYCLE=3600

2. Health checks конфликтуют с остановкой

# Проблема - healthcheck мешает остановке контейнера
services:
  app:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s  # Будит контейнер каждые 30 секунд!

# Решение - отключаем healthcheck для Sablier окружений
services:
  app:
    # Используем depends_on вместо healthcheck
    depends_on:
      - database
    # Или настраиваем healthcheck только для продакшена
    healthcheck:
      disable: true

3. WebSocket и длинные соединения

# nginx.conf
upstream backend {
    server backend:8000;
}

server {
    location / {
        proxy_pass http://backend;
        
        # Для WebSocket соединений
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        
        # Увеличиваем таймауты для длинных соединений
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        
        # Важно: устанавливаем session affinity
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
    }
}

4. Инициализация приложения

# app.py - Пример правильной инициализации для Sablier
import time
import signal
import sys

class GracefulApp:
    def __init__(self):
        self.running = True
        signal.signal(signal.SIGTERM, self.shutdown)
        signal.signal(signal.SIGINT, self.shutdown)
    
    def shutdown(self, signum, frame):
        print("Получен сигнал завершения, корректно останавливаем...")
        self.running = False
        # Закрываем соединения, сохраняем состояние
        sys.exit(0)
    
    def start(self):
        print("Приложение запускается...")
        # Быстрый старт - не делаем тяжёлую инициализацию в конструкторе
        self.init_lightweight()
        
        while self.running:
            # Основной цикл приложения
            time.sleep(1)
    
    def init_lightweight(self):
        # Минимальная инициализация для быстрого старта
        pass

if __name__ == "__main__":
    app = GracefulApp()
    app.start()

Альтернативы и сравнение

Kubernetes HPA с minReplicas: 0

# hpa.yaml - Kubernetes альтернатива
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: app
  minReplicas: 0  # Поддерживается с Kubernetes 1.16+
  maxReplicas: 10
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 10
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15

Плюсы K8s HPA:

  • Нативная интеграция с Kubernetes
  • Больше метрик для масштабирования (CPU, память, кастомные)
  • Enterprise-ready

Минусы:

  • Сложнее настроить
  • Нужен полноценный Kubernetes кластер
  • Нет красивой страницы ожидания

Knative Serving

# knative-service.yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: app
spec:
  template:
    metadata:
      annotations:
        autoscaling.knative.dev/minScale: "0"
        autoscaling.knative.dev/maxScale: "10"
        autoscaling.knative.dev/target: "10"
    spec:
      containers:
      - image: myapp:latest
        ports:
        - containerPort: 8080

Плюсы Knative:

  • Очень мощная платформа
  • Автоматическое масштабирование
  • Blue-green deployments

Минусы:

  • Overkill для простых задач
  • Требует серьёзных ресурсов
  • Сложная отладка

Когда использовать Sablier

✅ Идеальные случаи использования:

  1. Dev/staging окружения

    # 15 веток → 15 окружений → экономия 80% ресурсов
    dev-feature-auth.company.com
    dev-feature-payments.company.com
    staging-release-2.1.company.com
    
  2. Внутренние админки

    # Используются 2-3 раза в день
    admin.company.com
    metrics-dashboard.company.com
    
  3. Demo стенды для клиентов

    # Запускаются только во время презентаций
    demo-client-a.company.com
    demo-client-b.company.com
    
  4. Feature branch деплои

    # Автоматические деплои PR
    pr-123.company.com
    pr-456.company.com
    

❌ Когда НЕ использовать:

  1. Production сервисы - нужен мгновенный отклик
  2. Сервисы с фоновыми задачами - cron, queue workers
  3. Критичные к latency приложения - real-time системы
  4. Stateful приложения - с локальным состоянием

Полезные команды для управления

# Проверить статус всех групп
curl http://sablier:10000/api/strategies | jq '.strategies[] | {name, status, lastActivity}'

# Принудительно остановить сервис
curl -X POST http://sablier:10000/api/strategies/stop/dev-backend

# Принудительно запустить сервис  
curl -X POST http://sablier:10000/api/strategies/start/dev-backend

# Получить метрики Prometheus
curl http://sablier:9090/metrics | grep sablier

# Мониторинг логов
docker logs -f sablier | jq 'select(.level == "info")'

# Проверить конфигурацию Traefik
curl http://localhost:8080/api/http/middlewares | jq '.[] | select(.name | contains("sablier"))'

Экономический эффект

Расчёт экономии для нашего случая:

# До внедрения Sablier:
# 15 окружений × 24/7 × (2GB RAM + 0.5 CPU) = постоянное потребление

# После внедрения:
# 15 окружений × ~3 часа в неделю активности = 87.5% экономии

# В деньгах (AWS):
# t3.large (2 vCPU, 8GB) = $0.0832/час
# 15 инстансов × $0.0832 × 24 × 30 = $899.52/месяц

# С Sablier:
# 15 инстансов × $0.0832 × 3 × 4 = $149.76/месяц
# Экономия: $749.76/месяц = $8997.12/год

Заключение

Sablier — это простое и элегантное решение для экономии ресурсов на некритичных окружениях. Настраивается за час, работает стабильно, экономит реальные деньги.

Что получаем в итоге:

  • ✅ Автоматическое управление жизненным циклом контейнеров
  • ✅ Красивые страницы ожидания для пользователей
  • ✅ Прозрачную интеграцию с существующим reverse proxy
  • ✅ Существенную экономию ресурсов (до 90%)
  • ✅ Простую настройку и минимальные накладные расходы

Следующие шаги:

  1. Попробуйте на тестовом окружении
  2. Настройте мониторинг и алерты
  3. Внедрите на всех dev/staging средах
  4. Посчитайте реальную экономию

Полезные ссылки:

Какие окружения планируете оптимизировать первыми? Обсуждаем в телеграм канале!