Guide

Deployment

Run a flow from the CLI on your laptop, ship the editor as a container, spin up a local Kubernetes cluster, or stand up the full managed stack on GCP with one Terraform apply. This guide covers each path and how an integration's endpoints become public or stay private.

Full deployment.md ↗

Where it runs

Four ways to run Octo.

From smallest to largest. The runtime is a single Go binary; the editor and orchestrator are the platform around it.

⌨️

Local CLI

Build the binary once, then run any config with it — no Go toolchain at run time, no cluster, no editor. Great for developing a flow.

task build    # compiles ./bin/octo
./bin/octo --config samples/http-orders.yaml --watch
🐳

Editor in Docker

The editor image bundles the Next.js app and the octo binary, so the RUN feature works in-container.

# builds from the repo root (runtime + editor)
docker build -f editor/Dockerfile -t octo-editor .
docker run -p 3000:3000 octo-editor
☸️

Local k3d

A local Kubernetes cluster with the full stack (editor + orchestrator + Postgres) via DevSpace, with optional hot reload.

task cluster:deploy   # bring up k3d + apply manifests
task cluster:dev      # add hot reload
☁️

Managed GCP

Terraform stands up a single-node k3s VM, DNS, TLS, and installs the Helm chart. Releases are a tagged Helm upgrade.

task state:bucket PROJECT=<project>   # once
task infra:apply                      # VM, DNS, certs
task deploy TAG=v0.1.4                 # Helm release
What Terraform owns. The VM only bootstraps k3s, cert-manager, and the ingress controller (Traefik). Terraform — not the VM — owns the Helm release, so a deploy is a declarative upgrade and state lives in a GCS bucket.
On a running platform

Shipping an integration.

On a running Octo platform the editor and orchestrator are the control plane (backed by Postgres); deploying an integration turns its YAML into running pods. Four steps:

Create the integration

Author flows in the editor and save. The integration — a name + the definition YAML — is stored by the orchestrator in Postgres and reopens at /i/<id>.

Add secrets

Create cluster secrets (UPPER_SNAKE_CASE) in the editor. Values are written straight into one Kubernetes Secret (octo-secrets) and are never read back — the catalog keeps only names and timestamps.

Configure & deploy

In the deploy dialog choose replicas, an internal slug, and whether to expose it. Bind each declared env var to a literal value or a secret. HTTP_PORT / HTTP_HOST are managed for you.

It becomes pods

The orchestrator renders Kubernetes objects and streams status back live. Scale by changing replicas; undeploy tears everything down.

What a deploy creates

For a deployment <id> with slug <slug>, the orchestrator applies:

ObjectNameWhy
ConfigMapocto-dep-<id>Holds your integration YAML, mounted read-only at /etc/octo/integrations — the runtime loads every file there.
Deploymentocto-dep-<id>Runs the Octo runtime image; replicas → that many pods. Env is injected as literal values or secretKeyRefs into octo-secrets.
Service (ClusterIP)octo-dep-<id>Fronts this deployment's pods.
Service (ClusterIP)octo-int-<slug>A stable in-cluster address other flows call by name.
Ingressocto-dep-<id>Only when exposed externally: host <subdomain>.<base-domain>, TLS via cert-manager.
Env bindings. Each declared env: entry is bound at deploy time to a literal value or a cluster secret (the secret reference wins). required vars must be bound. HTTP_PORT (which marks the integration as a network service) and HTTP_HOST (0.0.0.0) are supplied by the orchestrator and can't be set by hand. Deploys stream live status over server-sent events, so the editor shows pods going ready in real time.
Public vs private

Exposing an integration.

Deployed integrations are private by default — reachable only inside the cluster. Going public is an explicit opt-in per integration.

🔒 Private (default)

The orchestrator gives every deployment a ClusterIP Service, plus a stable per-integration Service that load-balances across its deployments:

http://octo-int-<slug>.octo-dev:8080

No load balancer, no public DNS — other in-cluster flows and services can reach it, the internet can't.

🌐 Public (opt-in)

An integration becomes externally reachable when all of these hold:

  • it declares an HTTP_PORT env var (so it serves HTTP),
  • you toggle Expose externally in the editor,
  • the platform has a BASE_DOMAIN configured.

The orchestrator then creates a Traefik Ingress at <subdomain>.<base-domain> pointing at the deployment's Service.

HTTP_PORT / HTTP_HOST are managed. The orchestrator supplies these to a deployment (HTTP_HOST is 0.0.0.0); you can't bind them yourself. Declaring HTTP_PORT in the integration's env: is what marks it as a network service eligible for exposure.
TLS & domains

Certificates.

cert-manager issues Let's Encrypt certificates. Two modes:

  • Wildcard via DNS-01 — one cert for *.<domain> (and the apex), issued once via Cloud DNS and shared by the editor and every integration subdomain. Enable with wildcardTLS.enabled.
  • Per-host via HTTP-01 — when wildcard is off, each ingress gets its own cert via the letsencrypt-prod cluster issuer.

DNS: an A record for the apex plus a wildcard A record point every <slug>.<domain> at the node's static IP → Traefik → the integration's Service.

Access control

OIDC SSO.

SSO is opt-in (auth.oidc.enabled); local runs stay open. When enabled it gates the editor via Auth.js (the eetr OIDC provider):

  • browser requests without a session are redirected to sign-in; unauthenticated /api calls get 401,
  • write routes can require roles via AUTH_WRITE_ROLES (read from a configurable id-token claim),
  • the orchestrator stays internal and unauthenticated — only the editor's server-side BFF talks to it, in-cluster.

Register the redirect URI https://<domain>/api/auth/callback/eetr on the identity provider.

Config & secrets

Two kinds of secrets.

Keep platform credentials separate from the secrets your integrations consume.

Platform credentials

The Postgres password, the Auth.js session secret, and the OIDC client secret are generated into the bucket-backed Terraform release state — not a separate secret manager — and the Helm chart materializes them as Kubernetes Secrets for the editor and orchestrator.

Lock down the state bucket. Because these credentials live in release state, the GCS bucket must have versioning on and access tightly restricted.

Integration secrets

The secrets your flows use (API keys, DSNs) are cluster secrets you manage in the editor. Values are written into one Kubernetes Secret (octo-secrets) and never read back; a deployment's env binding references one by key. An integration declares the env it expects — required vars must be bound at deploy time:

env:
  - { name: API_KEY, required: true }
  - { name: HTTP_PORT, default: "8080" }   # marks it a network service
Implemented vs. roadmap. Everything above ships today: GCP via Terraform + Helm on k3s, Traefik ingress, cert-manager (HTTP-01 and wildcard DNS-01), the create → secrets → deploy workflow with live status, per-deployment and stable internal Services, opt-in public ingress, OIDC SSO for the editor, and state-backed secrets. Each deploy is independent — integration versioning and promote/rollback are coming soon; scaling (replica count) is the only in-place change today. Cloud Run, IAP, and API-gateway policies are not part of the current stack.