Configure

Registry and air-gapped installs

Pinning images by digest, mirroring the image set into your own registry, and pulling from a private one.

Registry and air-gapped installs

By default the chart pulls its images from ghcr.io on every install. If your cluster cannot reach GHCR, or you want to know exactly which bytes are running, this page covers pinning and mirroring.

Pulling from a private registry

Our images are on GHCR. If the packages you were given are private, create a dockerconfigjson Secret in the release namespace and name it:

kubectl create secret docker-registry ghcr-pull \
  --docker-server=ghcr.io \
  --docker-username=<user> \
  --docker-password=<token> \
  -n my-mvno

helm upgrade --install my-mvno ... --set imagePullSecret=ghcr-pull

imagePullSecret is a single name, and the chart attaches it to the six pods that pull an application image: the API, the web tier, the auth service, the email worker, realtime and the license agent. The bundled Postgres, Redis and Kafka StatefulSets do not carry it, so if you mirror those images into a private registry as well, that registry has to be readable without a pull secret, or the images have to be present on the nodes already.

Leave it empty if the registry is public, or if you have already imported the images into the node container runtime with something like k3s ctr images import.

Pinning by digest

A tag can be re-pushed under you. A digest cannot. For production, and for any install you want to be able to reproduce, pin by digest.

--set image.digest=sha256:<api-image-digest>

image.digest pins the API image and nothing else. This is the trap on this page. The web tier is a separate image, and it keeps resolving through image.registry, platform.kind and image.tag, so an install with only image.digest set is not a pinned install: the API is frozen and the console or storefront still floats on a tag. To pin the web tier, give app.image a full digest reference of its own.

The license agent, the email worker and the realtime service are full image references already, and they take the repo@sha256:... form directly. A fully pinned install sets all five:

--set image.digest=sha256:<mvno-api-digest> \
--set app.image=ghcr.io/solvegio/mvno@sha256:<mvno-web-digest> \
--set images.licenseAgent=ghcr.io/solvegio/license-agent@sha256:<digest> \
--set images.ess=ghcr.io/solvegio/ess@sha256:<digest> \
--set images.realtime=ghcr.io/solvegio/realtime@sha256:<digest>

GoTrue is structured differently: images.auth.repository and images.auth.tag (default supabase/gotrue at v2.188.1). It already ships on a version tag rather than latest.

image.pullPolicy (default IfNotPresent) covers all five first-party workloads: the API, the web tier, the email worker, realtime and the license agent. GoTrue reads images.auth.pullPolicy instead. IfNotPresent is right for digests and released tags. Always only earns its registry round trip if you keep re-pushing one moving tag.

Mirroring the image set

On a host that can reach GHCR, copy each image into your own registry. skopeo preserves the digest and every architecture in the manifest list:

skopeo copy --all \
  docker://ghcr.io/solvegio/mvno-api:latest \
  docker://registry.acme.internal/solvegio/mvno-api:latest

crane does the same thing if that is what you already have:

crane copy \
  ghcr.io/solvegio/mvno-api:latest \
  registry.acme.internal/solvegio/mvno-api:latest

Then point the chart at the mirror. image.registry covers the API image and the derived web image. Everything else is named explicitly:

--set image.registry=registry.acme.internal \
--set images.licenseAgent=registry.acme.internal/solvegio/license-agent:latest \
--set images.ess=registry.acme.internal/solvegio/ess:latest \
--set images.realtime=registry.acme.internal/solvegio/realtime:latest \
--set images.auth.repository=registry.acme.internal/supabase/gotrue \
--set imagePullSecret=<your-registry-cred-secret>

Note that images.auth.repository carries its own registry prefix. It does not read image.registry, so a mirrored install has to name it in full or the auth pods will still try Docker Hub.

First-party images

Mirror the pair that matches your platform.kind, plus the three shared ones.

ImageValue that points at itNeeded when
ghcr.io/solvegio/mvno-api or ghcr.io/solvegio/mvne-apiimage.registry + image.repository + image.tag / image.digestAlways. Derived from platform.kind.
ghcr.io/solvegio/mvno or ghcr.io/solvegio/mvneapp.image (or derived from image.registry, platform.kind and image.tag)app.enabled=true
ghcr.io/solvegio/license-agentimages.licenseAgentAlways.
ghcr.io/solvegio/essimages.essess.enabled=true
ghcr.io/solvegio/realtimeimages.realtimerealtime.enabled=true

Everything else the chart pulls

An air-gapped install needs these too, or the pods stay in ImagePullBackOff while the first-party images sit there mirrored and ready.

ImageValueNeeded when
supabase/gotrue:v2.188.1images.auth.repository, images.auth.tagAlways.
ghcr.io/cloudnative-pg/postgresql:18postgres.imageNamepostgres.mode=operator, the default
redis:7-alpineredis.imageredis.mode=bundled, the default
curlimages/curl:8.10.1ordering.waitImageordering.enabled=true, the default
postgres:18-alpineordering.dbWaitImageordering.enabled=true, the default
busybox:1.36ordering.kafkaWaitImageordering.enabled=true, the default

The three ordering.* images are the init containers that hold each workload back until its dependency answers. They are small and they are easy to forget.

The images the operators bring

This is the part that catches people out. Postgres and Kafka run under CloudNativePG and Strimzi, and those operators pull images that this chart never names. Mirroring only the table above leaves you with a healthy set of first-party pods and a data tier stuck in ImagePullBackOff.

You need, at minimum:

  • the CloudNativePG operator image, from the version of its chart you install
  • the Strimzi operator image (quay.io/strimzi/operator), from the version of its chart you install
  • the Kafka broker image Strimzi runs, which is the one worth thinking about

Pin kafka.version before you mirror anything. By default the chart leaves it empty and Strimzi picks a Kafka version it supports, which is the right behaviour on a connected cluster and the wrong one when you are trying to work out which image to copy into a private registry. Set kafka.version to a version your installed Strimzi accepts, then mirror the broker image for exactly that version. A Strimzi release only accepts a narrow window of Kafka versions and refuses anything outside it, so check its release notes rather than guessing.

What still has to reach the internet

Mirroring the images does not make the deployment offline. The license agent has to reach activation.parentUrl (the Solvegio Hub by default, or your MVNE) to enroll, to renew its lease, and to send the heartbeat. An install that cannot make outbound HTTPS to that one host will come up, wait, and never start serving. See What phones home for the exact traffic, and How licensing works for what happens when a lease cannot be renewed.

Upgrading a pinned install means changing a digest rather than letting a tag drift: Upgrade. Every value here is listed with its default in the Helm values reference.

Copyright © 2026