Octov0.11.7
Deployment

Install on Kubernetes

A complete, start-to-finish install of the Octo platform on any Kubernetes cluster, with no credential in a values file.

This is the whole install, in order, on a cluster you already have, with no cloud provider assumed and no Terraform involved. It ends with a working editor on your own hostname, TLS, and every credential held in a Kubernetes Secret rather than in a values file.

Helm Chart is the reference for what each value means. This page is the path through it.

Before you start

You need:

  • A Kubernetes cluster, 1.26 or newer, and kubectl pointing at it.
  • Helm 3.8 or newer; earlier versions cannot pull an OCI chart.
  • An ingress controller and a hostname you can point at it. Any controller works; the chart writes an ordinary Ingress.
  • A default StorageClass, for the Postgres volume. kubectl get storageclass should show one marked (default).
  • Roughly 4 GB of allocatable memory free across the cluster for the bundled stack (the editor, orchestrator, observability service, Postgres, NATS and Redis), plus whatever your integrations need.

Optional, and worth deciding now rather than later:

  • cert-manager, if you want the chart to get certificates for you. Without it you supply a TLS Secret yourself, or terminate TLS upstream.
  • A wildcard DNS record, if you want each deployed integration published at its own public hostname. See Publish your integrations.

Trying Octo rather than deploying it? The standalone editor runs from a single image with no cluster at all (see Running from Docker), and Local Cluster (k3d) brings the full platform up on a laptop in one command.

The install

Choose a namespace and create it

The credentials have to exist before the chart that reads them, and a Secret is only readable by workloads in its own namespace, so the namespace comes first:

kubectl create namespace octo

Everything below assumes octo. Use whatever name you like.

Mint the credentials

Three secrets, generated once, on your machine. Nothing about them comes from Octo: they are random bytes with specific shapes. (The auth and embeddings Secrets come later, in the steps that turn those features on.)

# Postgres password for the bundled database
kubectl -n octo create secret generic octo-db-password \
  --from-literal=postgres-password="$(openssl rand -hex 24)"

# AES-256 key encrypting KV secret namespaces at rest: base64 of 32 raw bytes
kubectl -n octo create secret generic octo-kv-key \
  --from-literal=kv-encryption-key="$(openssl rand -base64 32)"

# HMAC key deriving every dev run's identity and public hostname
kubectl -n octo create secret generic octo-devrun-key \
  --from-literal=dev-run-hash-secret="$(openssl rand -base64 32)"

The names matter. They must not be the ones the chart uses for Secrets of its own: {release}-postgres, {release}-auth, {release}-kv, {release}-devruns, {release}-embeddings. {release}-postgres collides on install, since the chart creates it whatever the password's source (it also carries the username and database name). The others collide on migration, deleting the Secret you just pointed the release at. The names used here are clear of all five.

Back up the KV key and the dev-run key before you go any further, and never rotate either one.

A new kv-encryption-key makes every value already written to a secret KV namespace permanently unreadable. A new dev-run-hash-secret re-labels every exposed dev run, so a webhook registered against the old hostname silently stops being delivered, with no error anywhere. The Postgres password is the ordinary kind and can be rotated; the other two cannot. Put them somewhere durable now, while there is nothing to lose.

Write a values file

One file, and it holds no credentials: only the names of the Secrets you just created.

octo-values.yaml
ingress:
  # The hostname you will point at your ingress controller.
  host: octo.example.com
  # The controller that should claim this Ingress. Omit it only if your cluster
  # marks one IngressClass default (`kubectl get ingressclass` says).
  className: nginx
  tls:
    # cert-manager issues a certificate per host over HTTP-01. See step 5 for
    # the alternatives.
    mode: cert-manager
    clusterIssuer: letsencrypt-prod
    secretName: octo-tls

postgres:
  auth:
    existingSecret: octo-db-password
    existingSecretPasswordKey: postgres-password

kv:
  existingSecret: octo-kv-key
  existingSecretKey: kv-encryption-key

orchestrator:
  devRuns:
    existingSecret: octo-devrun-key
    existingSecretKey: dev-run-hash-secret

That is a complete install. NATS, Redis, the observability service, the retention sweep and the schema job are on by default and need nothing said about them.

References, rather than the values themselves. Helm keeps every value it is given in the release history, so a password written into this file lands in the release Secret in the cluster and stays there for every retained revision. --set and --set-file are the same value by another route. A reference is a name: the chart never sees the value, and the pod reads it from your Secret at start-up.

Install the chart

helm install octo oci://ghcr.io/juancavallotti/charts/octo \--version 0.11.7 \--namespace octo \--values octo-values.yaml

No --create-namespace: you made it in step 1, and Helm creating it would mean the Secrets had nowhere to go.

The chart pulls its images from Docker Hub by default, at the version of the chart itself: image.tag falls back to appVersion, so a chart installs the images of its own release rather than drifting onto latest. To pull from your own registry, add image.registry and, for a private one, defaults.imagePullSecrets.

