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 17 + pg_partman (custom image — vanilla postgres:17 is not a drop-in) |
nats-server | NATS JetStream event buffer |
release-pin-fetcher | One-shot init container verifying the release's signed agent-image digests |
Requirements
- 4 vCPUs, 8 GB RAM, 40 GB SSD recommended. 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). - 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. - 64-bit Linux with Docker Engine ≥ 24 and Docker Compose ≥ 2.24.4 (
docker compose version), plusgit. - 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.io(images — NATS and Traefik come from Docker Hub),github.com(release pin),api.github.com, andfulcio.sigstore.dev,rekor.sigstore.dev,tuf-repo-cdn.sigstore.dev(therelease-pin-fetcherverifies the pin with keyless cosign, which reaches Sigstore, not just GitHub). On an egress-filtered host, a missing Sigstore domain shows up as the release-pin step failing even thoughgithub.comis reachable.
1. Get the files
Check out the newest version from the releases page:
VERSION=0.73.2 # 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/docker/self-host
cp .env.example .env
Everything below runs from /opt/hephaestus/docker/self-host. Once step 2 has filled in every
required value, docker compose config renders the effective configuration — it is the quickest way
to confirm a variable landed where you expected. Before then it exits with the first
required variable … is missing a value, which is also how every other docker compose subcommand
behaves in this directory; see Troubleshooting.
2. Configure .env
Open .env and fill in every REQUIRED value. The short version:
| Variable | What / how |
|---|---|
APP_HOSTNAME | Your public hostname, e.g. hephaestus.example.com |
IMAGE_TAG | The same $VERSION you fetched above — an exact release, never latest |
ACME_EMAIL | Let's Encrypt expiry notices |
POSTGRES_PASSWORD | openssl rand -hex 16 |
HEPHAESTUS_SECURITY_ENCRYPTION_KEY | openssl rand -base64 24 | cut -c1-32 — exactly 32 chars (see the warning below) |
HEPHAESTUS_AUTH_STATE_COOKIE_KEY | openssl rand -base64 32 |
WEBHOOK_SECRET | openssl rand -hex 32 — you'll enter the same value on GitHub later |
GH_OAUTH_CLIENT_ID / _SECRET | From step 3 |
HEPHAESTUS_AUTH_BOOTSTRAP_ADMINS | From step 4 — set it before first boot |
:::danger Back up the encryption key
HEPHAESTUS_SECURITY_ENCRYPTION_KEY encrypts every stored provider token and seals the JWT
signing key. If you lose or change it, those tokens become unreadable and all sessions
invalidate. Store it as carefully as the database itself, and never change it after first boot.
:::
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 up -d
docker compose logs -f application-server # watch Liquibase migrations finish
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 bundle |
| 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, GIT_CHECKOUT_ENABLED, PRACTICE_REVIEW_FOR_ALL — set together; the job
queue itself runs on PostgreSQL, no NATS setup needed for this bundle), 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 detection 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"
# 2. Set IMAGE_TAG=$VERSION in docker/self-host/.env, then:
cd docker/self-host && docker compose pull && docker compose up -d
Compare your .env against the new release's .env.example for added variables. A release
that changes the bundled PostgreSQL major version needs a dump and restore, not an in-place
upgrade — such releases say so in their notes.
:::warning Downgrades are not supported
Never point an older IMAGE_TAG at a database a newer release has already migrated — restore
a 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 characters — regenerate with the command above. |
| 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). |
release-pin-fetcher fails | IMAGE_TAG is not a published release (no signed pin asset), or the host can't reach github.com or Sigstore. Signature verification is keyless, so it needs fulcio.sigstore.dev, rekor.sigstore.dev and tuf-repo-cdn.sigstore.dev as well — an egress filter that allows only GitHub fails here with the release tag perfectly valid. Use a real release tag and check outbound HTTPS to all four. |
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. |