Home/Operations/Deployment ENУКРРУС API Reference (ReDoc) ↗

Deployment

Where it runs, how a deploy happens, and what to do when one fails.

Topology

languages.service.custom.mt on the shared services host, behind the shared Caddy edge proxy.

Internet ──▶ Caddy (:443, /opt/caddy)
                └─ languages.service.custom.mt ──▶ language-service:8000
                                                        └─▶ postgres (own container, own volume)

The service has its own Postgres, in its own compose stack with its own volume. It has no connection to the legacy console database at any point — that database is read exactly once, by hand, to build the seed. See Rebuilding the Seed.

Everything lives in /opt/language-service/ on the host: docker-compose.prod.yml, .env, and the Postgres volume.

Auto-deploy

Push to main → CI → build → deploy, via .github/workflows/.

  1. CI — ruff, mypy, pytest against SQLite and Postgres, seed integrity check, Docker image build and smoke test.
  2. Buildlinux/arm64 image pushed to ghcr.io/custommt/language_service, tagged with the commit SHA and latest. The host is Graviton; an amd64-only image would fail with "exec format error".
  3. Deploy — over SSH: sync compose and Caddy files, pull, alembic upgrade head, seed, restart, wait for the health gate, reload Caddy.

Gated on the repository variable DEPLOY_ENABLED=true. Secrets: DEPLOY_HOST, DEPLOY_USER, DEPLOY_SSH_KEY.

The seed runs on every deploy

python -m app.cli seed is idempotent and upsert-shaped: it matches rows on their natural keys, updates in place, and never deletes. Edits made in the admin panel to rows the seed does not know about are left alone. A pinned code the seed does know about will be reset to the seeded value — so a correction meant to be permanent belongs in the seed, not only in the panel.

Configuration

Variable Required Notes
DATABASE_URL yes postgresql+asyncpg://…
APP_ENV yes production on the server
ADMIN_USERNAME yes
ADMIN_PASSWORD_HASH_FILE yes file holding the argon2id hash — see Admin Panel
ADMIN_SECRET_KEY_FILE yes openssl rand -hex 48 into a file
ADMIN_ENABLED no false removes the panel entirely
DOCS_ENABLED no Swagger UI. /redoc and /wiki are always public
CACHE_TTL_SECONDS no default 300

With APP_ENV=production and the admin enabled, a missing ADMIN_SECRET_KEY or ADMIN_PASSWORD_HASH is a startup error. Secrets are generated on the server and never leave it.

Health

Probe Checks Use
/health nothing — the process is running container liveness
/health/ready database reachable, catalog non-empty deploy gate, load balancer

Readiness fails when migrations ran but the seed did not: an empty catalog would answer 404 to everything.

Operating

cd /opt/language-service

docker compose -f docker-compose.prod.yml logs -f api
docker compose -f docker-compose.prod.yml run --rm api python -m app.cli stats
docker compose -f docker-compose.prod.yml run --rm api python -m app.cli resolve deepl_api pt_BR

# rotate the admin password
docker compose -f docker-compose.prod.yml run --rm api python -m app.cli hash-password
# → paste into .env, then:
docker compose -f docker-compose.prod.yml up -d api

Logs are one JSON object per line: event, level, plus per-event fields. Request lines carry correlation_id.

Rolling back

Deploy a previous image tag:

cd /opt/language-service
sed -i "s|^IMAGE=.*|IMAGE=ghcr.io/custommt/language_service:<sha>|" .env
docker compose -f docker-compose.prod.yml up -d api

Migrations are additive, so one release back needs no downgrade.

When a deploy fails

Symptom Check
Container unhealthy logs api — usually DATABASE_URL or a missing admin secret
/health/ready 503, catalog: false the seed did not run: run --rm api python -m app.cli seed
502 from Caddy the container is not on the edge network, or the site file did not sync
TLS not issued DNS: *.service.custom.mt must resolve to the host

Next