Watch it come up:

kubectl -n octo get pods -w

Six pods reach Running, plus a schema Job that runs once and completes. If Postgres stays Pending, its volume has nowhere to come from; check kubectl get storageclass and kubectl -n octo describe pvc.

Point DNS at it, and settle TLS

Find the address your controller published, and create an A (or CNAME) record for ingress.host pointing at it:

kubectl -n octo get ingress

TLS is one of five modes, and which one you want is a property of your cluster rather than of Octo:

ingress.tls.modeWhat it doesYou need
cert-managerAnnotates the Ingress with a ClusterIssuer; cert-manager issues per host over HTTP-01cert-manager, a ClusterIssuer, and DNS already resolving
secretReferences a TLS Secret that already existsTo create that Secret yourself
gke-managed-certCreates a ManagedCertificate for the GCE ingress controllerGKE
acmTerminates at an ALB against a certificate by ARNEKS with the AWS Load Balancer Controller
noneNo TLS on the Ingress; something upstream terminates itA load balancer or mesh that does

With cert-manager, DNS must resolve before the certificate can be issued, since the HTTP-01 challenge is a request to your own hostname. The first minute after DNS lands may serve the controller's default certificate; watch the real one arrive with:

kubectl -n octo get certificate

Open the editor

Visit https://octo.example.com in a browser.

The editor is open to anyone who can reach it at this point; step 7 is how you close it. If the host resolves to nothing, the usual cause is that no controller claimed the Ingress. kubectl -n octo describe ingress shows which class it asked for; kubectl get ingressclass shows what the cluster has.

Configure single sign-on

Signing in is how anybody reaches the editor — there is no open mode, and the chart refuses to render without the settings below. Any provider that speaks the authorization-code flow works. Register a client with yours, with this redirect URI:

https://octo.example.com/api/auth/callback/oidc

Then create the auth Secret, holding both credentials, because the chart reads them from one reference:

kubectl -n octo create secret generic octo-auth-creds \
  --from-literal=oidc-client-secret='<the client secret your provider issued>' \
  --from-literal=auth-secret="$(openssl rand -base64 32)"

and add to your values file:

octo-values.yaml
auth:
  oidc:
    issuer: https://id.example.com
    clientId: octo
    # Optional: what the sign-in button calls your provider.
    providerName: Acme SSO
  existingSecret: octo-auth-creds
  # Optional: restrict writes to these roles. Empty means every role except
  # platform:monitor.
  writeRoles: ""

auth-secret signs session cookies. Unlike the two keys in step 2 it is recoverable: rotating it signs everyone out and nothing worse.

Upgrade to apply it:

helm upgrade octo oci://ghcr.io/juancavallotti/charts/octo \--version 0.11.7 --namespace octo --values octo-values.yaml

MCP clients need a provider that supports Dynamic Client Registration. Without it they cannot reach /mcp at all — it accepts only access tokens the provider minted. See MCP authentication.

Publish your integrations

Each integration you deploy from the editor can get its own public hostname under a domain you nominate. Add:

octo-values.yaml
orchestrator:
  # Integrations are published at {slug}.apps.example.com.
  baseDomain: apps.example.com
  # The controller that claims the Ingress the orchestrator creates per
  # deployment. Usually the same one as ingress.className, and it is also what
  # the branded 502/503 page is selected from, so name it even where the cluster
  # has a default IngressClass.
  ingressClass: nginx

wildcardTLS:
  # One *.apps.example.com certificate rather than one per subdomain.
  enabled: true
  clusterIssuer: letsencrypt-dns
  secretName: octo-wildcard-tls

Then point *.apps.example.com at the same ingress address.

Prefer the wildcard over per-host certificates, especially with dev runs on. A per-host certificate puts every hostname you deploy into public Certificate Transparency logs; one wildcard publishes the domain and nothing under it. The wildcard needs a DNS-01 ClusterIssuer, because HTTP-01 cannot issue a wildcard, which means giving cert-manager credentials for the zone.

Leaving baseDomain unset is supported: integrations still deploy and work, reachable in-cluster, with no external address.

Turn on semantic memory search (optional)

With an embedding server, searching agent memory ranks by meaning; without one it ranks by matching text. Both are supported.

kubectl -n octo create secret generic octo-embeddings-key \
  --from-literal=apiKey='<your provider key>'
octo-values.yaml
embeddings:
  enabled: true
  connectorType: llm-openai   # or llm-gemini, llm-openrouter
  model: text-embedding-3-small
  dimensions: 1536
  existingSecret: octo-embeddings-key
  existingSecretKey: apiKey

The key is held in this one pod, rather than in every integration's pod.

dimensions must match the vector(N) columns in the schema, which are indexed and fixed at schema time. And do not change model on an installation that has embedded anything: vectors carry no record of which model produced them, so a store holding two models' vectors cannot be ranked coherently. Changing it means re-embedding everything, at whatever your provider charges.

