Kubernetes
pfCentral runs well on Kubernetes. Pick a shape first:
| Shape | NATS | Replicas | State volume | Strategy |
|---|---|---|---|---|
| Single | Embedded | 1 | PersistentVolumeClaim | Recreate |
| High availability | External | 2 or more | emptyDir | RollingUpdate |
With the embedded broker the pod owns the keys and the JetStream data on its volume, so only one pod may run at a time. With an external broker, every replica is stateless: keys live in Secrets and durable data lives in PostgreSQL and on the broker.
Secrets
Section titled “Secrets”Keep secrets out of the manifest:
kubectl create secret generic pf-central \ --from-literal=PFCENTRAL_DATABASE_URL='postgres://pf_central:…@postgres:5432/pf_central?sslmode=require' \ --from-literal=PFCENTRAL_TOKEN_PEPPER_CURRENT="$(openssl rand -hex 32)" \ --from-literal=PFCENTRAL_TOKEN_PEPPER_VERSIONS="<same value>"| Secret | Variable | Shape |
|---|---|---|
| Database DSN | PFCENTRAL_DATABASE_URL | Both |
| Token pepper | PFCENTRAL_TOKEN_PEPPER_CURRENT, _VERSIONS | Both |
| First owner (optional) | PFCENTRAL_SUPERUSER_EMAIL, _PASSWORD | Both |
| Email key and provider credentials | PFCENTRAL_EMAIL_* | Both, if email is on |
| NATS app, callout and sentinel creds | files at PFCENTRAL_NATS_*_CREDS_FILE | HA |
| Delegated signing key | file at PFCENTRAL_NATS_SIGNING_KEY_PATH | HA |
Manifests
Section titled “Manifests”apiVersion: v1kind: PersistentVolumeClaimmetadata: { name: pf-central-state }spec: accessModes: [ReadWriteOnce] resources: { requests: { storage: 2Gi } }---apiVersion: apps/v1kind: Deploymentmetadata: { name: pf-central }spec: replicas: 1 strategy: { type: Recreate } # two pods must never share the state volume selector: { matchLabels: { app: pf-central } } template: metadata: { labels: { app: pf-central } } spec: securityContext: { runAsNonRoot: true, runAsUser: 65532, fsGroup: 65532 } containers: - name: pf-central image: ghcr.io/danielmichaels/pf-central:0.0.12 envFrom: [{ secretRef: { name: pf-central } }] env: - { name: PFCENTRAL_SERVER_BASE_URL, value: https://pf.example.com } - { name: PFCENTRAL_TRUSTED_ORIGINS, value: https://pf.example.com } - { name: PFCENTRAL_NATS_TLS_ENABLED, value: "true" } - { name: PFCENTRAL_NATS_TLS_HOSTS, value: pf.example.com } - { name: PFCENTRAL_LOG_LEVEL, value: info } ports: - { name: http, containerPort: 2808 } - { name: nats, containerPort: 24222 } startupProbe: { tcpSocket: { port: http }, periodSeconds: 5, failureThreshold: 30 } livenessProbe: { tcpSocket: { port: http } } readinessProbe: { tcpSocket: { port: http } } volumeMounts: [{ name: state, mountPath: /data }] volumes: - { name: state, persistentVolumeClaim: { claimName: pf-central-state } }---apiVersion: v1kind: Servicemetadata: { name: pf-central-nats }spec: type: LoadBalancer # agents connect here; TCP passthrough, no HTTP proxy selector: { app: pf-central } ports: [{ name: nats, port: 24222, targetPort: nats }]---apiVersion: v1kind: Servicemetadata: { name: pf-central-http }spec: selector: { app: pf-central } ports: [{ name: http, port: 80, targetPort: http }]apiVersion: apps/v1kind: Deploymentmetadata: { name: pf-central }spec: replicas: 2 strategy: { type: RollingUpdate } selector: { matchLabels: { app: pf-central } } template: metadata: { labels: { app: pf-central } } spec: securityContext: { runAsNonRoot: true, runAsUser: 65532 } containers: - name: pf-central image: ghcr.io/danielmichaels/pf-central:0.0.12 envFrom: [{ secretRef: { name: pf-central } }] env: - { name: PFCENTRAL_SERVER_BASE_URL, value: https://pf.example.com } - { name: PFCENTRAL_TRUSTED_ORIGINS, value: https://pf.example.com } - { name: PFCENTRAL_NATS_MODE, value: external } - { name: PFCENTRAL_NATS_URL, value: tls://nats.example.com:4222 } - { name: PFCENTRAL_NATS_CREDS_FILE, value: /nats/app.creds } - { name: PFCENTRAL_NATS_CALLOUT_CREDS_FILE, value: /nats/callout.creds } - { name: PFCENTRAL_NATS_SENTINEL_CREDS_FILE, value: /nats/sentinel.creds } - { name: PFCENTRAL_NATS_SIGNING_KEY_PATH, value: /nats/agent-signing.nk } - { name: PFCENTRAL_LOG_LEVEL, value: info } ports: [{ name: http, containerPort: 2808 }] startupProbe: { tcpSocket: { port: http }, periodSeconds: 5, failureThreshold: 30 } livenessProbe: { tcpSocket: { port: http } } readinessProbe: { tcpSocket: { port: http } } volumeMounts: - { name: state, mountPath: /data } - { name: nats, mountPath: /nats, readOnly: true } volumes: - { name: state, emptyDir: {} } - { name: nats, secret: { secretName: pf-central-nats } }Agents connect to your broker, not to the pods, so there is no NATS Service.
Put your usual Ingress or Gateway in front of the HTTP Service for HTTPS, and set its read timeout to at least an hour so the UI’s live updates are not cut off. See HTTPS for the web UI.
Probes
Section titled “Probes”pfCentral has no health endpoint yet. Use TCP probes on the HTTP port, as above. / answers with a redirect to /login, which also counts as healthy for an HTTP probe. The startupProbe gives the first start time to run database migrations.
How several replicas share the work
Section titled “How several replicas share the work”- Migrations and first-run bootstrap take a PostgreSQL advisory lock, so two pods starting at once cannot both run them.
- Sessions live in PostgreSQL, so any replica serves any user.
- The authorization callout runs on every replica in a NATS queue group; one replica answers each login.
- Telemetry and presence: every replica reads the same JetStream stream and buckets, so every replica shows the full fleet.
- Bring your own NATS for the HA shape.
- Back up and restore: back up the PVC and the database together.