Install (Self-Hosted)
This is the only supported install path: the Docker Compose stack in
docker/self-host/
on one Linux host. Other setups (own reverse proxy, external database, Kubernetes, Podman)
can work but are unsupported.
The stack you get:
| Service | What it is |
|---|---|
reverse-proxy | Traefik — TLS via Let's Encrypt, routes /, /api, /webhooks |
webapp | React SPA served by nginx |
application-server | Spring Boot API + in-process job worker |
webhook-server | Same image, webhook profile — keeps receiving events while the app restarts |
postgres | PostgreSQL 18 + pg_partman (custom image — vanilla postgres:18 is not a drop-in) |
nats-server | NATS JetStream event buffer |
Requirements
Supported host matrix
Hephaestus supports the blessed single-host stack on the following native (not emulated) hosts:
| Distribution | CPU architecture |
|---|---|
| Ubuntu 24.04 LTS | x86-64 (amd64) |
| Ubuntu 24.04 LTS | ARMv8 64-bit (arm64) |
Both require Docker Engine 28 or newer and Docker Compose 2.24.4 or newer. Docker supports these hosts according to its Ubuntu installation requirements. Each release is qualified on both matrix cells before publication.
Only the listed native hosts and the unmodified self-hosted Compose topology are supported. All other hosts, runtimes, and topologies are outside release qualification.
- 4 vCPUs, 8 GB RAM, 40 GB SSD is the conservative recommended starting point. Absolute floor:
2 vCPUs / 8 GB RAM — the two JVMs
ship with container memory limits of 5 GB (
application-server) and 2 GB (webhook-server), so a smaller host does not swap, it OOM-kills. On a host below that, lower both limits before the first start: setAPPLICATION_SERVER_MEM_LIMITandWEBHOOK_SERVER_MEM_LIMITin.env(each JVM sizes its heap from its own limit, so lowering the limit lowers the heap with it). - These are deployment limits, not a claim that every workload fits. Before raising webhook volume,
mentor concurrency, or
SANDBOX_MAX_CONCURRENT, reproduce the release's Compose capacity qualification on your host shape. A release baseline is valid only when it includes the raw k6 and container-resource evidence described there; absent that evidence, keep the conservative defaults above. - AI practice review adds real memory: each concurrent review sandbox may use up to
4 GiB. The default caps it at 1 concurrent sandbox; raise
SANDBOX_MAX_CONCURRENTonly with RAM to match. - Container defaults are 4 CPUs / 512 PIDs for the application server and worker, and 2 CPUs / 256
PIDs for the webhook server. Override them with the corresponding
APPLICATION_SERVER_*,APPLICATION_WORKER_*, orWEBHOOK_SERVER_*limit variables. Keep PID limits on roles with Docker access. HIKARI_MAXIMUM_POOL_SIZEdefaults to 20. Provision at least that value multiplied by the total JVM replica count, plus capacity for migrations and operator access.- A host in the supported matrix, plus
gitandopenssl. - A DNS A record for your hostname pointing at the host, with ports 80 and 443 reachable from the internet (Let's Encrypt HTTP-01, OAuth callbacks, webhooks).
- Outbound HTTPS to
ghcr.ioanddocker.iofor images,github.comandapi.github.comfor the release lock, andfulcio.sigstore.dev,rekor.sigstore.dev, andtuf-repo-cdn.sigstore.devfor keyless signature verification. - The Node.js version pinned in
package.json#devEngines.runtime, GitHub CLI authenticated withgh auth login, and Cosign for release-lock verification.
1. Get the files
Check out the newest version from the releases page:
VERSION=0.74.0 # the release you are installing, without the leading "v"
sudo git clone --depth 1 --branch "v$VERSION" \
https://github.com/ls1intum/Hephaestus.git /opt/hephaestus
sudo chown -R "$USER" /opt/hephaestus
cd /opt/hephaestus
cd docker/self-host
./setup.sh
cd ../..
node scripts/prepare-release-lock.ts "v$VERSION"
cd docker/self-host
setup.sh creates or updates .env with mode 0600 and generates the database password,
credential-encryption key, OAuth state-cookie key, and webhook secret. It does not replace non-empty
values and never prints a secret.
Everything below runs from /opt/hephaestus/docker/self-host. After step 2 supplies the remaining
required values, docker compose config renders the effective configuration. Before then it exits
at the first missing required variable, as does every other docker compose subcommand in this
directory; see Troubleshooting.
2. Configure .env
Open .env and fill in the remaining REQUIRED values. setup.sh manages the four internal
secrets listed above; do not replace them manually.
| Variable | What / how |
|---|---|
APP_HOSTNAME | Your public hostname, e.g. hephaestus.example.com |
ACME_EMAIL | Let's Encrypt expiry notices |
GH_OAUTH_CLIENT_ID / _SECRET | From step 3 |
HEPHAESTUS_AUTH_BOOTSTRAP_ADMINS | From step 4 — set it before first boot |
:::danger Back up both encryption keys
HEPHAESTUS_SECURITY_ENCRYPTION_KEY protects legacy encrypted values and the JWT signing key.
HEPHAESTUS_SECURITY_CREDENTIAL_ENCRYPTION_KEY protects integration credentials. Losing either key
makes the corresponding database values unreadable. Store both with the database backup. Rotate the
credential key only with the credential-key rotation procedure.
:::
The other generated values also have lifecycle consequences: changing the database password does not
update an initialized PostgreSQL volume, rotating the state-cookie key invalidates OAuth flows already
in progress, and rotating the webhook secret requires the same change at every configured provider.
Back up .env with the database as described in Backup & Restore.
3. Create the GitHub OAuth App (login — mandatory)
Without a login provider the instance boots but shows no sign-in button. GitHub login needs a plain OAuth App (this is not the same thing as the GitHub App used for data access — see GitHub integration):
- github.com/settings/developers → New OAuth App (or under your org's settings).
- Homepage URL:
https://<APP_HOSTNAME> - Authorization callback URL:
https://<APP_HOSTNAME>/api/login/oauth2/code/github— exact string, including/api, no trailing slash. - Create a client secret; put both values into
.envasGH_OAUTH_CLIENT_ID/GH_OAUTH_CLIENT_SECRET.
GitLab login works the same way and can be used instead of (or next to) GitHub — see the GitLab rollout bundle.
4. First admin — before first boot
The admin UI is admin-gated, so the first instance admin must come from operator
config, not the UI. Set in .env:
HEPHAESTUS_AUTH_BOOTSTRAP_ADMINS=github:@your-github-username
- Format: comma-separated
<provider>:@<username>or<provider>:<numeric-id>. - On public github.com prefer the numeric id (
github:1234567, fromhttps://api.github.com/users/<login>) — abandoned handles can be reclaimed by others. - Promotion happens on sign-in, is idempotent, and never demotes. If you forgot to set
it, add it and restart — no reinstall needed. There is also a break-glass
HEPHAESTUS_AUTH_BOOTSTRAP_TOKEN; details in the auth runbook.
5. First boot
docker compose --env-file .env --env-file release-lock.env up -d
docker compose --env-file .env --env-file release-lock.env logs -f application-server
First start pulls ~2 GB of images and runs all database migrations; expect a few minutes. Then:
- Open
https://<APP_HOSTNAME>— the login page should offer Sign in with GitHub. - Sign in as the bootstrap admin. You should see instance-admin navigation.
- Create your first workspace and connect it to GitHub.
6. Connect GitHub data
Login alone syncs nothing. Two things make data flow, both covered step-by-step in GitHub integration:
- Repository access — either a PAT pasted into the workspace UI (simplest) or a GitHub App (needed for AI-review feedback posted back to GitHub, better rate limits).
- Webhooks — Hephaestus does not auto-register GitHub webhooks (unlike GitLab).
Configure the webhook on your GitHub App or create an org/repo webhook manually,
pointing at
https://<APP_HOSTNAME>/webhooks/githubwith yourWEBHOOK_SECRET. Without it the instance only sees changes on the hourly sync poll, and nothing warns you.
7. Optional integrations
All off by default. The compose file already passes their variables through — enable them
in .env and docker compose up -d. Each has its variable reference in
Integrations & Reference Deployment:
| Add-on | What it gives you | Where |
|---|---|---|
| GitLab | GitLab login, workspaces, and sync (webhooks auto-register here) | GitLab bundle |
| Slack | Mentor in Slack, channel ingestion, digests | Slack app setup |
| Outline | Wiki as a content source for reviews | Outline integration |
| AI practice review | Automated PR feedback from the coding agent | Practice review |
| An AI provider | The model everything AI runs on — required by the row above | Connect an AI Provider |
AI practice review needs two things, not one: the environment bundle above (AGENT_ENABLED and
GIT_CHECKOUT_ENABLED, set together on every role that submits or claims work; the job queue itself runs
on PostgreSQL, so this bundle needs no NATS setup), and a model that the workspace can actually run
on. Setting that model up — registering a provider connection, testing it, pricing the model, sharing it,
and binding it to the practice reviews purpose — is Connect an AI Provider, and
nothing reviews anything until it is done. With the flags on but nothing bound, webhook-driven reviews are
skipped with only a DEBUG log line, since an unbound workspace is a normal state and one line per event
would be noise. The manual dev trigger answers with the reason, so use it to tell "unbound" apart from
"broken".
8. Legal pages (public instances)
:::caution Public instances need legal pages
/imprint and /privacy serve a red "not configured" fallback until you provide your own.
For operators in Germany that is a § 5 DDG / Art. 13 GDPR obligation before opening the
instance to users. See Legal Pages.
:::
Upgrades
Any release upgrades directly to any later one — no intermediate steps. Support window and what a version number promises: Compatibility Policy.
# 1. Back up first — see Backup & Restore.
VERSION=<new version>
cd /opt/hephaestus
sudo git fetch --depth 1 origin tag "v$VERSION" && sudo git checkout "v$VERSION"
node scripts/prepare-release-lock.ts "v$VERSION"
cd docker/self-host && docker compose --env-file .env --env-file release-lock.env pull && docker compose --env-file .env --env-file release-lock.env up -d
Compare your .env against the new release's .env.example for added variables. When a release
changes the PostgreSQL major version, follow its entry in
MIGRATION.md before starting the new
stack.
Rollback
Restore a backup compatible with the target release, then deploy that release's published lock:
VERSION=<previous version>
cd /opt/hephaestus
sudo git fetch --depth 1 origin tag "v$VERSION"
sudo git checkout "v$VERSION"
node scripts/prepare-release-lock.ts "v$VERSION"
cd docker/self-host
docker compose --env-file .env --env-file release-lock.env up -d --remove-orphans --wait
Never reconstruct an old lock or resolve its image tags.
:::warning In-place downgrades are not supported Never run an older release against a database a newer release has already migrated — restore a compatible backup instead. Set up Backup & Restore before you have data worth losing. :::
Troubleshooting
| Symptom | Cause / fix |
|---|---|
application-server exits: ProxyTrustGuard | HEPHAESTUS_TRUSTED_PROXIES is blank. The compose default is non-blank, so this means you overrode it to empty — unset your override, or set it to match reverse-proxy's IP if you changed the network. |
| Exits: encryption-key length error | HEPHAESTUS_SECURITY_ENCRYPTION_KEY must be exactly 32 printable, non-space ASCII characters. Restore the original key for an existing database. On a new installation, clear the invalid value and rerun ./setup.sh. |
| Login page has no sign-in button | No login provider configured: both GH_OAUTH_CLIENT_ID and _SECRET must be non-empty (a half-filled pair is skipped and logged at ERROR). |
| OAuth redirect loop / cookie never sticks | A proxy in front of Traefik is injecting a Domain= attribute on cookies, which browsers reject for __Host- cookies. Serve the stack directly on ports 80/443. |
| Signed in but not admin | HEPHAESTUS_AUTH_BOOTSTRAP_ADMINS didn't match. On the account's first login the server logs (INFO) the exact provider subject username it saw — copy that into the allowlist and restart. (For an account that already existed, check that log line from its first sign-in, or use the numeric id.) |
GitHub webhook deliveries show 401 | WEBHOOK_SECRET on GitHub differs from .env — must be byte-identical. |
| Data appears only ~hourly | No GitHub webhook configured (see step 6). |
docker compose down/logs aborts with "variable is missing" | Required .env values are validated for every subcommand. Fill them (dummy values are fine) before teardown. |