Документ для оператора: развёртывание, обновление, диагностика и восстановление. Рассчитан на self-hosted single-node. Для кластерного деплоя смотрите deployments/k8s/ (экспериментальный режим).
- Linux x86_64, Docker 24+, Docker Compose v2.
- 4 ГБ RAM, 20 ГБ диска (под БД и резервные копии).
- Домен с A-записью на IP сервера.
- Открытые порты 80 и 443.
git clone https://github.com/bmstu-itstech/tjudge.git
cd tjudge
./scripts/init-secrets.sh # создаст ./secrets/{db_password,redis_password,jwt_secret}
chmod 600 secrets/*
cp .env.production.example .env.productionМинимум для .env.production:
ENVIRONMENT=production
BASE_URL=https://tjudge.example.com
JWT_SECRET=<случайные 48+ байт; openssl rand -hex 48>
DB_PASSWORD=<crypto-random>
REDIS_PASSWORD=<crypto-random>
CORS_ALLOWED_ORIGINS=https://tjudge.example.com
WEBSOCKET_ALLOWED_ORIGINS=https://tjudge.example.com
RATE_LIMIT_ENABLED=trueJWT_SECRET не должен содержать плейсхолдеры (CHANGE_ME, secret, password и подобные): при старте в prod код сразу упадёт с ошибкой.
# TLS: сертификат Let's Encrypt.
./scripts/init-ssl.sh tjudge.example.com admin@example.com
# Сборка и запуск.
docker compose -f docker-compose.prod.yml build
docker compose -f docker-compose.prod.yml up -d
# Миграции применяются автоматически сервисом migrate.
# Проверка health.
curl https://tjudge.example.com/health # ожидаем "OK"
# Назначение первого админа.
docker exec -it tjudge-api ./tjudge-admin promote admin@example.comАвтоматический backup включается профилем compose:
docker compose -f docker-compose.prod.yml --profile backup up -d backupПо умолчанию backup раз в сутки в volume backups_data (prod) или ./backups (self-hosted), retention 30 дней. Ручной запуск: ./scripts/backup.sh ./backups.
Проверка: docker logs tjudge-backup через сутки.
docker compose -f docker-compose.prod.yml --profile monitoring up -d prometheus grafana loki alertmanagerGrafana на https://tjudge.example.com:3000 (или за reverse-proxy). Учётные данные задаются в .env.production:
GF_ADMIN_USER=admin
GF_ADMIN_PASSWORD=<replace>
-
JWT_SECRETне короче 32 байт, не из blacklist-плейсхолдеров (автопроверка при старте). -
.env.productionне в git:git ls-files .env.productionпусто. - TLS настроен, HTTP-трафика нет, HSTS выставлен.
-
CORS_ALLOWED_ORIGINSсодержит только ваши домены без*. -
WEBSOCKET_ALLOWED_ORIGINSзадан (в prod fail-closed). - Worker запущен не от root:
docker compose ps workerпоказываетuser=1000. -
RATE_LIMIT_ENABLED=true. - Backup-сервис работает:
docker ps | grep tjudge-backup. - В CI прошли
gosec,npm audit,Trivy; HIGH-находки исправлены. - Первый админ назначен, пароль надёжный.
./scripts/blue-green-deploy.sh <new-tag>
./scripts/smoke-test.sh # проверяем готовность нового стека
./scripts/switch-traffic.sh # переключаем nginx-upstream
# После ~5 минут мониторинга:
./scripts/blue-green-deploy.sh cleanupОткат: ./scripts/rollback.sh.
scripts/deploy.sh и scripts/blue-green-deploy.sh автоматически вызывают cleanup_old_images после успешного деплоя. Функция делает три вещи:
- Удаляет dangling-образы (безымянные слои, оставшиеся после rebuild).
- Для каждого репозитория
tjudge-{api,worker,executor,migrate,cli}оставляет N последних тегов (по умолчанию 3, переопределяется черезTJUDGE_IMAGE_KEEP), остальные сносит. - Чистит build-кэш старше 7 дней.
Почему не docker image prune -a: образ tjudge-executor запускается воркером on-demand (internal/infrastructure/executor/executor.go:169-186), поэтому между матчами на него нет ни одного работающего контейнера. Blanket-prune удалил бы его и сломал бы выполнение матчей до следующего pull'а из ghcr.io. Tag-based retention решает это и сохраняет предыдущие версии API/worker для rollback.
Если на сервере всё равно накопился мусор (длительный простой деплоя, старые проекты), ручная чистка:
# Безопасно: dangling-образы + build-кэш.
docker image prune -f
docker builder prune -af
# Посмотреть, что занимает место.
docker system df
# Увеличить/уменьшить retention для следующего деплоя.
TJUDGE_IMAGE_KEEP=5 ./scripts/blue-green-deploy.sh <version>НЕ запускать на проде:
docker image prune -afбез фильтров — удалитtjudge-executor, следующий матч упадёт.docker system prune -af --volumesиdocker volume prune— если контейнерpostgresилиredisв этот момент остановлен (например, между переключениями blue-green), volume посчитается unused и улетит вместе с БД.
Ротация логов контейнеров — в /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": { "max-size": "50m", "max-file": "3" }
}После правки: sudo systemctl restart docker. Новые контейнеры подхватят настройку автоматически, старые — при следующем пересоздании (ближайший деплой).
Каталог data/programs/ (переменная HOST_PROGRAMS_PATH) хранит загруженные программы участников и не чистится автоматически. Это данные, не мусор; если нужна политика retention — обсуждается отдельно.
# Health API.
curl -sf http://localhost:8080/health # "OK"
curl -sfH "Authorization: Bearer <admin-jwt>" \
http://localhost:8080/api/v1/system/health # JSON со статусом
# Метрики.
curl -s http://localhost:8080/metrics | grep -E '^(tjudge_|go_goroutines)'
# Последние логи API.
docker logs --tail=200 tjudge-api
# Очереди и dead-letter.
docker exec -it tjudge-redis redis-cli -n 0 llen queue:high
docker exec -it tjudge-redis redis-cli -n 0 llen queue:dead_letter- Смотрим логи:
docker logs --tail=300 tjudge-api. Частые причины:JWT_SECRET must be at least 32 bytes: обновите секрет (см. §11.1).database connection refused: проверяем Postgres (§9.3).panic: send on closed channel: баг инфраструктуры, заводим issue.
- Рестарт:
docker compose restart api. - Если не помогает, откат:
./scripts/rollback.sh.
Триггер: tjudge_queue_size{priority="high"} > 1000.
- Проверяем размер пула воркеров:
curl -s localhost:9090/metrics | grep tjudge_worker_pool_size. - Увеличиваем
WORKER_MAXи перезапускаем воркер:echo "WORKER_MAX=50" >> .env docker compose up -d worker
- Если матчи падают:
docker logs --tail=300 tjudge-worker | grep ERROR. Частая причина - отсутствует образtjudge-cli. Пересобрать:docker compose build tjudge-cli.
docker ps | grep postgres- контейнер запущен?- Если нет:
docker compose up -d postgres, ждём healthcheck. - Если запущен, но недоступен:
docker exec -it tjudge-postgres psql -U tjudge -c 'SELECT 1'. - Проверить диск:
df -h /var/lib/docker. При переполнении удалить старые backup-ы и партиции. - Crash-loop: восстановление из backup (§10).
API автоматически переключается на fallback rate-limiter (0.5× от основного лимита). Очередь матчей при падении Redis не работает.
- Перезапустить:
docker compose restart redis. - Если данные очередей потеряны, запустить recovery-worker: он переставит pending-матчи в очередь.
- Метрика
tjudge_queue_deadletter_sizeрастёт - возможно poison-сообщения; проверить deserializer. - Клиентский flood: в
client.goвключён per-client rate limit (10 msg/sec). Лимитируемые клиенты получают close 1008.
curl -sH "Authorization: Bearer <admin-jwt>" \
'http://localhost:8080/api/v1/admin/audit?limit=500' | jqВ БД: SELECT * FROM audit_log ORDER BY created_at DESC LIMIT 200;.
# 1. Остановить API и worker, чтобы не было конкурентных записей.
docker compose stop api worker
# 2. Восстановить БД (пример: последний backup).
LATEST=$(ls -t backups/tjudge_*.sql.gz | head -1)
gunzip -c "$LATEST" | docker exec -i tjudge-postgres psql -U tjudge tjudge
# 3. Прогнать миграции на случай, если backup старше текущих.
docker compose up migrate
# 4. Запустить api и worker.
docker compose up -d api workerPoint-in-time recovery пока не настроен: WAL-archiving не включён. Для prod рекомендуется pgbackrest или wal-g с S3-хранилищем.
- Сгенерировать:
openssl rand -hex 48. - Обновить secret (Docker secrets или env-переменная).
docker compose up -d api(rolling приreplicas>1).- Все существующие сессии инвалидируются, пользователи должны перелогиниться.
make admin EMAIL=foo@bar.commake migrate-up # применить pending
make migrate-down # откат последней (только dev)
make migrate-create NAME=add_foo # шаблон новой миграции-- bcrypt-hash (cost 12, как в auth.Service):
-- SELECT crypt('newpassword', gen_salt('bf', 12));
UPDATE users SET password_hash = '$2a$12$...' WHERE email = 'foo@bar.com';WORKER_MAX: максимум 200 при 4 vCPU; не ставить вышеDB_MAX_CONNECTIONS * 1.5.DB_MAX_CONNECTIONS: держим в пределахpg_settings.max_connections - 10с запасом на админские сессии.REDIS_POOL_SIZE: 50-200 достаточно.
API stateless, запускается в нескольких экземплярах. Worker безопасен в multi-instance режиме через Redis distributed lock. Переход в K8s: см. deployments/k8s/ (требует доработки).
- Обновление ELO идёт delta-based: параллельные матчи одного участника могут давать snapshot-based deltas. Для строгой сериализации нужен advisory lock.
- Docker-in-Docker worker монтирует
docker.sockread-only с non-root пользователем. На хосте должна существовать docker-group с совпадающим GID.