Skip to main content

Air-Gapped Installation

For environments without access to ghcr.io, mirror all Helm charts and container images to an internal registry before installing. This page walks through the full mirroring process using ORAS.

Prerequisites

  • ORAS CLI v1.2+
  • Helm v3.8.0+
  • Write access to your internal container registry

Mirror the Helm charts

export REGISTRY=my-registry.corp.com

oras copy oci://ghcr.io/cosmonic/cosmonic-control:0.4.1 \
  oci://${REGISTRY}/cosmonic/cosmonic-control:0.4.1

oras copy oci://ghcr.io/cosmonic/cosmonic-control-hostgroup:0.4.1 \
  oci://${REGISTRY}/cosmonic/cosmonic-control-hostgroup:0.4.1

Mirror the container images

The chart ships the following container images. Mirror each one to your private registry:

export VERSION=0.4.2  # match chart appVersion

# Core control plane
oras copy ghcr.io/cosmonic/runtime-operator:${VERSION} ${REGISTRY}/cosmonic/runtime-operator:${VERSION}
oras copy ghcr.io/cosmonic/nexus:${VERSION}            ${REGISTRY}/cosmonic/nexus:${VERSION}
oras copy ghcr.io/cosmonic/control/envoy:v1.35.2       ${REGISTRY}/cosmonic/control/envoy:v1.35.2

# Observability stack
oras copy ghcr.io/cosmonic/control/otel-collector-contrib:0.120.0  ${REGISTRY}/cosmonic/control/otel-collector-contrib:0.120.0
oras copy ghcr.io/cosmonic/control/prometheus:v3.3.1               ${REGISTRY}/cosmonic/control/prometheus:v3.3.1
oras copy ghcr.io/cosmonic/control/loki:3.5                        ${REGISTRY}/cosmonic/control/loki:3.5
oras copy ghcr.io/cosmonic/control/tempo:2.9.0                     ${REGISTRY}/cosmonic/control/tempo:2.9.0
oras copy ghcr.io/cosmonic/control/jaeger:2.11.0                   ${REGISTRY}/cosmonic/control/jaeger:2.11.0
oras copy ghcr.io/cosmonic/control/cosmonic-perses:v0.53.1         ${REGISTRY}/cosmonic/control/cosmonic-perses:v0.53.1
oras copy ghcr.io/cosmonic/control/kiwigrid-k8s-sidecar:1.30.10    ${REGISTRY}/cosmonic/control/kiwigrid-k8s-sidecar:1.30.10

# HostGroup
oras copy ghcr.io/cosmonic/control-host:${VERSION} ${REGISTRY}/cosmonic/control-host:${VERSION}
note

The observability image tags (envoy, otel-collector, prometheus, loki, tempo, perses, kiwigrid-k8s-sidecar) are pinned to specific versions independent of the chart appVersion. Check helm show values oci://ghcr.io/cosmonic/cosmonic-control --version 0.4.1 to confirm the exact tags for a given chart version before mirroring.

Configure registry credentials

If your registry requires authentication, create the pull secret before installing:

kubectl create namespace cosmonic-system

kubectl create secret docker-registry registry-credentials \
  --docker-server=my-registry.corp.com \
  --docker-username=<username> \
  --docker-password=<password> \
  -n cosmonic-system

Install with mirrored images

Several chart components set their own image registries explicitly, so a global override alone is not sufficient. Use the following values file to point every component at your private registry:

# air-gapped-values.yaml
global:
  image:
    registry: my-registry.corp.com
    pullSecrets:
      - name: registry-credentials

operator:
  image:
    registry: my-registry.corp.com
nexus:
  image:
    registry: my-registry.corp.com
envoy:
  image:
    registry: my-registry.corp.com
opentelemetryCollector:
  image:
    registry: my-registry.corp.com
prometheus:
  image:
    registry: my-registry.corp.com
loki:
  image:
    registry: my-registry.corp.com
tempo:
  image:
    registry: my-registry.corp.com
perses:
  image:
    registry: my-registry.corp.com
  provisioning:
    sidecar:
      registry: my-registry.corp.com

Install Cosmonic Control from your mirrored Helm chart:

helm install cosmonic-control oci://${REGISTRY}/cosmonic/cosmonic-control \
  --version 0.4.1 \
  --namespace cosmonic-system \
  --create-namespace \
  --set envoy.service.type=LoadBalancer \
  -f air-gapped-values.yaml

Install the HostGroup from your mirrored chart:

helm install hostgroup oci://${REGISTRY}/cosmonic/cosmonic-control-hostgroup \
  --version 0.4.1 \
  --namespace cosmonic-system \
  --set image.repository=${REGISTRY}/cosmonic/control-host \
  --set "image.pullSecrets[0].name=registry-credentials"

Wait for all components to be ready:

kubectl rollout status deploy -l app.kubernetes.io/instance=cosmonic-control -n cosmonic-system
kubectl rollout status deploy -l app.kubernetes.io/instance=hostgroup -n cosmonic-system

DNS for HostGroup pods

OCI image resolution for Wasm artifacts runs inside the HostGroup pod, separate from the Kubernetes container image pull handled by the kubelet. In constrained environments where cluster DNS cannot reach the internal registry (or where the registry hostname resolves differently from the kubelet's perspective), override the HostGroup's DNS configuration:

# hostgroup-values.yaml
dns:
  policy: "None"
  config:
    nameservers:
      - "10.96.0.10"          # CoreDNS service IP, or a custom resolver
      - "8.8.8.8"
    searches:
      - "cosmonic-system.svc.cluster.local"
      - "corp.example.com"
    options:
      - name: "ndots"
        value: "1"
helm install hostgroup oci://${REGISTRY}/cosmonic/cosmonic-control-hostgroup \
  --version 0.4.1 \
  --namespace cosmonic-system \
  --set image.repository=${REGISTRY}/cosmonic/control-host \
  --set "image.pullSecrets[0].name=registry-credentials" \
  -f hostgroup-values.yaml

dns.policy accepts the standard Kubernetes values: ClusterFirst (default behavior), Default, ClusterFirstWithHostNet, or None. Set to None to replace cluster DNS entirely with the entries under dns.config. For finer-grained overrides, see the Kubernetes DNS Pod configuration reference.

If the node itself needs a custom resolver (for example, so the kubelet can pull container images), configure that at the kubelet level. On a Kind cluster, use kubeadmConfigPatches with kubeletExtraArgs.resolv-conf to mount a custom resolv.conf into the node.

Caching workload artifacts on the host

Wasm component images are pulled by the control-host at workload startup, separately from the kubelet's container image pull. On hosts behind slow or intermittent links to the registry, set imagePullPolicy: IfNotPresent on the component spec so the host reuses a cached artifact after the first pull instead of re-fetching it on every restart:

apiVersion: control.cosmonic.io/v1alpha1
kind: HTTPTrigger
metadata:
  name: example
  namespace: default
spec:
  ingress:
    host: example.localhost.cosmonic.sh
    paths:
      - path: /
        pathType: Prefix
  replicas: 1
  template:
    spec:
      components:
        - name: http
          image: my-registry.corp.com/components/hello-world:0.1.2
          imagePullPolicy: IfNotPresent

imagePullPolicy accepts Always (re-pull on every start), IfNotPresent (pull only if the artifact is missing from the host cache), or Never (fail if missing). The field is available on component specs in HTTPTrigger, Workload, WorkloadReplicaSet, and WorkloadDeployment manifests. Pair it with an in-cluster registry mirror to keep workloads warm across host restarts.