Skip to content

Install in an Air-Gapped Cluster

An offline installation needs internal copies of every chart, image, plugin, bundle, and identity endpoint used by Varroa or Jenkins.

ArtifactInclude
VarroaChart, backend, and dashboard
DependenciesEnabled NATS and Dex images
JenkinsImages selected by version profiles
AgentsImages referenced by bundles
PluginsPacks for permitted profiles
ConfigurationInternal Git or OCI artifacts

Render the exact values and inspect every image before transfer:

Terminal window
helm template varroa oci://ghcr.io/varroaci/charts/varroa \
--version <version> -n varroa-system -f values-airgap.yaml > rendered.yaml
grep 'image:' rendered.yaml

Run this in a connected environment for every profile that offline controllers may use:

Terminal window
varroactl export plugins \
--profile jenkins-version-2-570 \
--to tar:///transfer/plugins-2-570.tar.gz

The command resolves the plugin closure, downloads it, verifies checksums, and creates an OCI plugin pack. --to also accepts oci:// and dir://. Use --registry-config for registry credentials. Use --insecure only for plain HTTP on a trusted isolated network.

Create the pull secret in the Varroa namespace and in every controller namespace:

Terminal window
kubectl create namespace varroa-system
kubectl create secret docker-registry registry-credentials \
-n varroa-system \
--docker-server=registry.example.com \
--docker-username='<user>' \
--docker-password='<password>'
global:
imagePullSecrets:
- name: registry-credentials
operator:
image: {repository: registry.example.com/varroa-jenkins, tag: "<version>"}
gateway:
image: {repository: registry.example.com/varroa-jenkins, tag: "<version>"}
bff:
image: {repository: registry.example.com/varroa-jenkins, tag: "<version>"}
frontend:
image: {repository: registry.example.com/varroa-jenkins-frontend, tag: "<version>"}

Mirror and override enabled dependency images too. Set ProvisioningDefaults.spec.imagePullSecrets for controller pods. Each named Secret must exist in the controller namespace.

Use local storage when one update-center replica is sufficient:

updateCenter:
enabled: true
storage:
type: local
local:
size: 20Gi
pullThrough:
enabled: false
networkPolicy:
enabled: true
pullThroughEgress:
enabled: false

For shared registry storage, use updateCenter.storage.type: oci with an internal ref and existingSecret. Allow only that registry through networkPolicy.updateCenterRegistryEgress.

Install from an internal chart registry:

Terminal window
helm install varroa oci://registry.example.com/charts/varroa \
--version <version> -n varroa-system --create-namespace \
-f values-airgap.yaml
Terminal window
export VARROACTL_UC_TOKEN=$(kubectl get secret \
varroa-updatecenter-import-token -n varroa-system \
-o jsonpath='{.data.token}' | base64 -d)
kubectl port-forward -n varroa-system svc/varroa-updatecenter 8080:8080

In another shell:

Terminal window
varroactl import \
--from tar:///transfer/plugins-2-570.tar.gz \
--to uc://localhost:8080

--from accepts oci://, dir://, and tar://. uc:// is valid only for --to and requires VARROACTL_UC_TOKEN.

Require StorageReady=True, CoverageComplete=True, and Ready=True before provisioning offline controllers:

Terminal window
kubectl get updatecenter varroa-update-center -o yaml

status.gaps lists missing plugin versions. With pull-through disabled, an affected controller reports WaitingForUpdateCenter and remains blocked until coverage is complete.

The chart isolates only varroa-system. Apply default-deny egress and explicit allow rules in every controller namespace. Allow DNS, the Kubernetes API when needed, the mite gateway, internal registries, internal Git or OCI sources, the update center, identity services, and approved build destinations.

Test from the control-plane, Jenkins, and agent pods. Public registry and Jenkins update-service connections must fail while all internal endpoints remain reachable.

Create a controller using an imported version profile, then check:

Terminal window
kubectl get controller -A
kubectl get updatecenter varroa-update-center \
-o jsonpath='{.status.phase}{"\n"}{.status.gaps}{"\n"}'

The controller should reach Connected. Run a build that creates an agent pod to verify its image was mirrored.

SymptomCheck
ImagePullBackOffRepository, tag, architecture, and namespace pull secret.
Update center not readyPVC binding or internal registry reachability.
Coverage incompletestatus.gaps and imported profile packs.
Import returns 401Import token and update-center Secret.

See Update Center and Plugin packs.