Skip to content

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:

cp .env.example .env     # then set your provider API keys
docker compose up --build

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.