Deploy
Install with Helm
Install self-hosted MergeHarbor on Kubernetes (EKS, GKE, AKS, OpenShift) with the Helm chart, an external PostgreSQL database, your own registry and TLS.
The Helm chart installs the console, API, workers and review agent. PostgreSQL is yours: a managed service such as Amazon RDS or Aurora, Google Cloud SQL or Azure Database for PostgreSQL, or a database you run. The chart works on any conformant Kubernetes 1.27+ cluster, including EKS, GKE, AKS and OpenShift.
Before you start
- PostgreSQL 15 or later with the
vector(pgvector) extension 0.8.0 or later available (code search stores 16-bit vectors and uses iterative index scans; the migration stops with instructions on an older version). The first migration creates the extension. On Azure, allow-listvector; RDS, Aurora and Cloud SQL include it. - Embeddings: set
embedder.enabled: trueto run MergeHarbor’s CPU embeddings server in the cluster (no GPU, no download, no per-token cost), or bring any endpoint that speaks the OpenAI/v1/embeddingsAPI, such as Ollama, vLLM or the OpenAI API. - An ingress controller and a TLS certificate: cert-manager, or a Secret you create.
- A license file (
mergeharbor.lic) from MergeHarbor.
1. Create the secrets
kubectl create namespace mergeharbor
AGENT_SHARED_SECRET="$(openssl rand -hex 32)"
kubectl -n mergeharbor create secret generic mergeharbor-core \
--from-literal=DATABASE_URL='postgres://mergeharbor:<password>@db.internal:5432/mergeharbor?sslmode=verify-full' \
--from-literal=CONTEXT_SECRET="$(openssl rand -hex 32)" \
--from-literal=AGENT_SHARED_SECRET="$AGENT_SHARED_SECRET" \
--from-literal=ADMIN_TOKEN="$(openssl rand -hex 32)" \
--from-literal=SECRETBOX_KEYS="k1:$(openssl rand -base64 32)" \
--from-literal=AUTH_PEPPER="$(openssl rand -hex 32)"
kubectl -n mergeharbor create secret generic mergeharbor-agent \
--from-literal=AGENT_SHARED_SECRET="$AGENT_SHARED_SECRET" \
--from-literal=ANTHROPIC_API_KEY='<key>' # or OPENAI_API_KEY / SELFHOSTED_LLM_API_KEY
kubectl -n mergeharbor create secret generic mergeharbor-license \
--from-file=license.lic=./mergeharbor.lic
Add your code host’s credentials to mergeharbor-core too: for a GitHub App, GITHUB_PRIVATE_KEY and GITHUB_WEBHOOK_SECRET. See GitHub and the other code host guides. With External Secrets Operator, set externalSecrets.enabled: true to sync these Secrets from AWS Secrets Manager or Vault instead.
2. Write a values file
profile: small # small | medium | large
ingress:
className: "" # your IngressClass; empty = the cluster default
host: mergeharbor.example.com
annotations:
cert-manager.io/cluster-issuer: letsencrypt
tls: { enabled: true } # secret <release>-tls, or set tls.secretName
embedder:
enabled: true # in-cluster embeddings; or set config.EMBED_URL, EMBED_MODEL and EMBED_DIM for your own server
config:
MODEL_PROVIDERS: anthropic # the providers the agent has credentials for
license:
existingSecret: mergeharbor-license
agent:
llmBackend: anthropic
The chart includes this as examples/minimal-values.yaml.
Changing EMBED_URL, EMBED_MODEL or EMBED_DIM later is safe: code search never
compares vectors from two models. Every repository is re-embedded in the background, and
until it is, its semantic search results are incomplete (exact-text search is unaffected).
3. Install
helm install mergeharbor oci://ghcr.io/mergeharbor/charts/mergeharbor \
--version <version> -n mergeharbor -f values.yaml
On the first install, pods apply the database schema themselves, one at a time.
4. Create the first administrator
If you sign in through your identity provider, set config.INSTANCE_ADMIN_EMAILS to your address and sign in. Otherwise, create the first organization and its owner with the break-glass admin API, then turn the token off again:
-
Turn the token on and name yourself an instance administrator:
helm upgrade mergeharbor oci://ghcr.io/mergeharbor/charts/mergeharbor --version <version> -n mergeharbor \ -f values.yaml --set config.ADMIN_TOKEN_ENABLED=true --set config.INSTANCE_ADMIN_EMAILS=you@example.com -
Forward the API’s internal admin port, which no Service exposes:
kubectl -n mergeharbor port-forward deploy/mergeharbor-api 8082:8082 & ADMIN_TOKEN="$(kubectl -n mergeharbor get secret mergeharbor-core -o jsonpath='{.data.ADMIN_TOKEN}' | base64 -d)" -
Create the organization, then invite its owner:
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" -d '{"name":"Example Corp"}' http://127.0.0.1:8082/api/v1/orgs curl -s -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" -d '{"email":"you@example.com","role_id":"owner"}' \ http://127.0.0.1:8082/api/v1/orgs/<handle>/invitationsUse the
handlereturned by the first call in the second one. -
Open the
accept_urlfrom the second response and choose a password. -
Run
helm upgradeagain withoutconfig.ADMIN_TOKEN_ENABLED=true, and keepINSTANCE_ADMIN_EMAILSin your values file.
Common settings
| Value | Default | Purpose |
|---|---|---|
profile |
medium |
Replicas, requests and limits, autoscaling and disruption budgets for every component |
image.registry |
ghcr.io/mergeharbor |
Pull from your own registry or mirror |
image.tag |
the chart’s app version | Pin a release |
image.pullSecrets |
[] |
For example [{name: regcred}]. Applied to every pod, including the migration Job and sandbox pods. |
ingress.className, ingress.host, ingress.annotations |
"", example host, {} |
Ingress. API paths go to the API; everything else goes to the console. |
ingress.tls.enabled, ingress.tls.secretName |
true, <release>-tls |
TLS certificate Secret |
license.existingSecret, license.key |
"", license.lic |
The license Secret, mounted read-only. A renewed Secret applies within about a minute. |
config.MODEL_PROVIDERS |
anthropic |
The providers routing may use |
config.S3_ENABLED |
"false" |
Optional review archives in S3-compatible storage |
airGapped |
false |
See Air-gapped install |
openshift |
false |
Run under OpenShift’s restricted SCC |
api, agent, console, cells[] |
from profile |
Override replicas, resources, hpa and pdb for one component |
Prometheus ServiceMonitor and PrometheusRule objects are off by default; turn on serviceMonitor.enabled and prometheusRule.enabled if you run the Prometheus Operator.
Burst indexing capacity
Onboarding many repositories at once is mostly first-time indexing. With KEDA installed (keda.enabled: true), burst.enabled: true adds a second worker StatefulSet per cell, <release>-worker-<cell>-burst. It scales from 0 up to burst.maxReplicas while the cell has more first-time indexing work that can run than your regular workers take, and back to 0 about 15 minutes after that drains.
Burst pods only run first-time indexing. Every repository keeps its regular worker for reviews, search and incremental indexing, so reviews are not slowed down or moved while a burst runs, and nothing is handed back when burst pods stop. A burst pod keeps its own copy of the repositories it indexed, and its volume is kept when it scales in, so the next burst starts warm.
| Value | Default | Purpose |
|---|---|---|
burst.nodeSelector, burst.tolerations, burst.nodeAffinity |
none | Put burst pods on spot or other cheap nodes, for example karpenter.sh/capacity-type: spot |
burst.maxReplicas |
10 |
Upper bound on burst pods per cell |
burst.drainTimeout |
1m |
Shutdown budget. The grace period is this + 40 s, so 100 s fits a spot interruption notice. |
burst.resources, burst.mirrors |
the cell’s | Pod size and volume |
burst.keda.batchesPerPod |
8 |
Index batches one burst pod runs at once (its INDEX_WORKERS) |
burst.keda.coreSlots |
0 |
First-time index batches your regular workers run at once (workers × INDEX_WORKERS); burst pods cover only the rest |
burst.keda.orgConcurrency |
INDEX_ORG_CONCURRENCY (16) |
One organization’s batches that can run at once; a single organization’s wave never asks for more pods than this allows |
burst.keda.databaseUrlKey |
"" (the cell’s database URL) |
Core-secret key with a read-only database URL for KEDA’s query |
cells[].burst overrides these values for one cell. Volumes kept at zero replicas are still billed. If a burst pod is reclaimed without warning, its unfinished batches run again on another worker within about two minutes. Details and trade-offs: docs/SCALE.md, “Burst indexing capacity (Kubernetes)”.
Model providers
| Provider | Settings |
|---|---|
| Anthropic API | agent.llmBackend: anthropic, ANTHROPIC_API_KEY in mergeharbor-agent, config.MODEL_PROVIDERS: anthropic |
| Amazon Bedrock | agent.llmBackend: bedrock, agent.awsRegion, an IAM role for the agent’s service account, config.MODEL_PROVIDERS: bedrock |
| Your own model server (vLLM, Ollama, TGI) | agent.selfHostedLLM.baseUrl, a priced entry in agent.modelRegistry, agent.reviewModel and agent.summaryModel set to its id, config.MODEL_PROVIDERS: openai_compatible |
Models without prices in the registry are never chosen for routing. Price a self-hosted model at your effective GPU cost per million tokens (0 is allowed). values.yaml has a complete modelRegistry example.
OpenShift
Set openshift: true. Pods then run with the namespace’s arbitrary user and group IDs: the chart sets no runAsUser, runAsGroup or fsGroup. They still run as non-root, with a read-only root filesystem, all capabilities dropped and the RuntimeDefault seccomp profile, so they work under the restricted-v2 SCC. The OpenShift router turns the Ingress into a Route: set ingress.className: openshift-default and the annotation route.openshift.io/termination: edge. The chart’s ci/openshift-values.yaml is a complete example.
Proof runs (the optional sandbox) need gVisor and a fixed sandbox user, so on OpenShift they need a custom SCC and are not covered by restricted-v2.
GKE and AKS
Use the managed PostgreSQL service (Cloud SQL for PostgreSQL, or Azure Database for PostgreSQL Flexible Server with the vector extension allow-listed), reached over private networking. Use the cloud’s ingress class, or ingress-nginx, with cert-manager or your own certificate. For object storage, any S3-compatible endpoint works through config.S3_ENDPOINT and config.S3_PATH_STYLE. Terraform modules are provided for AWS only.
AWS with Terraform
deploy/terraform/aws creates a VPC, an EKS cluster with node groups for workers (and optionally sandboxes), Aurora PostgreSQL, an S3 bucket encrypted with KMS, Secrets Manager entries, and IAM roles for the chart’s service accounts. It does not install the chart.
cd deploy/terraform/aws
terraform init
terraform apply -var region=us-east-1 -var environment=prod -var public_url=https://mergeharbor.example.com
Then install External Secrets Operator using the irsa_role_arns output, and install the chart with externalSecrets.enabled: true, the service account role annotations from the outputs, config.S3_BUCKET, ingress.className: alb and your host.
Network policy
networkPolicy.enabled (on by default) limits traffic between MergeHarbor’s pods. For production, also turn on networkPolicy.egress.enabled. Then list in networkPolicy.egress.allowCIDRs the private ranges the API and workers must reach: database, code hosts, identity provider and model servers. Cloud metadata addresses are always blocked.