Skip to main content

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:

ServiceWhat it is
reverse-proxyTraefik — TLS via Let's Encrypt, routes /, /api, /webhooks
webappReact SPA served by nginx
application-serverSpring Boot API + in-process job worker
webhook-serverSame image, webhook profile — keeps receiving events while the app restarts
postgresPostgreSQL 17 + pg_partman (custom image — vanilla postgres:17 is not a drop-in)
nats-serverNATS JetStream event buffer
release-pin-fetcherOne-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: set APPLICATION_SERVER_MEM_LIMIT and WEBHOOK_SERVER_MEM_LIMIT in .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_CONCURRENT only with RAM to match.
  • 64-bit Linux with Docker Engine ≥ 24 and Docker Compose ≥ 2.24.4 (docker compose version), plus git.
  • 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.io and docker.io (images — NATS and Traefik come from Docker Hub), github.com (release pin), api.github.com, and fulcio.sigstore.dev, rekor.sigstore.dev, tuf-repo-cdn.sigstore.dev (the release-pin-fetcher verifies 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 though github.com is 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:

VariableWhat / how
APP_HOSTNAMEYour public hostname, e.g. hephaestus.example.com
IMAGE_TAGThe same $VERSION you fetched above — an exact release, never latest
ACME_EMAILLet's Encrypt expiry notices
POSTGRES_PASSWORDopenssl rand -hex 16
HEPHAESTUS_SECURITY_ENCRYPTION_KEYopenssl rand -base64 24 | cut -c1-32 — exactly 32 chars (see the warning below)
HEPHAESTUS_AUTH_STATE_COOKIE_KEYopenssl rand -base64 32
WEBHOOK_SECRETopenssl rand -hex 32 — you'll enter the same value on GitHub later
GH_OAUTH_CLIENT_ID / _SECRETFrom step 3
HEPHAESTUS_AUTH_BOOTSTRAP_ADMINSFrom 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):

  1. github.com/settings/developersNew OAuth App (or under your org's settings).
  2. Homepage URL: https://<APP_HOSTNAME>
  3. Authorization callback URL: https://<APP_HOSTNAME>/api/login/oauth2/code/github — exact string, including /api, no trailing slash.
  4. Create a client secret; put both values into .env as GH_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, from https://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:

  1. Open https://<APP_HOSTNAME> — the login page should offer Sign in with GitHub.
  2. Sign in as the bootstrap admin. You should see instance-admin navigation.
  3. 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:

  1. 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).
  2. 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/github with your WEBHOOK_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-onWhat it gives youWhere
GitLabGitLab login, workspaces, and sync (webhooks auto-register here)GitLab bundle
SlackMentor in Slack, channel ingestion, digestsSlack app setup
OutlineWiki as a content source for reviewsOutline integration
AI practice reviewAutomated PR feedback from the coding agentPractice review bundle
An AI providerThe model everything AI runs on — required by the row aboveConnect 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".

:::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

SymptomCause / fix
application-server exits: ProxyTrustGuardHEPHAESTUS_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 errorHEPHAESTUS_SECURITY_ENCRYPTION_KEY must be exactly 32 characters — regenerate with the command above.
Login page has no sign-in buttonNo 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 sticksA 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 adminHEPHAESTUS_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 401WEBHOOK_SECRET on GitHub differs from .env — must be byte-identical.
Data appears only ~hourlyNo GitHub webhook configured (see step 6).
release-pin-fetcher failsIMAGE_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.