Early beta (0.1.0), not yet for critical processes. See the roadmap
PNeX logo

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).
  • helm and kubectl pointing 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.io resolves to 78.232.42.13, no DNS to manage.

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 --wait

Then 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:

cluster-issuer.yaml
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: traefik
kubectl apply -f cluster-issuer.yaml
kubectl get clusterissuer   # READY True
cert-manager, then the Let's Encrypt ClusterIssuer

2. 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 pods
CloudNativePG operator 1.30

3. 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-deploy

git 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:

pnex-values.yaml
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-tls
certificate.yaml
apiVersion: 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: letsencrypt
kubectl 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 pods

The first start takes a few minutes (image pulls, then PostgreSQL). pnex-api restarts a few times while the database comes up: this is expected.

helm upgrade --install pnex, then the rollout

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 -1

The 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; echo

Sign 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).

Certificate, ingress, HTTPS and the admin password (masked)

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; echo

Security 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.trustedProxies so 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.trustedProxies to 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.yaml

image.tag overrides the chart's appVersion (e.g. main-<sha>).

On this page