Напарник подкинул интересную идею — использовать 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: Ответ приложения
- Пользователь заходит на
dev.myapp.com
- Запрос попадает в reverse proxy (Traefik/Nginx)
- Proxy проверяет — работает ли нужный контейнер через Sablier middleware
- Если нет — перенаправляет запрос в Sablier
- Sablier запускает контейнер через Docker API
- Пока контейнер стартует — показывает страницу “Запускаю сервис…”
- Как только контейнер готов — перенаправляет пользователя
Установка и базовая настройка
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
✅ Идеальные случаи использования:
-
Dev/staging окружения
# 15 веток → 15 окружений → экономия 80% ресурсов dev-feature-auth.company.com dev-feature-payments.company.com staging-release-2.1.company.com
-
Внутренние админки
# Используются 2-3 раза в день admin.company.com metrics-dashboard.company.com
-
Demo стенды для клиентов
# Запускаются только во время презентаций demo-client-a.company.com demo-client-b.company.com
-
Feature branch деплои
# Автоматические деплои PR pr-123.company.com pr-456.company.com
❌ Когда НЕ использовать:
- Production сервисы - нужен мгновенный отклик
- Сервисы с фоновыми задачами - cron, queue workers
- Критичные к latency приложения - real-time системы
- 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%)
- ✅ Простую настройку и минимальные накладные расходы
Следующие шаги:
- Попробуйте на тестовом окружении
- Настройте мониторинг и алерты
- Внедрите на всех dev/staging средах
- Посчитайте реальную экономию
Полезные ссылки:
- GitHub: github.com/sablierapp/sablier
- Документация: sablierapp.dev
- Примеры конфигураций: github.com/sablierapp/sablier-examples
Какие окружения планируете оптимизировать первыми? Обсуждаем в телеграм канале!