Self-Hosting & Air-Gapped

Where each open-source component lives, how the local stack fits together, and step-by-step instructions to deploy and verify Hanzo locally or on an air-gapped host.

This guide covers how to deploy, wire, and smoke-test a local or air-gapped Hanzo installation from clean source and prebuilt binaries, with no connection to api.hanzo.ai.

Where everything lives

The hosted cloud (api.hanzo.ai) and a self-hosted installation are built from the same open-source repositories under github.com/hanzoai. Each repository owns one distinct boundary:

ComponentRepositoryRuntimeDefault local portsWhat runs offline today
Cloud Corehanzoai/cloudSingle static Go binary (CGO_ENABLED=0)127.0.0.1:8080 (HTTP + console), 127.0.0.1:9090 (/healthz, /readyz, /metrics), 127.0.0.1:9653 (ZAP)Encrypted SQLite document + KV store (/v1/base), durable task queue (/v1/tasks), functions (/v1/functions), embedded KMS (/v1/kms), MCP JSON-RPC (/mcp), OpenAPI spec (/v1/openapi.json)
Model Enginehanzoai/engineSingle Rust binary (hanzoai or hanzo-engine)127.0.0.1:1234 (HTTP + /ui)Local LLM, vision, audio, and embedding inference (/v1/models, /v1/chat/completions, /v1/embeddings, Anthropic /v1/messages) plus a built-in browser chat UI at /ui
Identity (IAM)hanzoai/iamSingle Go binary + embedded SQLite127.0.0.1:8000 (HTTP)OpenID Connect discovery, OAuth2 PKCE, JWKS (/v1/iam/.well-known/jwks), SCIM 2.0, and org JWTs for multi-tenant cloud and /v1/kms secrets
Agentic Bothanzoai/botNode.js 22+ runtime (hanzo-bot / @hanzo/bot)ws://127.0.0.1:18789 (gateway)Local multi-channel & CLI autonomous agent runtime wired to local engine (:1234) and local cloud (:8080)
AI Routerhanzoai/aiGo binary (cmd/aid) or ghcr.io/hanzoai/ai:8000 (configurable)Multi-model routing, metering, and RAG across local engine nodes or upstream providers (optional for single-node local setups)
Chat Clienthanzoai/chatStatic Vite + React SPA (@hanzo/chat)localhost:3090 (dev) or any static file hostMulti-conversation web workspace pointed at a local stack via VITE_HANZO_API and VITE_HANZO_IAM (optional; engine already includes an embedded /ui)
CLIhanzoai/cliSingle Rust binary (hanzo)Client onlyModel pulls (hanzo pull), local engine launcher (hanzo engine serve), terminal coding agent, and /v1 + MCP client

Start small. You do not need Kubernetes, PostgreSQL, Redis, or all seven repositories to validate an installation. For a clean local or air-gapped test, start with Cloud Core (hanzoai/cloud) and Model Engine (hanzoai/engine) — two static binaries backed by local files — and optionally attach Agentic Bot (hanzoai/bot) or IAM (hanzoai/iam).

Local architecture

Browser / OpenAI SDK / hanzo-bot / curl
   │
   ├──► 127.0.0.1:1234  hanzoai/engine   (LLM + embeddings: /v1/chat/completions, /v1/models, /ui)
   │         │
   │         └──► ~/.cache/huggingface/hub  (Model snapshots populated by `hanzo pull`)
   │
   ├──► 127.0.0.1:8080  hanzoai/cloud    (Data, queues, functions, KMS, MCP: /v1/base, /v1/tasks, /mcp)
   │         │
   │         └──► .dev/data/             (SQLCipher-encrypted SQLite files + .dev/master.key)
   │
   ├──► 127.0.0.1:18789 hanzoai/bot      (Optional local agentic gateway wired to :1234 + :8080)
   │
   └──► 127.0.0.1:8000  hanzoai/iam      (Optional OIDC + JWKS issuer for org JWTs & /v1/kms)

1

Deploy & test Cloud Core (hanzoai/cloud)

Prerequisites: Go 1.26.5+ and openssl (macOS or Linux; no C compiler, Docker, or Rust toolchain required).

Build and start

git clone https://github.com/hanzoai/cloud
cd cloud
make dev

