> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xpander.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Install an air-gapped deployment

> Install xpander Air-Gapped: run the installer, mirror the images and apply the license.

Install in three steps, from a terminal with `kubectl` access to the cluster. Check the [prerequisites](https://docs.xpander.ai/self-hosted/airgap.md#prerequisites) first. Options such as an external data tier, the executor fleet and models are on [Configure](https://docs.xpander.ai/self-hosted/airgap-configure.md) and [Models and local inference](https://docs.xpander.ai/self-hosted/airgap-models.md).

## Install in three steps

**1. Receive the license files and pull token by email**

The xpander.ai team emails you:

| Item | What it is |
| - | - |
| **Two sealed license files** | `install.json` (identity: org, edition, no expiry) and `activation.json` (entitlement: term, caps, features) |
| Your **organization id** | The value for `--org` |
| The **license public key** | Used to verify the license signature |
| **A registry pull token** | For the gated `xpanderaihub` Docker Hub org: read-only, scoped to your account, revocable. It gates *delivery* only. The license is the enforcement. |

Details: [Two-File Licensing](https://docs.xpander.ai/self-hosted/licensing.md).

**2. Run the one-liner**

```bash theme={"dark"}
curl -fsSL https://charts.xpander.ai/install.sh | bash -s -- \
  --edition airgap --org <ORG_ID> \
  --install-license-file ./install.json \
  --activation-license-file ./activation.json
```

A legacy v1 install passes `--license-file ./license.json` instead of the three flags.

The wizard runs preflight checks, installs with Helm and seeds an admin login. It prompts for:

* Registry
* Chart source (OCI pull or local path)
* Data tier
* Admin email
* Organization display name
* StorageClass
* Agent code execution (`--code-execution` / `--no-code-execution`). The runner embeds dockerd, so its pod is privileged, and a restricted PodSecurity policy refuses it
* A [base domain](https://docs.xpander.ai/self-hosted/airgap-access.md#domains-ingress-and-dns) for the public hosts
* LLM provider keys, for any hosted providers your egress policy allows

Image tags come from the [release](https://docs.xpander.ai/self-hosted/airgap-upgrade.md#releases).

**If your registry has no xpander images yet, the wizard offers to [mirror them right there](#when-the-mirror-is-empty-the-wizard-offers-to-fill-it)** with the pull token from step 1, and creates ECR repositories itself. On a sealed network, use [Sealed / manual mirroring](#sealed-or-manual-mirroring).

**3. Sign in as the seeded admin**

The first install creates your organization and a bootstrap admin in your in-cluster Supabase. The closing summary prints how to sign in: see [Signing In](https://docs.xpander.ai/self-hosted/airgap-access.md#signing-in).

Non-interactive (image tags resolve from the mirror unless pinned):

```bash theme={"dark"}
bash install.sh --edition airgap \
  --org <ORG_ID> --install-license-file ./install.json --activation-license-file ./activation.json \
  --registry <your-registry>/xpander \
  --chart-path xpander-airgap-<X.Y.Z>.tgz \
  --domain xpander.acme.internal \
  --admin-email <you@company.example> --org-name "<Your Company>" --yes
```

To review every command first, `bash install.sh --edition airgap --manual` prints a personalized runbook and runs nothing: see [Advanced: review-first / manual install](https://docs.xpander.ai/self-hosted/hybrid-install.md#advanced-review-first-or-manual-install). Pipelines: [Automation & CI](https://docs.xpander.ai/self-hosted/airgap-upgrade.md#automation-and-ci).

## What happens when you run the installer

The installer runs eight phases. Steps 1 to 6 take a few minutes. Step 7 takes **10–20 minutes**. Each phase prints a `[N/8] name: ...` line for scripts to grep ([Automation & CI](https://docs.xpander.ai/self-hosted/airgap-upgrade.md#automation-and-ci)). Every hook failure **names the Job**, prints its **last 30 log lines**, and ends with a `FIX:` line and a `RERUN:` line that resumes the install.

> [accordion] The eight phases in detail
>
> **1. Resolve: load the latest release**
>
> One release manifest lists the version and SHA-256 digest of every xpander image, including `llm-gw` and `tool-repo-seed`. The installer passes those pins to Helm. To repeat a release or install offline, pass `--release-manifest ./release-manifest.json`. A missing or invalid manifest stops the run. [How releases work](https://docs.xpander.ai/self-hosted/airgap-upgrade.md#releases).
>
> **2. Checklist: preconditions verified before anything changes**
>
> The registry answers `/v2/` (401 counts as up), `agent-controller` and `deployment-manager` exist **at their pinned tags**, an RWO StorageClass exists, the image pins are in place, `--org-name` and `--admin-email` are set, and a chart source exists. Any failure stops here with nothing to clean up.
>
> **3. License: validated locally, applied as a Secret**
>
> Two `xpander-license/v2` envelopes, one `kind: install` and one `kind: activation`, bound to `--org` (a legacy v1 file: envelope, expiry and org id). They become the `xpander-license` Secret. The **Ed25519 signature** is verified in the preflight hook and again at deployment-manager boot.
>
> **4. Chart: pulled over OCI**
>
> The `xpander-airgap` chart comes from the gated OCI registry, your own mirror, or a `.tgz` via `--chart-path`.
>
> **5. Values: secrets never touch the command line**
>
> Configuration goes to a temporary values file (mode 600, removed on exit), never to the helm command line, shell history or process list.
>
> **6. Install: helm, without --atomic on purpose**
>
> The **first** install runs without `--atomic`, because `--wait` would hold the seed hooks that the api and agent-controller pods are waiting on. The smoke hook is the readiness gate. **Upgrades always run with `--atomic`**, so a failed upgrade rolls back and the previous revision keeps serving.
>
> **7. Pipeline: the cluster comes up, hook by hook (10–20 min)**
>
> The installer watches the hook pipeline live:
>
> ```
> preflight → keygen → data-checks → workloads → migrations → org-seed → catalog-seed → smoke
> ```
>
> | Stage | What it does |
> | - | - |
> | **preflight** | StorageClass, node architecture, and the license's shape, expiry and **Ed25519 signature**. A bad license fails here, once, readably |
> | **keygen** | Every key is **minted in your cluster** (15+ inter-service keys, Supabase JWTs, the admin password) into `xpander-generated-keys`. **Lookup-preserve**: existing keys are never regenerated |
> | **data-checks** | External data tier only: `pg_isready` and `redis-cli ping` **from inside the cluster**, so network problems surface by name before any workload starts |
> | **workloads** | Data tier, Supabase, control plane and UI roll out |
> | **migrations** | The **full schema is replayed** into your Supabase, one versioned Job per revision. Forward-only: rollback means restore-from-backup |
> | **org-seed** | **Your organization is created in your cluster**: the licensed org, the admin user and the vault. Idempotent |
> | **catalog-seed** | The skills catalog loads from the mirrored artifacts image |
> | **smoke** | Every enabled component is probed through its Service. Success only when all answer |
>
> **8. Summary: URLs, password, logs**
>
> The closing box prints the **derived URLs** (with a [base domain](https://docs.xpander.ai/self-hosted/airgap-access.md#domains-ingress-and-dns): `https://chat.<domain>`, `https://api.<domain>`, `https://supabase.<domain>`), the **bootstrap admin password**, and how to read it later (see [Signing in](https://docs.xpander.ai/self-hosted/airgap-access.md#signing-in)).
>
> The secret-scrubbed session log (mode 600) is under `~/.xpander/logs/`, with `latest.log` pointing at the last run.

## When the mirror is empty, the wizard offers to fill it

Choose **Mirror them now**, give the xpander pull credentials and any destination credentials, and let the copy finish. ECR repositories are created when your AWS credentials allow. Non-interactively: `--mirror now|guide|skip` (`now` needs the credentials). Sealed networks use the archive flow below.

## Sealed or manual mirroring

Mirror with the current `image_sync.sh`: it includes `llm-gw` and `tool-repo-seed`, and stops on a release without the gateway image. It needs **skopeo and Python 3**. The Docker pull/tag/push fallback does not preserve release digests.

On a connected workstation, download the manifest once and use that file for both the copy and the upgrade:

```bash theme={"dark"}
curl -fsSLO https://charts.xpander.ai/image_sync.sh
curl -fsSLO https://charts.xpander.ai/install.sh
curl -fsSL https://github.com/xpander-ai/releases/releases/download/latest/release-manifest.json \
  -o release-manifest.json
bash image_sync.sh --source docker.io/xpanderaihub \
  --dest <your-registry>/xpander \
  --release-manifest ./release-manifest.json
```

Pass registry credentials with the source/destination flags or registry auth. `--dry-run` prints the plan. Keep the JSON manifest: a single `--tag` or a legacy service-only manifest is not a complete release. Tag flags and repository retention: [Releases](https://docs.xpander.ai/self-hosted/airgap-upgrade.md#releases).

**Fully sealed network?** Archive outside, carry the whole directory in, import inside:

```bash theme={"dark"}
# On a connected workstation:
bash image_sync.sh --source docker.io/xpanderaihub \
  --release-manifest ./release-manifest.json --archive-dir ./xpander-images

# After transferring the complete directory, on the inside:
bash image_sync.sh --from-archive ./xpander-images --dest <your-registry>/xpander
```

Carry `install.sh`, `image_sync.sh` and their `release_manifest.py` with the bundle. Get the validator from the immutable `release_manifest-<sha256>.py` URL in the scripts and save it as `release_manifest.py` beside them. The scripts verify its checksum. Import validates the full image set and digests first. Complete an incomplete archive on the connected side. Legacy archives need the previous importer.

The chart is an OCI artifact. Mirror it with the images (`--chart-version <X.Y.Z> --chart-source registry-1.docker.io/xpanderaihub/xpander-airgap`, landing at `oci://<your-registry>/xpander/charts/xpander-airgap`), or hand-carry the `.tgz`:

```bash theme={"dark"}
helm registry login registry-1.docker.io --username <user> --password-stdin
helm pull oci://registry-1.docker.io/xpanderaihub/xpander-airgap --version <X.Y.Z>
# copy xpander-airgap-<X.Y.Z>.tgz onto the sealed-network host, then --chart-path it
```

> [accordion] Using Nexus, Artifactory, or Harbor
>
> **Hosted repository: mirror into it.** Use it as `--registry`, and fill it with the wizard or `image_sync.sh`. `--dest-user` / `--dest-token` handle Basic auth:
>
> ```bash theme={"dark"}
> bash image_sync.sh --source docker.io/xpanderaihub \
>   --dest nexus.corp.example:5000/xpander \
>   --dest-user <user> --dest-token <token>
> ```
>
> Then install with `--registry nexus.corp.example:5000/xpander`.
>
> **Pull-through proxy, no mirror step.** Create a Docker *proxy* repository upstream of Docker Hub (`https://registry-1.docker.io`) with the xpander read token as its **upstream** credentials, and keep the org path in the registry value:
>
> ```bash theme={"dark"}
> --registry <nexus-host>/<proxy-repo>/xpanderaihub
> ```
>
> Images cache on first pull. Keep the `xpanderaihub` segment: the chart builds `<registry>/<name>`, so `<proxy-repo>/xpanderaihub/agent-controller` maps to `xpanderaihub/agent-controller` upstream. A **fully sealed** network cannot use a proxy: use the archive flow into a hosted repository.

> [!TIP]
>
> `bash install.sh --edition airgap --guide` prints this runbook personalized with your registry and namespace (also printed when no cluster is reachable). `--manual` prints the [whole install as commands](https://docs.xpander.ai/self-hosted/hybrid-install.md#advanced-review-first-or-manual-install) and runs nothing.

## The license

New installs use **two sealed files** (`install.json` + `activation.json`, `xpander-license/v2`): see **[Two-File Licensing](https://docs.xpander.ai/self-hosted/licensing.md)** for caps, expiry and renewal. The installer validates both and creates the `xpander-license` Secret:

```bash theme={"dark"}
kubectl -n xpander create secret generic xpander-license \
  --from-file=install.json=./install.json \
  --from-file=activation.json=./activation.json \
  --dry-run=client -o yaml | kubectl -n xpander apply -f -
```

The preflight hook verifies the **Ed25519 signature** against `global.license.publicKey`, and deployment-manager refuses to start on a bad one.

* **Your own `--values` file** displaces the installer's license block: also pass `--set global.license.installKey=install.json --set global.license.activationKey=activation.json --set global.license.publicKey=<license public key>`.
* **Renewing** replaces only `activation.json` and redeploys. See [Renewing: re-activate](https://docs.xpander.ai/self-hosted/licensing.md#renewing-re-activate).
* **Expired activation**: the installer warns and preflight refuses a fresh install. A running install degrades in stages. `GET /v1/license/status` returns `valid`, `expiring`, `grace` or `expired`. Alert on anything past `expiring`.
* **Single-tenant**: one license, one organization.
* **Legacy single file** (`xpander-license/v1`): `--license-file ./license.json` creates a Secret with one `license.json` key, wired through `global.license.key`. Renew by replacing that file and redeploying.

## Next steps

> [card] [Publish the hosts and sign in](https://docs.xpander.ai/self-hosted/airgap-access.md)
>
> Domains, ingress, DNS, TLS and the first sign-in.

> [card] [Configure the install](https://docs.xpander.ai/self-hosted/airgap-configure.md)
>
> Data tier, executor fleet, skill generation and skill credentials.

> [card] [Connect your models](https://docs.xpander.ai/self-hosted/airgap-models.md)
>
> Your LLM endpoint, Amazon Bedrock or a local vLLM server.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.