Skip to content
Docs ·Air-gapped install

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 --insecure for 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: true to run the same server in the cluster, or point config.EMBED_URL at 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 to compose.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); set VULNDB_MAVEN_URL to 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.