What you have now

ComponentWhat it isNotes
PlatformThe editor UI, at ingress.hostProxies the orchestrator and logs APIs through its BFF
OrchestratorDeploys integrations through the Kubernetes APINever exposed directly; reached through the editor
Observability serviceConsumes logs and traces into Postgres; serves pod stats from RedisServes /platform/logs, /platform/traces and /platform/metrics
PostgresThe bundled database, on a PVCRetain policy, so helm uninstall does not delete it
NATSPub-sub between components
RedisShared state across replicasRequired; the observability service will not start without one
Schema JobApplies sql/schema.sqlRe-runs idempotently on every upgrade
Retention CronJobDeletes logs and traces past their policyNightly

Verify end-to-end by building a flow in the editor and deploying it. Your first flow is the shortest path.

Production shape

The install above is already the right shape. What separates it from a long-running deployment is mostly what you point it at.

A managed database. The bundled Postgres is one replica with no backups you did not arrange. Point the chart at RDS, Cloud SQL or your own cluster instead:

octo-values.yaml
postgres:
  enabled: false
externalDatabase:
  host: octo.abcdef.us-east-1.rds.amazonaws.com
  database: octo
  user: octo
  sslmode: require
  existingSecret: rds-octo
  existingSecretPasswordKey: password

The schema Job then runs against that host. postgres.enabled: false skips the StatefulSet, its Service and its Secret entirely.

A managed Redis. Same idea. A Redis with a password must arrive as a Secret, not a URL in a values file, because the URL contains the credential:

kubectl -n octo create secret generic octo-redis \
  --from-literal=redis-url='rediss://default:<password>@cache.example:6380'
octo-values.yaml
redis:
  enabled: false
externalRedis:
  existingSecret: octo-redis
  existingSecretKey: redis-url

Resources and replicas. The chart's defaults are unopinionated. Set them once, for every workload, in defaults, and override per component:

octo-values.yaml
defaults:
  resources:
    requests: { cpu: 100m, memory: 256Mi }
    limits:   { memory: 1Gi }
postgres:
  resources:
    requests: { cpu: 250m, memory: 1Gi }
platform:
  replicas: 2
orchestrator:
  replicas: 2

A restricted security context, if your cluster enforces Pod Security Standards. defaults.podSecurityContext and defaults.securityContext apply to every workload at once. The GKE and EKS profiles set the restricted baseline and are worth reading as examples.

Upgrading

helm upgrade octo oci://ghcr.io/juancavallotti/charts/octo \--version 0.11.7 --namespace octo --values octo-values.yaml

Keep passing --values: --reuse-values and a values file do not compose, and the file is the record of what this release is.

A chart-version bump moves the images with it, so the Deployments roll, the schema Job re-runs idempotently, and Postgres and its volume are untouched. Integrations already deployed keep running on the runtime image they were deployed with, so redeploy them from the editor to pick up a newer one. See Releases and Upgrades.

Rotating a credential

Every credential is a Secret you own, so rotation does not involve Helm:

kubectl -n octo create secret generic octo-db-password \
  --from-literal=postgres-password='<new password>' \
  --dry-run=client -o yaml | kubectl apply -f -
kubectl -n octo rollout restart deploy,statefulset

Two exceptions, from step 2: never rotate kv-encryption-key or dev-run-hash-secret. Changing the first destroys every value in a secret KV namespace; changing the second breaks every webhook registered against a dev-run hostname.

Uninstalling

helm uninstall octo --namespace octo

The database survives on purpose: the StatefulSet sets persistentVolumeClaimRetentionPolicy: Retain, so the claim outlives the release and the next install adopts it. Deleting the namespace deletes the claim, the volume and your Secrets. That is the irreversible step:

kubectl delete namespace octo

When something is wrong

SymptomWhere to look
Pods Pendingkubectl -n octo describe pod; usually no StorageClass, or not enough allocatable memory
Hostname resolves to nothingkubectl -n octo describe ingress, then kubectl get ingressclass. An Ingress no controller claims is not an error anywhere
Certificate never appearskubectl -n octo describe certificate. HTTP-01 needs DNS resolving first; a wildcard needs DNS-01
Editor loads, sign-in fails at the providerThe redirect URI must match {auth.url}/api/auth/callback/oidc exactly, scheme included. Set auth.url explicitly if the chart cannot know your scheme
Writes to a secret KV namespace rejectedNo kv.existingSecret (or kv.encryptionKey). Plain KV keeps working, which is why this is quiet
Editor's Run says dev runs unavailableorchestrator.devRuns needs its Secret; the chart refuses to render without one, so this means the feature was turned off
A deployed integration's hostname 404sorchestrator.baseDomain and the wildcard DNS record; the catch-all page only renders for a controller the chart can identify

Next

On this page