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:preflightIf anything is missing, the task prints install hints (brew install k3d,
brew install devspace, Docker Desktop).
Bring up the cluster
task cluster:deployThis single target:
- Runs the preflight check.
- Creates a k3d cluster named
octofromdeploy/k3d/cluster.yaml(one server, no agents), mounting./datafrom the repo root onto/var/lib/octo-datain the node. That is the fixed path the chart's static PersistentVolume binds, so the database lives at./data/pgdataand outlives the cluster. See Database persistence. - 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. - 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-embeddingsandocto-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 withk3d image import, so no registry is needed, andtask cluster:images:verifychecks each one landed. - Installs the Helm chart via
devspace deploy, usinghelm/values-k3d.yamlfor the local shape: no registry,:devimages, and NodePorts instead of an Ingress. Single sign-on is required here as everywhere else: put your development provider'sOIDC_*settings andAUTH_SECRETin the repo root.env, whichdevspace deployreads. The schema Job runs as a Helm hook on every install and upgrade. - Restarts the platform, orchestrator, observability and embeddings Deployments
so they pick up freshly imported images (the
:devtag 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:
- Editor UI: http://localhost:3000
- Orchestrator: http://localhost:8090/healthz
(and
/db-version)
Check pod status with:
kubectl --context k3d-octo get pods -n octo-devHot-reload dev loop
task cluster:devThis 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 devimage and live-syncsapps/platform/into it, so edits hot-reload in the browser. - Swaps the orchestrator and observability pods for
go runimages, syncingorchestrator/andobservability/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-devTear down
task cluster:stopThis 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:resetThe 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) |
| Chart | static 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.