Kubernetes (Helm)
Run the Optimum Gateway on Kubernetes with the official Helm chart. The gateway sits beside your Ethereum consensus-layer (CL) client and bridges it into the Optimum network.
One Helm release = one gateway = one CL client.
Running on bare metal or Docker instead? See Quick Start (Docker).
What you need from Optimum
Three things — none of them belong in a file you commit:
| example | |
|---|---|
| API key | ogw_live_… |
| cluster ID | optimum_ethereum_mainnet_v0_1 |
| image digest | sha256:943f6bd4df92… |
You generate the API key yourself from the Partner Console — see Generate your API key. Ask Optimum for a digest, not a tag: a tag can be repointed, a digest cannot.
Requirements
- Kubernetes 1.25+, Helm 3.8+
- A node with a public IP
- Inbound TCP 33213 open to that node from the internet — this is the Optimum network (mump2p) port and the gateway is unusable without it
- The CL client able to reach the gateway on TCP 33212 (usually in-cluster; no public firewall hole needed for this)
- A namespace permitting
hostNetworkandhostPort. Under Pod Security Admission that meansprivileged—baselineblocks both - A default StorageClass supporting
ReadWriteOnce(two small volumes hold the gateway's identities) - Outbound HTTPS to
bootstrap.getoptimum.ioandauth.getoptimum.io
Resource use: requests 150m CPU / 2Gi memory, limit 4Gi memory.
Networking: why hostNetwork is required
The gateway advertises the addresses it detects in its own network namespace to the Optimum bootstrap registry, and has no announce-address override (there is no equivalent of Lighthouse's --enr-address / --nat=extip).
On a normal pod network it would detect and advertise the internal pod IP, register as "reachable," and then silently receive no inbound mump2p peers — a NodePort or LoadBalancer in front does not help, because there is no way to tell the gateway to announce that external address. Reachability comes from hostNetwork, not from a Service.
So the chart defaults to networking.hostNetwork: true (the K8s equivalent of Docker host-mode) and one gateway per node via anti-affinity, since it binds host ports. Schedule it on a node with a public IP and open inbound 33213.
Install
export CHART=oci://registry-1.docker.io/getoptimum/optimum-gateway
export VER=$(helm show chart $CHART | awk '/^version:/{print $2}')
kubectl create namespace optimum
kubectl label namespace optimum pod-security.kubernetes.io/enforce=privileged
# the API key, out of band — never in your values file
kubectl -n optimum create secret generic optimum-gateway-api-key \
--from-literal=api-key='<your API key>'
helm show values $CHART --version $VER > my-values.yaml
# fill in the four values below
helm install gateway $CHART --version $VER -n optimum -f my-values.yamlThe four values you must set
The chart refuses to install without these, rather than starting a gateway that does nothing.
image:
digest: sha256:… # from Optimum
gateway:
clusterId: optimum_… # from Optimum
directClPeers:
- /dns4/<cl-service>.<ns>.svc.cluster.local/tcp/<p2p-port>/p2p/<peer-id>
# CL outside the cluster: /ip4/<cl-host>/tcp/<p2p-port>/p2p/<peer-id>
apiKey:
existingSecret: optimum-gateway-api-keydirectClPeers is an allowlist — the gateway closes any connection from a peer not on it. A wrong peer ID or port means it talks to nothing.
Get your client's peer ID:
curl -s localhost:5052/eth/v1/node/identity | jq -r .data.peer_id # lighthouse, or nimbus with --rest
curl -s localhost:3500/eth/v1/node/identity | jq -r .data.peer_id # prysm
curl -s localhost:5051/eth/v1/node/identity | jq -r .data.peer_id # teku with --rest-api-enabled
curl -s localhost:9596/eth/v1/node/identity | jq -r .data.peer_id # lodestarNimbus does not answer on 9596, and its REST server is off until --rest. Teku's REST server is off until --rest-api-enabled. A refused curl is the beacon API.
Use the client's P2P port in the multiaddr — prysm 13000, others 9000 — not the HTTP port you just queried.
apiKey.existingSecretis the one under the top-levelapiKey:block.
Point your CL client at the gateway
On Helm the order is the reverse of Docker. The chart rejects an empty gateway.directClPeers, so the CL peer ID has to exist before install. After the pod is ready, point the CL at the gateway. Flags are in Connecting your CL client.
kubectl -n optimum port-forward svc/gateway-optimum-gateway 48123:48123
curl -s localhost:48123/api/v1/self_info | jq -r '.peer_id, .libp2p.multiaddrs[]'Build the multiaddr from the address the CL can route, and port 33212:
/ip4/<gateway-node-public-ip>/tcp/33212/p2p/<gateway-peer-id>A pod address in libp2p.multiaddrs is not that address. The peer ID stays across restarts. The IP does not: if the pod moves to another node, update the multiaddr on the CL. Nimbus skips --direct-peer when its netkey is random. See Nimbus.
Check it works
A Running pod proves nothing on its own — the gateway can start, register, and peer with no one.
kubectl -n optimum port-forward svc/gateway-optimum-gateway 48123:48123
curl -s localhost:48123/health | jq '{status, cl: .checks.cl_health.status, cl_peers: .checks.cl_peers.value, mump2p: .checks.mump2p_health.status, mump2p_peers: .checks.mump2p_peers.value}'
curl -s localhost:48123/api/v1/self_info | jq '{peer_id, multiaddrs: .libp2p.multiaddrs}'Healthy looks like: status: "healthy", cl: "ok", cl_peers: 1, and mump2p_peers in the tens. Expect this within a couple of minutes of the pod going ready. On a stream_only deployment cl reads skipped and cl_peers prints null (the check carries no value), while status is still "healthy".
In self_info, multiaddrs must contain a public IP. If it only shows a private or pod address (10.x), nothing outside your cluster can dial you and mump2p_peers will stay at 0.
For the full meaning of each /health and self_info field, see Telemetry & Monitoring.
Upgrade, rollback, uninstall
When Optimum sends a new image digest, put it in image.digest in your values file, then:
helm upgrade gateway $CHART --version $VER -n optimum -f my-values.yaml
helm history gateway -n optimum
helm rollback gateway <REVISION> -n optimum
helm uninstall gateway -n optimumThe deployment strategy is Recreate, not RollingUpdate — two pods must never hold the same identity volume. Expect a brief gap on every upgrade.
Uninstall keeps the identity volumes, so reinstalling keeps the same peer IDs and your CL client's configured multiaddr still matches. To discard them:
kubectl -n optimum delete pvc -l app.kubernetes.io/instance=gatewayThat is permanent. The gateway returns as a new peer and you must update your CL client.
Fleet enrollment (join key)
For large fleets, use one org join key (ojk_) instead of one API key per pod. See Gateway Self-Enrollment.
- Store the join key in a Secret and inject it as
OPT_JOIN_KEY(notOPT_API_KEY). - Set a unique
OPT_GATEWAY_IDper pod — for example the pod name via the downward API — as the enrollment label. - The chart's identity PVC already covers
identity_mump2p_dir, whereenrollment.jsonis written by default, along with a transientenrollment.keyduring first boot. Keep the PVC rather than anemptyDir. AnemptyDirsurvives a container restart but is discarded whenever the Pod is replaced — rescheduling, scaling, or a rolling update — and each replacement then re-enrolls, burning a join-key use and a slot against the org credential cap. It also lets an interrupted first enrollment resume.
The published Helm chart documents the
apiKeyvalues block for the legacy path. If you adopt join-key enrollment, wireOPT_JOIN_KEYthrough your values or workload overrides instead of the API key secret. Contact Optimum if you need chart guidance for your fleet size.
Optional
Send telemetry to Optimum — lets us help you debug. Authenticated with a token derived from your credential, so it needs no extra push credentials:
gateway:
remotePush:
enabled: trueScrape it yourself — if you run the Prometheus Operator:
podMonitor:
enabled: trueMetrics are on :48123/metrics. Keep gateway.logLevel: info; debug is very noisy. Propagation (forwarding Optimum messages into the CL network) is managed by Optimum centrally — you do not need to configure it.
Consumer block stream is off by default. Enable it in gateway config if you need a local WebSocket/gRPC feed of decoded blocks; listeners bind loopback. See Consumer Block Stream.
When it doesn't work
Kubernetes-specific symptoms below. For gateway behaviour that isn't K8s-specific (CL peering, PeerDAS, identity, log noise), see Troubleshooting.
| Symptom | Cause |
|---|---|
helm install fails naming a value | That value is required — the message says which |
helm install says deployed but there are no pods | Pod Security is rejecting them. kubectl -n optimum describe rs -l app.kubernetes.io/instance=gateway — if it mentions violates PodSecurity, the namespace needs privileged |
CrashLoopBackOff immediately | Usually a rejected API key — kubectl logs will show api key not recognized (401) |
cl_peers: 0 | directClPeers wrong, or your CL client was never pointed at the gateway (see above) |
mump2p_peers: 0 after a few minutes | Inbound 33213 is not reachable from the internet |
multiaddrs shows only private IPs | The node has no public IP, or networking.hostNetwork was disabled — it must stay true |
Pod stuck Pending | No node with capacity; the chart also keeps one gateway per node |
Getting help
Send us this — it answers most of the first round of questions:
helm get values gateway -n optimum # safe: contains no secret
kubectl -n optimum get pods,pvc
kubectl -n optimum logs -l app.kubernetes.io/instance=gateway --tail=100
curl -s localhost:48123/health
curl -s localhost:48123/api/v1/self_infohelm get values is safe to share — with apiKey.existingSecret the key is only ever a reference, never a value in the release. Every setting is documented inline in helm show values.

