Kubernetes
Deploy PNeX on Kubernetes with cert-manager, CloudNativePG and the Helm chart
The chart lives in pnex-deploy
(helm/pnex). It deploys the PNeX server and job worker, Rauthy, OpenObserve,
Valkey, RustFS and a PostgreSQL cluster.
This page walks through a real install of 0.1.0-beta.2, recorded on a fresh
Ubuntu VM with a public IP. The demo uses k3s because it sets up a
single-node cluster in one command, with an ingress controller (Traefik) and a
default StorageClass included. Any conformant Kubernetes works: managed
(EKS, GKE, AKS, Scaleway Kapsule…), RKE2, microk8s, kubeadm… Only the ingress
class changes.
Requirements
- Kubernetes 1.27 or later, a default StorageClass.
- An ingress controller (Traefik in the demo, ingress-nginx is the chart's default).
helmandkubectlpointing at the cluster.- A public name for PNeX, with ports 80 and 443 reachable from the internet for
Let's Encrypt. Devices need a publicly trusted certificate. The demo uses
sslip.io:
pnex-78-232-42-13.sslip.ioresolves to78.232.42.13, no DNS to manage.
curl -sfL https://get.k3s.io | sh -
kubectl get nodes
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4 | bash
echo "export KUBECONFIG=/etc/rancher/k3s/k3s.yaml" >> ~/.bashrck3s installs kubectl and ships Traefik (ingress class traefik) and the
local-path StorageClass. helm reads the k3s kubeconfig through KUBECONFIG.
1. cert-manager (TLS certificates)
cert-manager obtains and renews the Let's Encrypt certificate:
helm install cert-manager oci://quay.io/jetstack/charts/cert-manager --version v1.21.2 \
-n cert-manager --create-namespace --set crds.enabled=true --waitThen a ClusterIssuer for Let's Encrypt, which validates the domain through
the ingress controller (HTTP-01). Set ingressClassName to your controller's
class (traefik on k3s, nginx with ingress-nginx), and replace
[email protected] with your own email:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected] # ← your email (Let's Encrypt account)
privateKeySecretRef:
name: letsencrypt-account
solvers:
- http01:
ingress:
ingressClassName: traefikkubectl apply -f cluster-issuer.yaml
kubectl get clusterissuer # READY True2. CloudNativePG operator (PostgreSQL)
The chart declares its database as a CloudNativePG
Cluster: the operator must be installed first.
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm install cnpg cnpg/cloudnative-pg --version 0.29.1 \
-n cnpg-system --create-namespace --wait
kubectl -n cnpg-system get pods3. PNeX (Helm chart)
Fetch the chart of a published release. The chart of a release tag deploys the
images of that release (its appVersion); on main it follows latest.
git clone --depth 1 --branch v0.1.0-beta.5 https://github.com/Pnex/pnex-deploygit may warn that the tag "is not a commit" and that you are in "detached
HEAD" state: both are harmless for an annotated release tag.
The values: the public name, the admin account and the ingress. ingress.tls
points at the certificate requested from cert-manager:
publicHost: pnex-78-232-42-13.sslip.io
admin:
email: [email protected] # ← your email (PNeX admin login)
ingress:
className: traefik # nginx with ingress-nginx
tls:
- secretName: pnex-tlsapiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: pnex-tls
namespace: pnex
spec:
secretName: pnex-tls
dnsNames: [pnex-78-232-42-13.sslip.io]
issuerRef:
kind: ClusterIssuer
name: letsencryptkubectl create namespace pnex && kubectl apply -f certificate.yaml
helm upgrade --install pnex ./pnex-deploy/helm/pnex -n pnex -f pnex-values.yaml
kubectl -n pnex wait --for=condition=Available deployment --all --timeout=15m
kubectl -n pnex get podsThe first start takes a few minutes (image pulls, then PostgreSQL). pnex-api
restarts a few times while the database comes up: this is expected.
4. Check and get the admin password
kubectl -n pnex get certificate,ingress # certificate READY True
curl -sI https://pnex-78-232-42-13.sslip.io/ | head -1The admin password is generated in the cluster on first install. Read it from
the pnex-secrets Secret (<release>-secrets):
kubectl -n pnex get secret pnex-secrets -o jsonpath='{.data.admin-password}' | base64 -d; echoSign in at https://<publicHost>/ with admin.email and this password. It
only seeds the account on the first Rauthy start: change it from the account
page afterwards (the Secret value is then no longer valid).
Secrets
Every secret left empty is generated in the cluster on first install and kept
across upgrades. To manage them yourself, keep them in an encrypted values file
(sops) or point secrets.existingSecret at your own Secret.
Back up the secrets-keys entry of the generated Secret with your database
backups: it is the key ring of the organisations' secrets vault. Without it,
restored secrets cannot be decrypted.
kubectl -n pnex get secret pnex-secrets -o jsonpath='{.data.secrets-keys}' | base64 -d; echoSecurity defaults
- Containers run as non-root, without privilege escalation or capabilities, with the RuntimeDefault seccomp profile and no service account token.
- Valkey requires a password; Valkey, OpenObserve and RustFS only accept PNeX pods (NetworkPolicy, needs a CNI that enforces it).
- Unauthenticated routes are rate limited. Behind a CDN, add its IP ranges to
api.rateLimit.trustedProxiesso limits apply per client. - Sign-in is authorization code with PKCE only.
- Rauthy trusts forwarded client addresses from the private ranges (the pods
of the ingress controller, on any CNI). On a shared cluster, narrow
rauthy.trustedProxiesto your pod CIDR.
Scaling
Set api.replicas above 1: the pods share the HTTP and WebSocket traffic and
form a flow execution cluster — each organisation's flows run on exactly one
pod, and move when a pod fails or drains. Rolling updates drain the old pod
first. Keep replicas × (api.dbMaxConnections + 2) below PostgreSQL's
max_connections.
Upgrade
git -C pnex-deploy fetch --depth 1 origin tag v0.1.0-beta.6 # the next release
git -C pnex-deploy checkout v0.1.0-beta.6
helm upgrade pnex ./pnex-deploy/helm/pnex -n pnex -f pnex-values.yamlimage.tag overrides the chart's appVersion (e.g. main-<sha>).

