Self-hosted install
Stand up Hekkos in your own infrastructure, end to end. Budget ~30 minutes. Hekkos is a Go server + an MCP server + PostgreSQL (with pgvector). It runs a GitHub App, so part of the setup happens on GitHub.
At a high level:
- Prepare config (secrets, base URL).
- Bring up PostgreSQL.
- Boot the server in setup mode and register the GitHub App.
- Paste the App credentials into your config and restart.
- Sign in and connect your first source.
0. Prerequisites
- A public HTTPS base URL for the deployment (e.g.
https://hekkos.example.com). GitHub must be able to reach it for webhooks and redirects. Call itAPP_BASE_URLbelow. - PostgreSQL 16+ with the
pgvectorextension. Hekkos runs migrations itself on startup — you just need an empty database it can connect to. Back it up; Hekkos does not manage backups for you. - Docker (the quickest path) or the built binaries.
1. Generate secrets
# 32-byte AES key that encrypts every secret at rest. Generate a REAL one —# a placeholder (all-identical bytes) is rejected at startup.openssl rand -hex 32 # -> MASTER_KEY
# Any strong random string; you'll give the same value to the GitHub App later.openssl rand -hex 20 # -> GITHUB_WEBHOOK_SECRET2. Write your config
Copy .env.example and fill it in. The required values for self-hosted:
| Variable | Value |
|---|---|
DATABASE_URL | postgres://user:pass@host:5432/hekkos |
MASTER_KEY | from step 1 |
GITHUB_WEBHOOK_SECRET | from step 1 |
APP_BASE_URL | your public base URL |
DEPLOY_MODE | self-hosted (the default) |
GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, GITHUB_CLIENT_ID, and
GITHUB_CLIENT_SECRET come from step 4 — leave them blank for now.
Useful optional self-hosted knobs (full annotated reference: docs/reference/configuration.md):
| Variable | Why |
|---|---|
JWT_EXPIRY | Session lifetime (default 8h). |
CONTENT_RETENTION_DAYS | Window (default 180 days) after which the retention sweep NULLs private stored doc content. Always bounded — 0/unset floors to 180 days; it does not disable retention or keep content forever. |
LLM_CALL_TIMEOUT / GITHUB_CALL_TIMEOUT | Bound external calls (Ollama 70B can be slow — the LLM default is generous). |
RIVERUI_BASIC_AUTH_USER / _PASSWORD | Enable + protect the job-queue UI at /admin/jobs/. |
LICENSE_FILE_PATH / LICENSED_ORG_NAME | Your license (see Licensing below); LICENSE_SIGNING_KEY_PATH only on a deployment that issues licenses. |
COMMUNITY_INGEST_ENABLED + CATALOG_REPO / CATALOG_GITHUB_TOKEN | Opt into community-catalogue ingestion (opens a review PR; never a live write). |
TIERED_RETRIEVAL_ENABLED | Defaults off for self-hosted (your orgs run their own embedder and get semantic chat). Set true if you want free-tier orgs on keyword search. |
OLLAMA_EMBED_MODEL | Self-hosted embedder model (default jina-embeddings-v2-base-code, code-specialized 768-dim). ollama pull it first, or set nomic-embed-text to keep the older default. |
CONTEXTUAL_RETRIEVAL_ENABLED | Defaults off. When true, the indexer adds a short LLM “situating context” per chunk (memoized, budget-metered) to lift retrieval recall — it runs only for paid/licensed orgs, so an unlicensed deployment indexes raw regardless. Adds per-chunk LLM cost on a doc’s first scan. |
CALL_EDGES_{TS,PY,RUST,RUBY,JAVA}_ENABLED + {LANG}_INDEXER_BIN + {LANG}_INDEXER_SANDBOX_CMD | Defaults off. Opt into a non-Go call graph (calls_scip* edges) for TypeScript / Python / Rust / Ruby / Java. Self-hosted uses the subprocess backend: point _BIN at the language’s SCIP indexer (scip-typescript / scip-python / rust-analyzer / scip-ruby / scip-java) and _SANDBOX_CMD at a sandbox wrapper (untrusted repo code runs under it — AGENTS.md #12). Go’s call graph is always on (in-process, no config). Fail-safe: a missing/failing indexer just yields no call edges for that language, never a broken scan. Full var list in the configuration reference. |
RERANKER_ENABLED + RERANKER_URL (+ _MODEL / _API_KEY / _TIMEOUT) | Defaults off. A cross-encoder reranker reorders the retrieval pool before the LLM (quality only, no user-facing change). It needs a served cross-encoder behind an https URL — Ollama CANNOT serve it (Ollama’s API is embeddings + generate only). D1 ships an HTTP backend only, and the zero-new-sidecar in-process ONNX path is deferred, so self-hosted defaults OFF until either that lands or you stand up a served endpoint yourself. SaaS runs a sealed Infinity CPU sidecar (michaelf34/infinity serving mixedbread-ai/mxbai-rerank-xsmall-v1, provisioned by Terraform) — a self-hosted operator can do the same (an Infinity or TEI container, a second sidecar) or point RERANKER_URL at a hosted rerank API. Fail-safe: an unset/unreachable/slow reranker degrades to RRF order, never breaks a query — and RERANKER_ENABLED=true with an empty/cleartext URL is caught loud at boot (rerank.ConfigWarning Warn + a rerank.Preflight check), not fatal. Full var list in the configuration reference. |
For the LLM, self-hosted mode uses Ollama by default; set OLLAMA_BASE_URL
to your Ollama instance (it needs models like llama3.1:8b / llama3.1:70b
pulled).
Local models. You don’t have to run Ollama yourself — the compose
local-llmprofile (docker compose --profile local-llm up) and the Helm chart’sollama.enabled=truebring up an Ollama and pull the model for you. In saas mode you can also keep hosted generation but move just the cheap classifier/diff-summarizer role to a local model withCLASSIFIER_PROVIDER=ollama. See Running LLM roles on a local model.
Set
ALERT_WEBHOOK_URLto a Slack/Discord/PagerDuty-compatible incoming webhook. Without it, operational alerts (failed jobs, revoked LLM keys, budget exhaustion) are silently dropped. The server logs a warning at startup if it’s unset.
3. Bring up PostgreSQL and the server (setup mode)
The GitHub App doesn’t exist yet, so GITHUB_APP_ID / GITHUB_APP_PRIVATE_KEY
are still blank — and the server won’t fully boot without them. To break this
chicken-and-egg, start the server in setup mode just long enough to register
the App:
- Provide a throwaway
GITHUB_APP_ID=1and a throwaway PEM (openssl genrsa 2048) so config validation passes, and start the server. - Everything that needs the real App will be non-functional, but the manifest setup route works.
docker compose up (using docker-compose.yml, which wires Postgres for you) is
the easiest way to get there.
4. Register the GitHub App
-
Visit
APP_BASE_URL/setup/github-app. -
Enter your GitHub org slug and click Continue to GitHub.
-
GitHub creates the “Hekkos” App from a manifest (permissions, events, and — importantly — the Setup URL and OAuth callback are all pre-filled for you).
-
GitHub redirects back and shows you the credentials once:
GITHUB_APP_ID=...GITHUB_WEBHOOK_SECRET=...GITHUB_APP_PRIVATE_KEY="..."GITHUB_CLIENT_ID=...GITHUB_CLIENT_SECRET=... -
Copy all five into your config (replacing the throwaway App id/key), keep your own
GITHUB_WEBHOOK_SECRETfrom step 1, and restart the server.
GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRETare what “Login with GitHub” uses. If you skip them, the App works for webhooks but no one can sign in.
5. Sign in and connect a source
- Go to
APP_BASE_URL, click Login with GitHub, authorize. - Create your org — name only; membership starts in Manual (invite-only) mode and you’re its admin. The onboarding wizard opens — four steps: Connect → Preferences → Team → Confirm.
- The wizard’s first step is Connect GitHub: install the App on the repos you want, and GitHub returns you to the wizard with the source connected. (If it doesn’t, see GitHub App configuration.)
- The initial scan starts automatically while you finish the wizard’s remaining steps — a scan strip shows the repo counts on every step, and Overview afterwards.
- Preferences shows the settled defaults (doc standards treated as recommended, pilot-first rollout; BYOK deployments also get an LLM-key row) — accept them as-is, or open a “Customize” link to adjust one. From this step onward, Finish with defaults ends the wizard early with the same safe values.
- The wizard’s Team step is where teammates come in: create an invite link (Manual mode), or — once the GitHub source is connected — switch to GitHub-org mirroring, where each member of your GitHub org joins at their first sign-in. Confirm recaps your choices (each with an edit jump), then lands you on Overview — its Setup checklist card (connect · first scan · adopt a doc standard · team) is the post-wizard guide, so anything you skipped stays one click away until all four items are done.
Teammates who sign in early. Self-hosted deployments are single-org: a second person who logs in before being invited can’t create another org — they see a screen explaining that this deployment already has an organization, with guidance on how to join (and the org’s name, if an invite is already pending for their email). Send them an invite link from the Team step or Settings → Members, or switch on mirroring and have them sign in again.
Licensing
Self-hosted Hekkos runs at free-tier limits with no license. To lift them,
set LICENSE_FILE_PATH (and LICENSED_ORG_NAME) to a license issued to you. A
missing or expired license never takes the server down — it degrades to
free-tier enforcement and logs a warning.
Production checklist
- Real
MASTER_KEY(not a placeholder), stored in your secret manager. -
APP_BASE_URLset and publicly reachable over HTTPS. - All five GitHub App vars set; login works.
-
ALERT_WEBHOOK_URLset. - PostgreSQL has automated backups; you’ve tested a restore.
- TLS terminated at your ingress (the Helm chart ships
ingress.tlsempty — supply your own). -
/metricsgated: setMETRICS_BASIC_AUTH_USER/METRICS_BASIC_AUTH_PASSWORD(or keep the endpoint network-isolated). - Smoke test:
GET APP_BASE_URL/livezreturnsok;GET /readyzreturnsready.
Kubernetes
The helm/hekkos chart deploys the server + MCP with liveness/readiness probes,
resource limits, a non-root securityContext, and secrets via a Kubernetes
Secret. Set your image tags and the required env/secret values in
values.yaml. Bring your own backed-up PostgreSQL (the chart does not ship one),
and supply TLS at the ingress. Keep replicaCount: 1 unless you’ve confirmed the
concurrent-migration lock is in place (it is) for HA.