Skip to content
Docs ·Install with Helm

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-list vector; RDS, Aurora and Cloud SQL include it.
  • Embeddings: set embedder.enabled: true to 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/embeddings API, 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:

  1. 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
  2. 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)"
  3. 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>/invitations

    Use the handle returned by the first call in the second one.

  4. Open the accept_url from the second response and choose a password.

  5. Run helm upgrade again without config.ADMIN_TOKEN_ENABLED=true, and keep INSTANCE_ADMIN_EMAILS in 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.