Octov0.11.7
Deployment

Local Cluster (k3d)

Run the full platform locally with k3d and DevSpace.

For day-to-day development the full platform runs on a local k3d cluster (real k3s inside Docker) with images built locally and no registry, GCP, or Terraform involved. One task brings the whole stack up; a second adds a hot-reload dev loop via DevSpace.

Prerequisites

You need docker, k3d, and devspace on your PATH, plus task to run the targets and kubectl to poke at the cluster. Verify the tools:

task cluster:preflight

If anything is missing, the task prints install hints (brew install k3d, brew install devspace, Docker Desktop).

Bring up the cluster

task cluster:deploy

This single target:

  1. Runs the preflight check.
  2. Creates a k3d cluster named octo from deploy/k3d/cluster.yaml (one server, no agents), mounting ./data from the repo root onto /var/lib/octo-data in the node. That is the fixed path the chart's static PersistentVolume binds, so the database lives at ./data/pgdata and outlives the cluster. See Database persistence.
  3. Cross-compiles every Go binary the images need for the node's architecture into dist/bin/linux_<arch>/ (task bins:linux), on the host, where the Go build cache persists between runs.
  4. Builds the eleven images locally (task cluster:images): octo-platform, octo-orchestrator, octo-observability, octo-schema, octo-runtime, octo-devsidecar, octo-statssidecar, octo-devruntime, octo-agenticrunner, octo-embeddings and octo-api, all tagged :dev, copying in the binaries from the step above rather than compiling them again per image (see Where the Go binaries come from). They are then loaded straight into the cluster's containerd with k3d image import, so no registry is needed, and task cluster:images:verify checks each one landed.
  5. Installs the Helm chart via devspace deploy, using helm/values-k3d.yaml for the local shape: no registry, :dev images, and NodePorts instead of an Ingress. Single sign-on is required here as everywhere else: put your development provider's OIDC_* settings and AUTH_SECRET in the repo root .env, which devspace deploy reads. The schema Job runs as a Helm hook on every install and upgrade.
  6. Restarts the platform, orchestrator, observability and embeddings Deployments so they pick up freshly imported images (the :dev tag never changes between runs).

The target is idempotent: re-run it after code changes to rebuild the images and roll the pods. The embedding server is deployed only when EMBEDDINGS_API_KEY is set in your shell; without it agent-memory search matches text rather than meaning.

Local development runs the same Helm chart as every other target, so a change to the chart is exercised locally before it reaches a cluster. Every cluster:* task pins the kube context to k3d-octo, so a stray current-context pointing at a remote cluster can never be the target of a deploy or purge.

Access the stack

The k3d load balancer maps localhost ports to NodePort services, so nothing needs a long-running port-forward:

Check pod status with:

kubectl --context k3d-octo get pods -n octo-dev

Hot-reload dev loop

task cluster:dev

This ensures the cluster is up (cluster:deploy), builds the dev-flavored images (cluster:images:dev, from the Dockerfile.dev files), then hands off to devspace dev, which:

  • Swaps the platform pod for a next dev image and live-syncs apps/platform/ into it, so edits hot-reload in the browser.
  • Swaps the orchestrator and observability pods for go run images, syncing orchestrator/ and observability/ and restarting the container on each upload so Go recompiles.
  • Tails the platform, orchestrator, observability, Postgres, and NATS logs to your terminal.

The session stays attached until you press Ctrl-C. The NodePort mappings keep working during dev mode, so the editor stays on localhost:3000. To restore the production-image pods afterwards:

devspace reset pods -n octo-dev

Tear down

task cluster:stop

This purges the DevSpace deployment and deletes the k3d cluster, reclaiming Docker memory. It keeps ./data, so Postgres comes back with its data on the next task cluster:deploy.

To wipe the database as well:

task cluster:db:reset

The task refuses to run while the cluster is up (Postgres holds the data directory open), prompts for confirmation, then deletes ./data. The next task cluster:deploy recreates Postgres and re-applies the schema fresh.

task cluster:db:reset permanently deletes all local Postgres data: integrations, deployments, logs, and KV contents.

Database persistence

task cluster:stop deletes the whole k3d cluster, and with it every Kubernetes object, including the claim Postgres writes to. For the data to survive, the claim has to resolve to a path that does not change when the cluster is recreated, which is what postgres.storage.hostPath in helm/values-k3d.yaml gives it:

Repo./data/pgdata
Node/var/lib/octo-data/pgdata (mounted by task cluster:deploy)
Chartstatic PersistentVolume, Retain, pre-bound to data-octo-postgres-0

Only task cluster:db:reset removes it. Nothing in the chart can: the StatefulSet sets persistentVolumeClaimRetentionPolicy: Retain so neither helm uninstall nor scaling to zero deletes the claim, and the volume carries helm.sh/resource-policy: keep so uninstalling leaves it in place for the next install to adopt.

This is why local dev does not use k3s's local-path provisioner. That provisioner names each volume's directory after the claim's UID (pvc-3b9766b4-…_octo-dev_data-octo-postgres-0), and a recreated cluster mints a new UID, so every task cluster:stop would quietly start you on an empty database while the old directory accumulated on disk. The same applies to any single-node cluster, including a k3s VM. On a multi-node cluster use a StorageClass instead and leave hostPath empty.

On this page