Private Registries and Air-Gapped Deployments
Some environments restrict access to public container registries like ghcr.io and docker.io. In these cases, you need to mirror the required container images to your private registry before deploying.
This guide covers how to mirror the images required by the wasmCloud Helm chart and configure the deployment to use your private registry.
Required images
The wasmCloud runtime-operator Helm chart uses the following container images:
| Image | Component |
|---|---|
ghcr.io/wasmcloud/runtime-operator:<version> | Operator |
ghcr.io/wasmcloud/runtime-gateway:<version> | Gateway |
ghcr.io/wasmcloud/wash:<version> | Runtime host |
docker.io/nats:<nats-version> | NATS server (optional) |
To find the exact image tags for a given chart version, run:
helm show values oci://ghcr.io/wasmcloud/charts/runtime-operator --version <version>For example, with version 2.2.0, the images are:
ghcr.io/wasmcloud/runtime-operator:2.2.0ghcr.io/wasmcloud/runtime-gateway:2.2.0ghcr.io/wasmcloud/wash:2.2.0docker.io/nats:2.11.3-alpine
If you disable the built-in NATS server (--set nats.enabled=false), you do not need to mirror the NATS image.
Mirror images to your registry
Prerequisites
- oras CLI
- yq and jq
- Authenticated to both the source registries (
ghcr.io,docker.io) and your destination registry
Create an image manifest
Create a mirror.yaml file listing the source and destination for each image. Replace myregistry with your private registry:
images:
# wasmCloud runtime images
- source: ghcr.io/wasmcloud/runtime-operator:2.2.0
destination: myregistry/wasmcloud/runtime-operator:2.2.0
- source: ghcr.io/wasmcloud/runtime-gateway:2.2.0
destination: myregistry/wasmcloud/runtime-gateway:2.2.0
- source: ghcr.io/wasmcloud/wash:2.2.0
destination: myregistry/wasmcloud/wash:2.2.0
# Third-party images
- source: docker.io/nats:2.11.3-alpine
destination: myregistry/nats:2.11.3-alpineRun the mirror script
Create and run mirror.sh:
#!/bin/bash
CONFIG_FILE="mirror.yaml"
if ! command -v oras >/dev/null 2>&1; then
echo "oras not found. Please install: https://oras.land"
exit 1
fi
echo "Mirroring images from $CONFIG_FILE..."
yq -o=json e '.images[]' "$CONFIG_FILE" | jq -c '.' | while read -r line; do
src=$(echo "$line" | jq -r '.source')
dst=$(echo "$line" | jq -r '.destination')
if [[ -n "$src" && -n "$dst" && "$dst" != "null" ]]; then
echo "Copying $src -> $dst"
oras copy "$src" "$dst"
fi
donechmod +x ./mirror.sh
./mirror.shDeploy with your private registry
The Helm chart provides a global.image.registry value that overrides the registry for all component images at once.
Basic install with global registry override
helm install wasmcloud-runtime oci://ghcr.io/wasmcloud/charts/runtime-operator \
--version 2.2.0 \
--namespace wasmcloud \
--create-namespace \
--set global.image.registry="myregistry"With an image pull secret
If your private registry requires authentication, create a Kubernetes pull secret and reference it in the Helm install:
kubectl create namespace wasmcloud
kubectl create secret docker-registry my-registry-secret \
--namespace wasmcloud \
--docker-server=myregistry \
--docker-username=<username> \
--docker-password=<password>Then install with the pull secret:
helm install wasmcloud-runtime oci://ghcr.io/wasmcloud/charts/runtime-operator \
--version 2.2.0 \
--namespace wasmcloud \
--create-namespace \
--set global.image.registry="myregistry" \
--set global.image.pullSecrets[0].name="my-registry-secret"Per-component registry overrides
If you need to mirror images to different registry paths, you can override each component individually:
helm install wasmcloud-runtime oci://ghcr.io/wasmcloud/charts/runtime-operator \
--version 2.2.0 \
--namespace wasmcloud \
--create-namespace \
--set operator.image.registry="myregistry" \
--set gateway.image.registry="myregistry" \
--set runtime.image.registry="myregistry" \
--set nats.image.registry="myregistry"Rendering manifests with helm template
If your organization uses a GitOps workflow or requires manifests to be reviewed before applying, you can use helm template to render the Kubernetes manifests locally without installing anything to the cluster.
Render to stdout
helm template wasmcloud-runtime oci://ghcr.io/wasmcloud/charts/runtime-operator \
--version 2.2.0 \
--namespace wasmcloud \
--set global.image.registry="myregistry" \
--set global.image.pullSecrets[0].name="my-registry-secret"Render to a directory
Write the rendered manifests to a directory for review or to check into version control:
helm template wasmcloud-runtime oci://ghcr.io/wasmcloud/charts/runtime-operator \
--version 2.2.0 \
--namespace wasmcloud \
--set global.image.registry="myregistry" \
--set global.image.pullSecrets[0].name="my-registry-secret" \
--output-dir ./wasmcloud-manifestsThis creates a wasmcloud-manifests/ directory with one file per Kubernetes resource. You can then apply them with:
kubectl apply --recursive -f ./wasmcloud-manifestsVerify image references
After rendering, you can verify that all image references point to your private registry:
helm template wasmcloud-runtime oci://ghcr.io/wasmcloud/charts/runtime-operator \
--version 2.2.0 \
--namespace wasmcloud \
--set global.image.registry="myregistry" \
| grep "image:"You should see all images prefixed with your registry and no references to ghcr.io or docker.io.
Use a values file
For repeatable deployments, store your overrides in a values file rather than passing --set flags:
global:
image:
registry: myregistry
pullSecrets:
- name: my-registry-secretThen reference it with -f:
helm template wasmcloud-runtime oci://ghcr.io/wasmcloud/charts/runtime-operator \
--version 2.2.0 \
--namespace wasmcloud \
-f my-values.yamlThis same values file works with helm install and helm upgrade.
If your team uses Kustomize to manage Kubernetes manifests, you can combine it with helm template. Render the chart to a directory as shown above, then use Kustomize to apply additional patches, labels, or transformations on top of the rendered output. For example, you can use a Kustomize images transformer to rewrite image references, or layer environment-specific overlays on top of a shared base.
A minimal setup looks like:
resources:
- ./wasmcloud-manifests/runtime-operator/templates
images:
- name: ghcr.io/wasmcloud/runtime-operator
newName: myregistry/wasmcloud/runtime-operator
- name: ghcr.io/wasmcloud/runtime-gateway
newName: myregistry/wasmcloud/runtime-gateway
- name: ghcr.io/wasmcloud/wash
newName: myregistry/wasmcloud/wash
- name: docker.io/nats
newName: myregistry/natskubectl apply -k .This approach is particularly useful when you need to manage multiple environments (staging, production) with different registries or configurations.
Helm values reference
These are the image-related values you can configure:
| Value | Default | Description |
|---|---|---|
global.image.registry | "" | Override registry for all images |
global.image.pullSecrets | [] | Global image pull secrets |
operator.image.registry | ghcr.io | Operator image registry |
operator.image.repository | wasmcloud/runtime-operator | Operator image repository |
operator.image.tag | Chart appVersion | Operator image tag |
gateway.image.registry | ghcr.io | Gateway image registry |
gateway.image.repository | wasmcloud/runtime-gateway | Gateway image repository |
runtime.image.registry | ghcr.io | Runtime host image registry |
runtime.image.repository | wasmcloud/wash | Runtime host image repository |
runtime.image.tag | Chart appVersion | Runtime host image tag (defaulted to the chart's appVersion starting in 2.0.3) |
nats.image.registry | docker.io | NATS image registry |
nats.image.repository | nats | NATS image repository |
nats.image.tag | 2.11.3-alpine | NATS image tag |
Offline documentation
These docs are also published as a container image and a static tarball so operators can read them inside an air-gapped environment.
Mirror the docs image alongside the other images:
export REGISTRY=my-registry.corp.com
export DOCS_VERSION=2.2.0
oras copy ghcr.io/wasmcloud/docs:${DOCS_VERSION} \
${REGISTRY}/wasmcloud/docs:${DOCS_VERSION}Then run the docs 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: wasmcloud-docs
namespace: wasmcloud
spec:
replicas: 1
selector:
matchLabels:
app: wasmcloud-docs
template:
metadata:
labels:
app: wasmcloud-docs
spec:
imagePullSecrets:
- name: registry-credentials
containers:
- name: nginx
image: my-registry.corp.com/wasmcloud/docs:2.2.0
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
---
apiVersion: v1
kind: Service
metadata:
name: wasmcloud-docs
namespace: wasmcloud
spec:
selector:
app: wasmcloud-docs
ports:
- port: 80
targetPort: 80The image bakes only the current (v2) docs — the archived v1 and 0.82
versions are stripped at build time. It is a sealed snapshot built with
external integrations disabled, so it makes no outbound requests to Google
Analytics, HubSpot, Reo, Algolia, Google Fonts, or api.github.com. Links to
external sites (GitHub, Slack, social) 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 wasmcloud-docs-2.2.0.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.