Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Deploy on Kubernetes

Orion ships an official Helm chart that deploys the cluster topology in one command. You get N stateless replicas in cluster mode behind a Service, with one shared PostgreSQL or MySQL and one shared Redis. A pre-upgrade migration Job runs first, and rolling deploys surge rather than dip.

Every release publishes the chart to GHCR as an OCI artifact, so there is no chart repository to add.

Before you start

You need a Kubernetes cluster, Helm 3, kubectl, and access to GHCR. A production install also needs reachable PostgreSQL or MySQL and Redis services, an admin API key, and permission to create workloads, Services, Jobs and Secrets. The commands below pin chart 1.0.0, matching the chart version in this checkout. Choose the chart version that matches the Orion release you intend to deploy.

The chart installs with ORION_ENVIRONMENT=production by default, which enforces admin auth and refuses permissive CORS at boot. A bare helm install does not come up until you provide admin API keys, or opt into the dev stack. The failure is loud at install time rather than silent in production.

Try it with the dev stack

For a throwaway install, devStack.enabled=true runs a single-replica in-namespace PostgreSQL and Redis with no persistence guarantees, and wires Orion to them. Dev-stack installs run as development, so no admin keys are required:

helm install orion oci://ghcr.io/goplasmatic/charts/orion \
  --version 1.0.0 --set devStack.enabled=true
kubectl port-forward svc/orion 8080:8080
open http://localhost:8080/docs

Never use the dev stack in production. The bundled PostgreSQL and Redis are disposable.

Install for production

A production install requires three inputs: a database URL, a Redis URL, and at least one admin API key. Inline is quickest:

helm install orion oci://ghcr.io/goplasmatic/charts/orion --version 1.0.0 \
  --set storage.url="postgres://orion:secret@my-postgres:5432/orion" \
  --set cluster.redisUrl="redis://my-redis:6379" \
  --set adminAuth.apiKeys="{$(openssl rand -hex 32)}"

Pre-existing Secrets keep credentials out of Helm values and release history. The chart reads the storage-url key from storage.existingSecret and the api-keys key, a comma-separated list, from adminAuth.existingSecret:

kubectl create secret generic orion-storage \
  --from-literal=storage-url="postgres://orion:secret@my-postgres:5432/orion"
kubectl create secret generic orion-admin-auth \
  --from-literal=api-keys="$(openssl rand -hex 32)"

Then a minimal values.yaml:

storage:
  existingSecret: orion-storage      # Secret key: storage-url
cluster:
  redisUrl: redis://my-redis:6379    # shared dedup / response cache / rate limits
adminAuth:
  existingSecret: orion-admin-auth   # Secret key: api-keys (comma-separated)

# Only needed for browser clients of the admin API, such as the Orion Console.
# Empty means no cross-origin access; "*" is refused in production at boot.
cors:
  allowedOrigins:
    - https://console.example.com

ingress:
  enabled: true
  className: nginx
  hosts:
    - host: orion.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: orion-tls
      hosts:
        - orion.example.com

Install with it:

helm install orion oci://ghcr.io/goplasmatic/charts/orion \
  --version 1.0.0 -f values.yaml

Admin requests then need the key:

curl -H "Authorization: Bearer <key>" https://orion.example.com/api/v1/admin/engine/status

TLS terminates at the Ingress in this setup. The Ingress routes only the main HTTP port; the metrics listener is deliberately not exposed.

Notable values

The important subset. The chart’s values.yaml is the full annotated list:

