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
Mirror the Helm charts
export REGISTRY=my-registry.corp.com
export CHART_VERSION=0.5.3 # the Cosmonic Control version you are installing
oras copy oci://ghcr.io/cosmonic/cosmonic-control:${CHART_VERSION} \
oci://${REGISTRY}/cosmonic/cosmonic-control:${CHART_VERSION}
oras copy oci://ghcr.io/cosmonic/cosmonic-control-hostgroup:${CHART_VERSION} \
oci://${REGISTRY}/cosmonic/cosmonic-control-hostgroup:${CHART_VERSION}Mirror the container images
Rather than maintain a hardcoded image list that drifts with every release,
derive the exact set each chart references with helm template and mirror it.
This always matches the CHART_VERSION you set above, including the
observability image tags, which are pinned independently of the chart
appVersion.
# Collect every image the control-plane and HostGroup charts reference.
images=$(
{
helm template oci://ghcr.io/cosmonic/cosmonic-control --version "${CHART_VERSION}"
helm template oci://ghcr.io/cosmonic/cosmonic-control-hostgroup --version "${CHART_VERSION}"
} | grep -oE 'image:[[:space:]]*"?[^"[:space:]]+' \
| sed -E 's/image:[[:space:]]*"?//' \
| sort -u
)
# Mirror each to your registry, preserving the repository path.
for src in ${images}; do
oras copy "${src}" "${REGISTRY}/${src#*/}"
doneIf you pass custom -f values.yaml overrides at install time, pass the same
-f flags to the helm template commands above so the mirrored set matches
exactly what your install will pull.
The Cosmonic Control documentation is also published as a container image so customers can read it inside an air-gapped environment. Mirror it alongside the other images:
export DOCS_VERSION=0.5.3 # the Cosmonic Control version you are installing
oras copy ghcr.io/cosmonic/docs:${DOCS_VERSION} \
${REGISTRY}/cosmonic/docs:${DOCS_VERSION}The Kubernetes manifests that run the docs are below in Deploy the documentation.
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-systemInstall 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.comInstall Cosmonic Control from your mirrored Helm chart:
helm install cosmonic-control oci://${REGISTRY}/cosmonic/cosmonic-control \
--version ${CHART_VERSION} \
--namespace cosmonic-system \
--create-namespace \
--set envoy.service.type=LoadBalancer \
-f air-gapped-values.yamlInstall the HostGroup from your mirrored chart:
helm install hostgroup oci://${REGISTRY}/cosmonic/cosmonic-control-hostgroup \
--version ${CHART_VERSION} \
--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-systemDeploy the documentation
Run the mirrored docs image as a single Deployment with a ClusterIP
Service. Front it with whatever ingress your environment already uses
(Traefik, Istio, your existing reverse proxy):
apiVersion: apps/v1
kind: Deployment
metadata:
name: cosmonic-docs
namespace: cosmonic-system
spec:
replicas: 1
selector:
matchLabels:
app: cosmonic-docs
template:
metadata:
labels:
app: cosmonic-docs
spec:
imagePullSecrets:
- name: registry-credentials
containers:
- name: nginx
image: my-registry.corp.com/cosmonic/docs:0.5.3
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
---
apiVersion: v1
kind: Service
metadata:
name: cosmonic-docs
namespace: cosmonic-system
spec:
selector:
app: cosmonic-docs
ports:
- port: 80
targetPort: 80The image is a sealed snapshot built with external integrations disabled, so it makes no outbound requests to Algolia, analytics, or Google Fonts. Navbar and footer links to public marketing pages (cosmonic.com, GitHub, blog) will not resolve from an air-gapped browser.
For environments without Kubernetes, the same content is published as a static tarball on the docs release page:
tar xzf cosmonic-docs-0.5.3.tar.gz -C /var/www/docs
# serve /var/www/docs with any static web serverThe static site is built with trailingSlash: true, so URLs end in / and
resolve to <dir>/index.html. Any HTTP server that serves index.html for
directory requests will work; python -m http.server and the default nginx
try_files $uri $uri/ $uri/index.html configuration both do.
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 ${CHART_VERSION} \
--namespace cosmonic-system \
--set image.repository=${REGISTRY}/cosmonic/control-host \
--set "image.pullSecrets[0].name=registry-credentials" \
-f hostgroup-values.yamldns.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: IfNotPresentimagePullPolicy 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.