# Install in an air-gapped cluster

Install Omnia in a cluster that cannot reach `ghcr.io`, pulling every image from a registry you run.

## Prerequisites

- A machine with internet access, a checkout of the Omnia repository at the release tag you want, `helm` (v3) and [`skopeo`](https://github.com/containers/skopeo/blob/main/install.md).
- A container registry the cluster can pull from, and `skopeo` on a machine that can push to it.
- The [Helm install prerequisites](/how-to/operations/install-helm/#prerequisites) for the cluster itself.

## Steps

1. On the connected machine, build the bundle for the version you are installing:

   ```bash
   make airgap-bundle VERSION=0.9.0
   ```

   This writes `dist/omnia-airgap-0.9.0/` and `dist/omnia-airgap-0.9.0.tar`. The bundle holds:

   | Path | Contents |
   |---|---|
   | `oci/` | Every image, all platforms, in an OCI image layout |
   | `images.txt` | The source reference of each image |
   | `charts/` | `omnia-<version>.tgz` with its subcharts, and `omnia-crds-<version>.tgz` |
   | `schemas/` | The PromptPack JSON Schema |
   | `manifest.json` | The version and every image, chart and schema in the bundle |
   | `mirror.sh` | The script for step 3 |
   | `SHA256SUMS` | Checksums of everything outside `oci/` |

   The image list comes from rendering the packaged chart. It covers every image the chart deploys and every image the operator creates at runtime: agent facade and runtime, session-api, memory-api, privacy-api, eval workers, the policy broker, Arena workers, sandbox classes and the sandbox egress proxy. Every Omnia component is switched on for that render, so the bundle serves any combination you enable later.

2. If your values enable an observability subchart (Prometheus, Grafana, Loki, Alloy, Tempo, KEDA or the NFS CSI driver), pass your values file so its images are bundled too:

   ```bash
   make airgap-bundle VERSION=0.9.0 VALUES=values.yaml
   ```

   An image that no chart names, such as a custom `spec.framework.image` on an AgentRuntime, goes in a file of one reference per line, passed to `hack/airgap-bundle.sh` with `--extra`:

   ```bash
   hack/airgap-bundle.sh 0.9.0 --out dist -f values.yaml --extra my-images.txt
   ```

3. Carry the tarball across, unpack it, and push the images to your registry:

   ```bash
   tar -xf omnia-airgap-0.9.0.tar
   cd omnia-airgap-0.9.0
   ./mirror.sh registry.internal.example.com .
   ```

   Each image keeps its path and loses its source registry host: `ghcr.io/altairalabs/omnia-operator:0.9.0` lands at `registry.internal.example.com/altairalabs/omnia-operator:0.9.0`, and `busybox:1.36` at `registry.internal.example.com/busybox:1.36`. A registry value with a path, such as `registry.internal.example.com/omnia`, prefixes it.

   Arguments after `--` go to every `skopeo copy`. Use them for registry credentials or a private CA:

   ```bash
   ./mirror.sh registry.internal.example.com . -- --dest-authfile auth.json
   ```

   Add `--charts` to also push both charts to `oci://<registry>/charts`. Add `--dry-run` to print each source and destination without copying.

4. If the registry needs credentials, create a pull secret in the release namespace and in every workspace namespace. Pods the operator creates run in the workspace namespaces, and a pod can only use a Secret in its own namespace:

   ```bash
   kubectl create secret docker-registry mirror-pull \
     --namespace omnia-system \
     --docker-server=registry.internal.example.com \
     --docker-username=<user> --docker-password=<token>
   ```

5. Install the CRDs chart from the bundle:

   ```bash
   helm install omnia-crds charts/omnia-crds-0.9.0.tgz \
     --namespace omnia-system --create-namespace
   ```

6. Install the app chart from the bundle with `global.imageRegistry`, and `global.imagePullSecrets` if you made a secret:

   ```bash
   helm install omnia charts/omnia-0.9.0.tgz \
     --namespace omnia-system \
     --set dashboard.auth.mode=builtin \
     --set global.imageRegistry=registry.internal.example.com \
     --set 'global.imagePullSecrets[0].name=mirror-pull' \
     -f values.yaml
   ```

   The chart rewrites every image it renders. It also passes the operator and the Arena controller the rewritten image of every pod they create, and passes `global.imagePullSecrets` on as their `--image-pull-secrets` flag. An image you set on a custom resource, such as `spec.framework.image`, `spec.facades[].image` or a SandboxClass you write, is used as written.

7. Turn off every outbound call, since the cluster has no route to the endpoints: add `--set airGapped=true` to the install. It switches off install telemetry, Enterprise licence activation and its heartbeat, and the community Arena template sync from GitHub, keeps PromptPack schemas on the embedded copy, and fails the render if another value names an outside host. [Outbound connections](/reference/platform/outbound-connections/) lists every destination and the value that controls it.

   Licence validation needs no network.

## Verify

List every image the release namespace runs:

```bash
kubectl get pods -n omnia-system \
  -o jsonpath='{range .items[*]}{range .spec.containers[*]}{.image}{"\n"}{end}{end}' | sort -u
```

Every line starts with your registry. Once you create an AgentRuntime, run the same command against its workspace namespace: the facade and runtime images start with your registry too.

## Troubleshooting

### A pod is in `ImagePullBackOff` with an image under your registry

The image is not in the mirror. Compare the image in `kubectl describe pod` with `images.txt`. An image missing from `images.txt` was not in the render the bundle was built from: rebuild with the values file that enables it, or add it with `--extra`.

### A pod in a workspace namespace fails with `unauthorized`

The pull secret named in `global.imagePullSecrets` does not exist in that namespace. Create it there (step 4). The operator adds the secret's name to the pod, and the kubelet reads it from the pod's own namespace.

### An observability subchart still pulls from its public registry

Each subchart reads its own registry setting, and only some read `global.imageRegistry`. The [Helm Values Reference](/reference/platform/helm-values/#private-registry) lists the key each one reads.

## Related pages

- [Install with Helm](/how-to/operations/install-helm/)
- [Helm Values Reference](/reference/platform/helm-values/#private-registry)
- [Configure pod overrides](/how-to/operations/configure-pod-overrides/)