Deployment¶
Two services: the FastAPI backend and the Next.js frontend, each shipped as a container image. The backend image needs Postgres, because it runs its migrations at start, and a volume if run data is to survive a redeploy.
Self-hosting with docker compose¶
The whole stack (Postgres, backend, frontend) in one command:
Frontend on http://localhost:3000, backend on http://localhost:8000. Run data
persists in the runs-data volume, Postgres in postgres-data.
This runs in single-tenant mode, which performs no authentication, so do not expose it to the internet. See Authentication.
API_URL is read at request time, so pointing the frontend at a different backend
takes a restart rather than a rebuild.
Images¶
| Service | Build file | Contents |
|---|---|---|
| Backend | Dockerfile |
Python 3.14, FastAPI, weasyprint system libraries |
| Frontend | frontend/DockerfileFrontend |
Node 26, Next.js standalone output from a three-stage build |
.github/workflows/publish-images.yml builds both and pushes them to GHCR on a
version tag. Tags come from the release label on a merged pull request. See
Releases. Each is a manifest list covering
linux/amd64 and linux/arm64, so a pull resolves to the host's own
architecture. Releases up to and including v0.2.0 carry amd64 only.
The backend image runs alembic upgrade head on every start, so the schema is at
the latest revision before the server accepts requests.
Hosting the two services¶
Any host that runs the two images works the same way.
Backend service: needs a volume for run data and a Postgres.
| Variable | What it is |
|---|---|
DATABASE_URL |
Required. The Postgres connection string; the image runs alembic upgrade head at start and fails without it |
| Provider API keys | One per provider your runs use: ANTHROPIC_API_KEY, OPENAI_API_KEY, and so on. The shipped presets use an Anthropic judge wherever a scenario has one |
ALLOWED_ORIGINS |
Comma-separated frontend URLs, for CORS |
| whatever the installed identity provider reads | Required for multi-tenant auth. With no provider installed the server is single-tenant. See Authentication |
OAUTH_ISSUER_URL |
Public backend URL. Enables the MCP endpoint |
FRONTEND_URL |
Base URL the MCP consent flow redirects the browser to. Falls back to the first ALLOWED_ORIGINS entry, then http://localhost:3000. Set it in multi-tenant mode so /mcp-consent reaches the right host |
ENABLE_EVALUATIONS |
Set false to disable the REST evaluate endpoint: it returns 403 and the frontend hides its button. Does not affect the CLI |
Frontend service: reads API_URL at runtime (required), plus any
AUTH_PUBLIC_* values its auth adapter needs. None are compiled into the
bundle, so one image serves any environment.
Deploy order: backend first, to get its URL. Set that as the frontend's
API_URL, deploy the frontend, then add the frontend's URL to the backend's
ALLOWED_ORIGINS.
The image keeps runs under /data/runs (GLOSSOGEN_RUNS_DIR), so mount the
volume there.
On Railway, the hosted instance runs the backend from the published image
with a volume at /data/runs and an attached Railway Postgres, whose connection
string becomes DATABASE_URL. The frontend service builds from the repository
with root directory frontend, configured by frontend/railway.toml.
.env.example documents every variable, including the ones that only matter
locally.
Authentication¶
Two modes, switched by whether an identity provider is installed.
Single-tenant mode, the default for a clone and for docker compose up.
Install no provider. The identity middleware resolves every request to a
synthetic local group and local-user, and the frontend renders with no
sign-in flow. It performs no authentication, so do not expose it to a network.
Multi-tenant mode, for anything hosted. Multi-tenancy is in the platform:
/g/[groupSlug]/ routing on the frontend, /api/g/{group_slug}/... on the
backend, the groups table, and the Identity attached to each request. What
the platform does not ship is the provider, so a deployment supplies one; see
Creating an identity provider.
Each organization in the identity provider is a study group. Every run
belongs to exactly one group and is never shared across groups except through
the export/import flow. The active group is the URL slug: /g/team-a/runs/...
on the frontend hits /api/g/team-a/runs/... on the backend, and the request
is accepted only if the caller's session covers team-a.