Getting Started

Prerequisites

The cluster, the four operators, DNS, image pull access, and an activation code you need before you run helm install.

Prerequisites

Everything here you set up once per cluster, before the first helm install. Reuse it for every deployment you put on that cluster afterwards. The activation code is the one exception: you mint that fresh, right before each install.

A Kubernetes cluster

Any conformant Kubernetes works. k3s on a single VPS, kind on your laptop, GKE, EKS, whatever you already run. The chart makes no cloud-specific assumptions: no Gateway API, no Workload Identity, no cloud secret manager.

A single-node k3s box is enough to get a deployment serving:

curl -sfL https://get.k3s.io | sh -s - --disable traefik
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml

--disable traefik matters, because the chart's Ingress objects are nginx.

The four operators

The chart renders custom resources for four things and expects an operator already running in the cluster to act on them: ingress-nginx for the Ingress objects, cert-manager for the certificates, CloudNativePG for Postgres, and Strimzi for Kafka. None of these ship with the chart. Install them once, before you install the chart itself.

OperatorWhat it runsNeeded unless
ingress-nginxRoutes traffic to the platformyou front it with your own gateway (ingress.enabled=false)
cert-managerIssues the TLS certificate for your domainsame
CloudNativePGThe Postgres Cluster (postgres.mode: operator, the default)postgres.mode=external
StrimziThe Kafka KafkaNodePool + Kafka (kafka.mode: operator, the default)kafka.mode=external

Redis is not on this list. It runs as a bundled StatefulSet either way (redis.mode: bundled, the default) or against a managed instance (redis.mode: external); there is no Redis operator involved.

The one-command path

./deploy/scripts/install-prereqs.sh

It is a thin wrapper around plain helm upgrade --install calls, echoed before each one runs, so read it before pointing it at your own cluster. Skip a piece you already run:

./deploy/scripts/install-prereqs.sh --skip-ingress --skip-cert-manager
./deploy/scripts/install-prereqs.sh --skip-cnpg --skip-strimzi

If you set postgres.mode: external or kafka.mode: external, skip the matching operator entirely: the chart never renders a Cluster or a Kafka object in external mode, so there is nothing for CNPG or Strimzi to act on.

Strimzi's operator image is large and pulls from quay.io. The script waits up to 15 minutes per install by default; on a slow link raise it:

WAIT_TIMEOUT=25m ./deploy/scripts/install-prereqs.sh

The raw commands, if you would rather not run a script against your cluster

# ingress-nginx
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
  -n ingress-nginx --create-namespace

# cert-manager (see the Autopilot callout below before you skip the leaderElection flag)
helm repo add jetstack https://charts.jetstack.io
helm upgrade --install cert-manager jetstack/cert-manager \
  -n cert-manager --create-namespace \
  --set crds.enabled=true \
  --set global.leaderElection.namespace=cert-manager

# CloudNativePG (runs Postgres)
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm upgrade --install cnpg cnpg/cloudnative-pg \
  -n cnpg-system --create-namespace

# Strimzi (runs Kafka, cluster-wide)
helm repo add strimzi https://strimzi.io/charts/
helm upgrade --install strimzi strimzi/strimzi-kafka-operator \
  -n strimzi-system --create-namespace --set watchAnyNamespace=true

Already running CNPG or Strimzi on this cluster for something else? Keep yours. The chart only needs the CRDs registered and does not care who installed the operator.

The chart checks for you

If you try to install with postgres.mode=operator or kafka.mode=operator and the matching CRDs are not registered, helm install fails immediately with a message naming the missing operator and the install command, instead of silently rendering a Cluster or Kafka object nobody reconciles. That is intentional: Install failures has the full message and what to do with it.

cert-manager on GKE Autopilot

If your cluster is a GKE Autopilot cluster, do not skip --set global.leaderElection.namespace=cert-manager. install-prereqs.sh sets it unconditionally, for this reason.

cert-manager defaults leader election to the kube-system namespace. Autopilot's Warden admission controller blocks anything writing to kube-system, so cainjector never wins the leader election, never becomes leader, and never injects the CA bundle into any webhook. Every certificate you try to create afterwards fails with:

x509: certificate signed by unknown authority

That error points at TLS, not at leader election, so it is easy to lose an afternoon chasing the wrong cause. If you see it on a GKE Autopilot cluster, check cainjector's logs for a leader-election denial before you touch DNS or the ClusterIssuer:

kubectl -n cert-manager logs deploy/cert-manager-cainjector | grep -i leader

The fix is the one flag above. It is harmless on every other cluster, which is why the install script always sets it.

A DNS name you control

You need a hostname pointing at your cluster's ingress IP before you install, not after. cert-manager cannot issue a certificate for a name that does not resolve, so a missing record shows up as an Ingress stuck without TLS.

Point either a wildcard *.yourdomain.com A record, or individual A records for the hosts you plan to use (platform.host, and ingress.hosts.app / ingress.hosts.realtime if you split them), at the ingress controller's external IP.

The chart does not create DNS records. ingress.externalDNS is false by default, meaning you manage the record yourself. Turn it on only if you already run external-dns.

Pull access to the images

Every first-party image ships from GHCR: ghcr.io/solvegio/mvne-api, mvno-api, mvne, mvno, ess, realtime, and license-agent. If those packages are public for you, there is nothing to do.

If they are private, create a pull secret in the namespace you are installing into and name it at install:

kubectl create secret docker-registry ghcr-pull \
  --docker-server=ghcr.io --docker-username=<user> --docker-password=<pat> \
  -n my-mvno
# then add: --set imagePullSecret=ghcr-pull

Mirroring the images into your own registry, for restricted egress or an airgapped cluster, is covered in Registry and airgap.

Transactional email, or turning it off

ess.enabled defaults to true, and Kafka is wired by default. That combination means a bare install with no Resend key used to fail late, as a crashlooping pod. It now fails at render, with a clear message, before anything gets scheduled. Decide which side of this you want before you install:

  • Set --set auth.resendAPIKey=<your Resend key> to send transactional email through Resend.
  • Set --set ess.enabled=false to install without transactional email at all.

An activation code

The one licensing value. You mint it in the Hub console for the specific deployment you are about to install, and you pass it as activation.code. The license agent redeems it once, on first enrollment, and gets back a signed lease.

Two properties of the code matter for how you plan the install:

  • It is single use. The agent consumes it on the first successful enrollment. Re-running helm upgrade with the same value in activation.code is a no-op, not a second activation, so it is safe to leave it in your values.
  • It is short lived. The default TTL is 15 minutes from the moment you mint it.

Mint the code when you are ready to run helm install, not the day before. A code that has expired is not an error you can work around at install time; you go back to the console and mint another one. Activation covers the flow, including activating an MVNO against your own MVNE instead of the public Hub.

With all of this in place, go to the Quickstart.

Copyright © 2026