What make dev does on first boot:

  1. Compiles ./cloud with CGO_ENABLED=0 (pure-Go SQLite + SQLCipher encryption, zero C dependencies).
  2. Generates a 32-byte encryption key at .dev/master.key (0600) and stores encrypted state under .dev/data.
  3. Binds loopback only (127.0.0.1:8080 for HTTP and the embedded console, 127.0.0.1:9090 for health/metrics, 127.0.0.1:9653 for ZAP) and enables staged local functions (CLOUD_ENABLE_STAGED=functions).

To run without make (for example in a systemd unit or air-gapped startup script):

CGO_ENABLED=0 go build -o cloud ./cmd/cloud
mkdir -p .dev && (umask 077 && openssl rand -base64 32 > .dev/master.key)
CLOUD_LISTEN=127.0.0.1:8080 \
  CLOUD_DATA_DIR=.dev/data \
  CLOUD_ENABLE_STAGED=functions \
  CLOUD_KMS_MASTER_KEY_REF="$(cat .dev/master.key)" \
  ./cloud

Smoke-test the installation

From a second terminal, run these four checks to confirm health, encrypted storage, MCP tool discovery, and the OpenAPI contract:

# 1. Liveness check (answers on port 9090)
curl -sS http://127.0.0.1:9090/healthz

# 2. Write a JSON document to the embedded SQLCipher store (/v1/base)
curl -sS -X POST http://127.0.0.1:8080/v1/base/collections/notes \
  -H "Content-Type: application/json" \
  -d '{"doc":{"title":"airgap-smoke-test"}}'

# 3. Read the collection back
curl -sS http://127.0.0.1:8080/v1/base/collections/notes

# 4. Verify the MCP JSON-RPC server exposes the registered tools
curl -sS -X POST http://127.0.0.1:8080/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Expected responses:

  • /healthz returns {"status":"ok"}.
  • POST /v1/base/collections/notes returns the stored record with generated id and timestamps.
  • GET /v1/base/collections/notes returns {"records":[...]} containing your document.
  • POST /mcp returns "tools":[...] built from the exact same typed operation registry as http://127.0.0.1:8080/v1/openapi.json.

Loopback & unauthenticated namespace: Without an Authorization: Bearer <jwt> header, cloud writes to a dedicated local namespace so single-developer testing works with zero setup. Never change CLOUD_LISTEN from 127.0.0.1 to 0.0.0.0 until you place Hanzo IAM in front of it. Also keep .dev/master.key safe — encrypted SQLite stores can only be opened by the key that created them (or set CLOUD_DEV_UNENCRYPTED=1 for unencrypted scratch runs).


2

Deploy & test local model inference (hanzoai/engine)

hanzoai/engine serves open-weight models (Zen, Qwen, Llama, Gemma, DeepSeek, Mistral) over OpenAI-compatible /v1/chat/completions and /v1/embeddings, Anthropic-compatible /v1/messages, plus a self-contained web UI at /ui.

Download or build the binary

Release tarballs are published at github.com/hanzoai/engine/releases/latest for macOS (arm64 / amd64 with Metal) and Linux (amd64 / arm64 CPU, with cosign signatures):

curl -fL https://github.com/hanzoai/engine/releases/latest/download/hanzoai-macos-arm64.tar.gz | tar xz
curl -fL https://github.com/hanzoai/engine/releases/latest/download/hanzoai-linux-amd64.tar.gz | tar xz

Requires Rust 1.88 or newer (install.sh auto-detects CUDA, Metal, or CPU features and installs hanzo-engine into ~/.cargo/bin):

curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/hanzoai/engine/main/install.sh | sh

Pull and start a local model (zen-nano, ~1.2 GB RAM)

With the hanzo CLI (curl -fsSL https://hanzo.sh | sh) and hanzo-engine installed, pull any open-weight model into ~/.cache/huggingface/hub and serve it locally:

# 1. Pull model weights locally (resolves short aliases or any <org>/<model> ID)
hanzo pull zen-nano

# 2. List locally cached models
hanzo engine list

# 3. Serve on http://127.0.0.1:1234/v1 (with built-in browser chat UI at /ui)
hanzo engine serve zen-nano -- --host 127.0.0.1 -p 1234 --no-advertise

(If you extracted the standalone ./hanzoai release binary without the hanzo CLI, run ./hanzoai --serve-ip 127.0.0.1 --port 1234 run -m zenlm/zen-nano-0.6b.)

Smoke-test inference

# 1. Confirm the model is loaded
curl -sS http://127.0.0.1:1234/v1/models

# 2. Run a local completion (standard OpenAI wire format)
curl -sS http://127.0.0.1:1234/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "zenlm/zen-nano-0.6b",
    "messages": [{"role": "user", "content": "Reply with the single word pong."}],
    "max_tokens": 256
  }'

