Deploy
Air-gapped install
Install self-hosted MergeHarbor in a network with no internet access, from a checksummed image bundle, with models served inside your network.
MergeHarbor can run in a network with no internet access. Every release has an air-gap bundle with all images. The license is checked offline, and MergeHarbor sends no telemetry and makes no update checks.
1. Get the bundle
Each release vX.Y.Z has two bundles: mergeharbor-airgap-X.Y.Z-amd64.tar and mergeharbor-airgap-X.Y.Z-arm64.tar, each with a .sha256 file. MergeHarbor gives licensed customers access to them. Each bundle contains:
| Path | Contents |
|---|---|
images/ |
Every image as an OCI layout, digests preserved: api, worker, agent, embedder (with its embedding model), console, mergeharbor (CLI), sandbox-runner, plus PostgreSQL and Caddy for the Compose bundle |
images.txt, manifest.json |
Name, tag, digest, platform and source of each image |
chart/ |
The Helm chart package |
self-hosted/ |
The Docker Compose bundle |
load-bundle.sh |
Verifies the bundle and loads the images |
SHA256SUMS |
Checksums of every file |
Images are signed with Sigstore cosign and ship with an SBOM, so you can check them with cosign verify before you bring them in.
2. Load the images
Copy the .tar and .sha256 files into the network. load-bundle.sh checks both checksum files before it loads anything. To check the bundle without loading it, add --verify-only.
Into a private registry (for Kubernetes). Sign in to the registry first, with skopeo login, crane auth login or docker login:
tar -xf mergeharbor-airgap-1.4.0-amd64.tar mergeharbor-airgap-1.4.0-amd64/load-bundle.sh
./mergeharbor-airgap-1.4.0-amd64/load-bundle.sh --registry registry.internal:5000/mergeharbor \
mergeharbor-airgap-1.4.0-amd64.tar
- It uses skopeo, crane or Docker, whichever it finds; skopeo and crane keep the image digests.
- Add
--insecurefor a registry with plain HTTP or a self-signed certificate. - It prints the Helm values to use.
Into the local Docker (for the single-VM Compose install, no registry needed):
./mergeharbor-airgap-1.4.0-amd64/load-bundle.sh --docker-local mergeharbor-airgap-1.4.0-amd64.tar
3. Point models at your network
With AIR_GAPPED=true, the review agent only creates in-network providers. It refuses to start if a provider’s endpoint resolves to a public address.
| Models | Settings |
|---|---|
| Your own server: vLLM, Ollama, TGI or any OpenAI-compatible API | SELFHOSTED_LLM_BASE_URL (Helm: agent.selfHostedLLM.baseUrl), plus a priced model entry (Compose: config/models.yaml; Helm: agent.modelRegistry) |
| Amazon Bedrock, through VPC endpoints | AWS_REGION. For Claude models: ANTHROPIC_BEDROCK_MANTLE_BASE_URL at a private endpoint (Helm: agent.bedrockMantleUrl). For Converse models: a bedrock-runtime interface endpoint with private DNS, or AWS_ENDPOINT_URL_BEDROCK_RUNTIME (Helm: agent.bedrockEndpointUrl). |
Open-weight models served by vLLM or Ollama are the tested air-gapped setup. Claude through Amazon Bedrock in an air-gapped network is not confirmed yet.
Set MODEL_PROVIDERS to the providers you configured, for example openai_compatible. When air-gapped, routing only uses openai_compatible, bedrock and bedrock_converse.
Embeddings must come from inside the network too:
- Compose: the bundled embeddings server has its model inside the image. Nothing to set.
- Kubernetes: set
embedder.enabled: trueto run the same server in the cluster, or pointconfig.EMBED_URLat your own embeddings server.
4. Install
Helm:
image:
registry: registry.internal:5000/mergeharbor
pullSecrets: [{ name: regcred }]
airGapped: true
config:
MODEL_PROVIDERS: openai_compatible
embedder:
enabled: true
agent:
selfHostedLLM: { baseUrl: http://vllm.models.svc:8000/v1 }
reviewModel: selfhosted/qwen-coder
summaryModel: selfhosted/qwen-coder
helm install mergeharbor ./mergeharbor-airgap-1.4.0-amd64/chart/mergeharbor-1.4.0.tgz -n mergeharbor -f values.yaml
Compose: in the bundle’s self-hosted/ directory, set AIR_GAPPED=true and TLS_MODE=internal (or files) in .env. Then run ./install.sh --offline --domain … --version 1.4.0.
5. Load the vulnerability database
Dependency checks, SBOMs and vulnerability management match against the vulnerability database, which the vulnerability service (the vuln container) keeps in its own database. An air-gapped installation can’t download the advisory feeds, so you copy the database in as a file. With AIR_GAPPED=true, feed sync and the public OSV API are off automatically. Until you import a file, dependency checks find no vulnerabilities, and public advisory pages show only what you imported.
Export on a connected machine. A connected MergeHarbor keeps advisories only for the packages its organizations use, so export a full bundle, read straight from the public advisory exports. Run it in that installation’s vulnerability service:
vuln kb export -mode full -o vulndb-full.jsonl.gz
# distributions and container images too, if you import SBOMs of images:
vuln kb export -mode full -with-os Debian,Ubuntu,Alpine -o vulndb-full.jsonl.gz
The file is gzip-compressed JSON lines. It holds every advisory record of the code ecosystems (and the distributions you name), CISA’s Known Exploited Vulnerabilities catalog, EPSS scores, the CWE list and the SPDX license list.
Move the file into the network the same way you brought in the image bundle.
Import it inside the network (the service needs no internet access for this):
-
Compose: put the file in the
vulndb/directory next tocompose.yml, then run:./compose.sh exec vuln app kb import -file /vulndb/vulndb-full.jsonl.gz -
Helm: stream the file into the vulnerability service’s pod:
kubectl -n mergeharbor exec -i deploy/mergeharbor-vuln -- /usr/local/bin/app kb import -file /dev/stdin < vulndb-full.jsonl.gz
The import prints how many records it loaded. You can run it while MergeHarbor is running.
Refresh it regularly. New advisories, fixes and exploit data appear every day. Export and import a new file at least weekly, or daily if you can. Each import updates the database in place, so a newer file simply goes over the older one, and an interrupted import can be run again.
What you get: advisories for npm, PyPI, Go, Maven, NuGet, crates.io, RubyGems, Packagist, Pub and Hex as of the bundle’s date, plus the distributions you asked for with -with-os; CISA KEV, EPSS, CWE and SPDX data as of the exporting installation’s last sync. You don’t get package health (OpenSSF Scorecard, release activity, end of life) for packages the exporting installation didn’t use, or distribution and image coverage you didn’t ask for. -mode wanted exports only the exporting installation’s own packages (plus -top-n N popular packages per ecosystem), which suits a second installation of the same organization.
What AIR_GAPPED=true does
- Egress guard. It refuses every public address for all outbound connections MergeHarbor makes on your behalf: code hosts, identity providers, connectors, webhooks, audit log streaming, notifications and MCP servers. Private addresses are allowed; cloud metadata addresses stay blocked.
- Vulnerability database. Feed sync (
VULNDB_SYNC), lookups against the public OSV API (OSV_API_URL) and package lookups (VULNDB_DEPSDEV_URL,VULNDB_EOL_URL,VULNDB_PACKAGIST_URL) are off. MergeHarbor refuses to start if you turn any of them on. Repositories without lockfiles are read from the versions written in their manifests only (transitive dependencies aren’t estimated); setVULNDB_MAVEN_URLto an in-network Maven mirror so parent and BOM POMs outside a repository can be read. Load the database from a file instead (step 5). - Startup check. MergeHarbor refuses to start when a feature that only works with public services is configured: card payments, the website bot check, the public review demo, the Slack, Microsoft Teams, Google Chat and Discord apps, and Google or Microsoft sign-in.
- Review agent. It never calls the public Anthropic, OpenAI or Gemini APIs.
Use your own identity provider over OpenID Connect or SAML, and an internal SMTP relay. With the egress network policy on, list their private ranges in networkPolicy.egress.allowCIDRs.