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 18 + pg_partman (custom image — vanilla postgres:18 is not a drop-in)
nats-serverNATS JetStream event buffer

Requirements

Supported host matrix

Hephaestus supports the blessed single-host stack on the following native (not emulated) hosts:

DistributionCPU architecture
Ubuntu 24.04 LTSx86-64 (amd64)
Ubuntu 24.04 LTSARMv8 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: 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).
  • 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_CONCURRENT only 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_*, or WEBHOOK_SERVER_* limit variables. Keep PID limits on roles with Docker access.
  • HIKARI_MAXIMUM_POOL_SIZE defaults 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 git and openssl.
  • 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 for images, github.com and api.github.com for the release lock, and fulcio.sigstore.dev, rekor.sigstore.dev, and tuf-repo-cdn.sigstore.dev for keyless signature verification.
  • The Node.js version pinned in package.json#devEngines.runtime, GitHub CLI authenticated with gh 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.

VariableWhat / how
APP_HOSTNAMEYour public hostname, e.g. hephaestus.example.com
ACME_EMAILLet's Encrypt expiry notices
GH_OAUTH_CLIENT_ID / _SECRETFrom step 3
HEPHAESTUS_AUTH_BOOTSTRAP_ADMINSFrom 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):

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

  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
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 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".

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

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 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 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).
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.