Any OpenAI-compatible SDK or agent can now use base_url="http://127.0.0.1:1234/v1" with any non-empty api_key string.


3

Deploy & test the local Agentic Bot (hanzoai/bot)

hanzoai/bot (@hanzo/bot / hanzo-bot) runs an autonomous tool-using agent gateway locally against your hanzo-engine (:1234) and cloud (:8080) endpoints — with no external API keys required.

# 1. Point a minimal config at your local engine (:1234)
mkdir -p ~/.hanzo-local-bot/workspace
cat > ~/.hanzo-local-bot/bot.json <<'EOF'
{
  "agents": {
    "defaults": {
      "workspace": "~/.hanzo-local-bot/workspace",
      "model": { "primary": "local-engine/zenlm/zen-nano-0.6b" }
    }
  },
  "models": {
    "mode": "replace",
    "providers": {
      "local-engine": {
        "baseUrl": "http://127.0.0.1:1234/v1",
        "apiKey": "local",
        "api": "openai-completions",
        "models": [
          {
            "id": "zenlm/zen-nano-0.6b",
            "name": "Zen Nano 0.6B (Local)",
            "reasoning": false,
            "input": ["text"],
            "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
            "contextWindow": 32768,
            "maxTokens": 4096
          }
        ]
      }
    }
  }
}
EOF

# 2. Start the local gateway (Terminal A)
BOT_SKIP_CHANNELS=1 BOT_STATE_DIR=~/.hanzo-local-bot \
  BOT_CONFIG_PATH=~/.hanzo-local-bot/bot.json \
  hanzo-bot gateway run --port 18789 --auth none

# 3. Run an end-to-end agent turn against local Cloud Core (:8080) (Terminal B)
BOT_STATE_DIR=~/.hanzo-local-bot BOT_CONFIG_PATH=~/.hanzo-local-bot/bot.json \
  hanzo-bot agent --session-id smoke \
  --message "Query http://127.0.0.1:8080/v1/base/collections/notes and summarize the records."

4

Running completely air-gapped (zero outbound network)

Neither cloud, engine, nor hanzo-bot phones home, requires a license server, or makes background telemetry calls. Once model weights are cached via hanzo pull (or copied into a local directory), hanzo-engine resolves weights directly from disk with no special offline environment variables needed.

To stage an installation onto an air-gapped network:

Step A — On a connected staging machine

mkdir -p hanzo-airgap/{bin,weights}

# 1. Build the static cloud binary (pure Go, zero dynamic libc links)
git clone https://github.com/hanzoai/cloud
(cd cloud && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ../hanzo-airgap/bin/cloud ./cmd/cloud)

# 2. Fetch the Linux engine binary (or compile from source with CUDA for NVIDIA GPUs)
curl -fL https://github.com/hanzoai/engine/releases/latest/download/hanzoai-linux-amd64.tar.gz \
  | tar xz -C hanzo-airgap/bin