ValueDefaultMeaning
replicaCount2Replicas (ignored when autoscaling.enabled)
image.repository / image.tagghcr.io/goplasmatic/orion / chart appVersionServer image; empty tag tracks the chart’s app version
envproductionOrion environment; any prod* value enforces admin auth and refuses a CORS wildcard
storage.url / storage.existingSecretDatabase URL (required unless devStack); the Secret’s storage-url key wins over the inline URL
storage.autoMigratefalseReplicas never migrate at boot; refused as true on a production cluster install
cluster.enabledtrueMulti-instance coordination (dedup, response cache, rate limits through Redis)
cluster.redisUrlShared Redis (required when cluster.enabled unless devStack)
adminAuth.apiKeys / adminAuth.existingSecretAdmin API keys (required unless devStack); Secret key api-keys, comma-separated
cors.allowedOrigins[]Browser origins for the admin API (empty = deny)
migrateJob.enabledtruePre-install/pre-upgrade orion-server migrate Job (backoffLimit: 3)
server.shutdownDrainSecs15Keep serving after readiness is withdrawn on SIGTERM
server.shutdownForceTimeoutSecs20Bound on the post-drain in-flight wait
metrics.enabledtruePrometheus metrics on a dedicated listener
metrics.port9090Metrics container/Service port (separate from server.port 8080)
metrics.serviceMonitor.enabledfalsePrometheus Operator ServiceMonitor (needs the CRD; set labels to match your serviceMonitorSelector)
metrics.podMonitor.enabledfalsePodMonitor alternative; works with metrics.service.enabled=false
metrics.prometheusAnnotationsfalseprometheus.io/* pod annotations for annotation-based discovery
ingress.enabledfalseIngress for the main HTTP port only (never the metrics port)
resources250m CPU / 256Mi req, 512Mi limitContainer resources
autoscaling.enabledfalseCPU-based HPA (min 2, max 6, target 75%)
podDisruptionBudget.enabledtruemaxUnavailable: 1 during voluntary disruptions
networkPolicy.enabledfalseIngress on the HTTP/metrics ports + egress rules you declare; with no egress rules the pod gets DNS and nothing else (fail-closed). The network-level pairing for allow_private_urls
strategyRollingUpdate, maxUnavailable: 0, maxSurge: 1Deploys never drop below replicaCount Ready replicas
persistence.enabledfalsePVC at /app/data for single-node SQLite installs
extraEnv[]Additional ORION_* overrides (see Server configuration)
devStack.enabledfalseThrowaway in-namespace PostgreSQL + Redis; dev/demo only
tests.enabledtrueRender the helm test hooks

Misspelled values fail the render. The chart enforces values.schema.json on every install, upgrade and template, with unknown keys rejected. Every required value on this chart is a string, so without the schema a typo like --set cluster.enabld=true would silently no-op; instead it fails at once.

The pods run under a restricted security posture by default: non-root (UID 10001), read-only root filesystem, all capabilities dropped, RuntimeDefault seccomp. /tmp is an emptyDir, and persistence.mountPath (default /app/data) is the only durable writable path.

The metrics listener is dedicated and unauthenticated by design. On the main listener /metrics sits behind admin auth, and a scraper should not hold a credential that can also rewrite workflows. Keep it cluster-internal, or turn it off with metrics.enabled=false; the alerts in What to alert on then have no scrape target.

Plugins and schedules

Neither has a chart value. Both are ORION_* overrides through extraEnv, which every replica shares, and each has one consequence worth planning for:

extraEnv:
  - name: ORION_PLUGINS__ENABLED
    value: "true"
  - name: ORION_CRON__ENABLED
    value: "false"

Plugins are off by default. Turning them on makes the sandbox’s pooling allocator reserve max_live_instances × max_memory_bytes of address space at startup, 16 GiB with the defaults. It is virtual, not resident, so it does not belong in resources.requests. It does matter wherever a container limits virtual memory.

The cron scheduler is on by default, and every replica must agree. A mixed setting quarantines an active cron channel on the replicas that have it off and runs it on the rest. That is visible as components.cron: degraded on /health, but it is not what anyone meant. Nothing else needs configuring for a multi-replica install. Occurrence identity, claim leases and singletons all live in the shared database, and there is no leader to elect.

Upgrade

Upgrade the release, keeping the values you set:

helm upgrade orion oci://ghcr.io/goplasmatic/charts/orion \
  --version <new-version> --reuse-values
  • Migrations run as a pre-install and pre-upgrade Job (<release>-migrate), before any new pod starts. Replicas boot with storage.auto_migrate=false and refuse to start on a pending migration, so a failed migration stops the rollout rather than booting mismatched replicas.
  • Schema changes follow the expand and contract convention across one release. The old replicas keep serving against the migrated schema while the new ones roll in, which is what makes a rolling upgrade with maxUnavailable: 0, maxSurge: 1 safe.
  • Shutdown is graceful by construction. On SIGTERM a replica withdraws readiness, keeps serving for server.shutdownDrainSecs, then waits up to server.shutdownForceTimeoutSecs for in-flight requests. terminationGracePeriodSeconds is derived as drain plus force timeout plus 10, so the kubelet never cuts the sequence short.

Verify

The chart ships helm test hooks, inert until run:

helm test orion

test-connectivity runs the binary’s own test-connectivity subcommand: it opens the storage pool, counts pending migrations, and probes Kafka when enabled. test-api checks /health and /readyz, and that the metrics port serves Prometheus exposition text without a credential. To inspect by hand:

kubectl get pods -l app.kubernetes.io/name=orion,app.kubernetes.io/instance=orion
kubectl port-forward svc/orion 8080:8080
curl -s http://localhost:8080/readyz     # 200 once the engine is built
curl -s http://localhost:8080/health     # component detail (database, engine)

Troubleshooting

A pod is not Ready and has not restarted

Boot is still in progress. The startup probe budgets up to 5 minutes before liveness kicks in. That covers the pending-migration check, the cluster Redis connect, connector loading and the engine build. kubectl logs shows which stage it is in.

A pod crash-loops right after install

Most often a missing required input. With env=production, the default, Orion refuses to boot without admin keys, with a CORS wildcard, or with storage.autoMigrate=true on a cluster install. The log line names the offending setting.

Replicas refuse to start after helm upgrade

A pending migration. Check the <release>-migrate Job’s logs with kubectl logs job/orion-migrate.

Nothing is scraping metrics

The install notes say so explicitly. Enable metrics.serviceMonitor (Operator), metrics.podMonitor, or metrics.prometheusAnnotations.

Single-node SQLite

For a small single-node install the chart can run Orion against embedded SQLite on a PVC instead of an external database:

helm install orion oci://ghcr.io/goplasmatic/charts/orion --version 1.0.0 \
  --set storage.url="sqlite:/app/data/orion.db" \
  --set persistence.enabled=true \
  --set cluster.enabled=false \
  --set replicaCount=1 \
  --set strategy.type=Recreate \
  --set migrateJob.enabled=false \
  --set storage.autoMigrate=true \
  --set adminAuth.apiKeys="{$(openssl rand -hex 32)}"

The combination matters. A ReadWriteOnce claim cannot serve a surge replica, hence Recreate and one replica. A hook Job cannot share the replica’s volume, hence boot-time migration instead of the Job. Backups then land under /app/data/backups; see Back up and restore.

Next steps

Last verified 14 September 2026