# 3. Pull model weights locally (either via `hanzo pull` into ~/.cache/huggingface/hub
#    or directly into a portable weights folder)
hanzo pull zen-nano
cp -RL ~/.cache/huggingface/hub/models--zenlm--zen-nano-0.6b/snapshots/* \
  hanzo-airgap/weights/zen-nano-0.6b

# 4. Archive for transfer
tar czf hanzo-airgap.tar.gz hanzo-airgap/

(If your policy requires compiling inside the air-gapped enclave instead of transferring binaries, run go mod vendor inside cloud and cargo vendor inside engine before archiving the source trees.)

Step B — On the air-gapped target host

tar xzf hanzo-airgap.tar.gz
cd hanzo-airgap

# 1. Start Cloud Core
mkdir -p data && (umask 077 && openssl rand -base64 32 > master.key)
CLOUD_LISTEN=127.0.0.1:8080 \
  CLOUD_DATA_DIR=./data \
  CLOUD_ENABLE_STAGED=functions \
  CLOUD_KMS_MASTER_KEY_REF="$(cat master.key)" \
  ./bin/cloud &

# 2. Start Model Engine directly from the staged weights directory
./bin/hanzoai --serve-ip 127.0.0.1 --port 1234 \
  run -m ./weights/zen-nano-0.6b &

Re-run the curl smoke tests from Sections 1 and 2 against 127.0.0.1:9090/healthz, 127.0.0.1:8080/v1/base/collections/notes, and 127.0.0.1:1234/v1/chat/completions to verify both services are healthy offline.


5

Optional: Add local Identity & KMS secrets (hanzoai/iam)

hanzoai/cloud delegates authentication to an OIDC issuer via {CLOUD_IAM_ISSUER}/v1/iam/.well-known/jwks (defaulting to http://127.0.0.1:8000). Add hanzoai/iam when you want multi-tenant org isolation or sealed secrets in /v1/kms:

git clone https://github.com/hanzoai/iam
cd iam

# Generate a local dev signing key + seed file once
mkdir -p .dev/keys
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out .dev/keys/cert-dev
printf '{"organizations":[{"owner":"admin","name":"hanzo"}],"certs":[{"owner":"admin","name":"cert-dev","cryptoAlgorithm":"RS256"}],"applications":[{"owner":"admin","name":"hanzo-dev","clientId":"hanzo-dev","organization":"hanzo","enablePassword":true}]}\n' > .dev/init_data.json

# Boot local OIDC + JWKS on 127.0.0.1:8000 (ZAP on :19653 so it can run beside cloud's :9653)
IAM_DEV_HOST_RELATIVE=1 IAM_SIGNING_KEYS=.dev/keys \
  go run . serve --http http://127.0.0.1:8000 --zap 127.0.0.1:19653 --init-data .dev/init_data.json

Verify OIDC discovery and JWKS:

curl -sS http://127.0.0.1:8000/.well-known/openid-configuration
curl -sS http://127.0.0.1:8000/v1/iam/.well-known/jwks

With a bearer token issued by that local IAM (owner claim matching <org>), /v1/kms seals and unwraps secrets under .dev/data/orgs/<org>/kms.db:

curl -sS -X POST http://127.0.0.1:8080/v1/kms/orgs/<org>/secrets \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"UPSTREAM_KEY","env":"dev","value":"example-secret"}'

curl -sS "http://127.0.0.1:8080/v1/kms/orgs/<org>/secrets/UPSTREAM_KEY?env=dev" \
  -H "Authorization: Bearer $TOKEN"

Optional web chat client (hanzoai/chat)

If you want the full multi-project web chat workspace (@hanzo/chat) in addition to hanzo-engine's built-in /ui, point its Vite build at your local API and IAM origins:

git clone https://github.com/hanzoai/chat
cd chat
pnpm install --no-frozen-lockfile
VITE_HANZO_API=http://127.0.0.1:8080 VITE_HANZO_IAM=http://127.0.0.1:8000 pnpm dev

This serves the web client on http://localhost:3090.


Known current boundaries

To avoid surprises during evaluation, note three active boundaries in the current open-source cut:

  1. CLI loopback host (hanzoai/cloud#4): Running hanzo host serve or hanzo network use local in the hanzo CLI currently expects a separate host helper binary and stops with no local cloud host. Start cloud directly via make dev or ./cloud and call its HTTP (:8080) or MCP (/mcp) endpoints directly.
  2. /v1/base replaces standalone local /v1/kv and /v1/sql: In local cloud, document collections, key-value records, and SQLite tables all live behind /v1/base.
  3. External subsystems fail closed: Routes in cloud that proxy optional companion daemons (/v1/s3 on 127.0.0.1:9000, /v1/dns on 127.0.0.1:8443, /v1/exec on 127.0.0.1:8100) return an explicit 503 until their upstream environment variables (S3_ENDPOINT, HANZO_DNS_URL, CODE_EXEC_UPSTREAM) are configured.

Real-time assistance & support

If you hit a missing detail while standing up a local, on-prem, or air-gapped environment:

  • Discord (fastest path to core engineers): Join discord.gg/XthHQQj — post in #general or #docs for direct help from the engineers maintaining cloud, engine, and iam.
  • GitHub Issues: File reproduction steps or doc gaps directly on hanzoai/cloud, hanzoai/engine, hanzoai/iam, or hanzo-docs/docs.
  • Enterprise & air-gapped architecture onboarding: Email [email protected] for dedicated deployment assistance, custom model weights, and security review support.
Was this page useful?
Broken link or missing step? Drop it in #docs on Discord or open a PR.
Last updated Nov 11, 2026