Local Development Cluster - ericfitz/tmi GitHub Wiki

Local Development Cluster

This page is the contributor onramp for the cluster-based TMI development environment. make dev-up deploys the TMI server and the full component platform (Redis, NATS JetStream, KEDA, the TMIComponent controller, and the extractor/chunk-embed workers) into a local Kubernetes cluster, backed by an in-cluster Postgres (or an external Oracle Autonomous Database with DB=oracle).

⚠️ Breaking change: the host-process dev path is retired

make dev-up no longer runs the server and workers as host Go processes. It now builds container images, delivers them to the cluster you name with CLUSTER= (imported into the Docker Desktop node, or pushed to the k3s in-cluster registry), and deploys them there. There is no longer a host tmi-server process or a make start-server step in the dev loop.

If you have an old workflow that expected dev-up to launch a bare Go binary, that path is gone. The standalone manage-database.py local Postgres container (make start-database) still exists, but dev-up no longer uses it β€” the dev Postgres now runs inside the cluster (see Database configuration).


How it works

make dev-up deploys into one of two clusters, selected with CLUSTER=. CLUSTER is required β€” the Makefile has no default, and every dev-* target exits with a hint if it is unset (a deliberate guard against deploying to the wrong cluster).

CLUSTER= Use Kube context Image delivery Cluster lifecycle
docker-desktop Laptop, Docker Desktop's built-in Kubernetes docker-desktop Built on the host and imported straight into the node's containerd (docker save | ctr import); no registry Docker Desktop owns it; TMI never creates or deletes it
k3s A dedicated, persistent k3s cluster Your k3s context (context in .local/k3s.json) Pushed to an in-cluster registry at <k3s-node>:<registry-nodeport> (deployed by dev-up) You manage it; TMI never creates or deletes it

Example: make dev-up CLUSTER=k3s DB=postgres.

TMI does not create, own, or delete either cluster, and dev-up does not use kind. (Separately, make e2e-platform-up creates its own kind cluster for platform end-to-end tests. Docker Desktop's Kubernetes node is internally a kind container, desktop-control-plane; that is Docker's implementation detail.) On Docker Desktop, reach the server through the port-forward dev-up starts on localhost:8080.

With DB=postgres (the default) the database is a Postgres StatefulSet in the cluster (svc/postgres), and its data lives on a PVC. With DB=oracle the server dials an external OCI Autonomous Database instead; that is supported on CLUSTER=docker-desktop only.

host: curl / tmi-ux / oauth-stub / postman / wstest
        β”‚  http://localhost:8080  (managed kubectl port-forward β†’ svc/tmi-server)
        β–Ό
   tmi-server (pod)                 config: ConfigMap (your config-development.yml)
        β”‚  database.url (host rewritten) ─► in-cluster svc/postgres (PVC)
        β”‚                                   (DB=oracle: external ADB, wallet Secret mounted)
        β”‚  redis (in-cluster svc) ◄─ TMI_REDIS_HOST=redis (baked into server.yml), TLS + password
        β”‚  TMI_NATS_URL=tls://nats.tmi-platform.svc:4222 (mTLS)
        β–Ό
   NATS JetStream ──► KEDA scales ──► tmi-extractor / tmi-chunk-embed (0β†’N)

Everything lands in the tmi-platform namespace.

In-cluster Redis accepts only TLS with a password, and NATS accepts only mTLS clients. The certificates come from an internal CA that cert-manager runs (deployments/k8s/platform/pki.yml), and Stakater Reloader rolls pods when a certificate is renewed. dev-up installs the vendored cert-manager and Reloader, or reuses an existing Helm-managed install; an existing cert-manager must run in the cert-manager namespace, or dev-up fails fast. On first start dev-up also seeds a random TMI_REDIS_PASSWORD into the tmi-secrets Secret. A cluster brought up before TLS was added (tmi 1.16.0) needs make dev-up, make dev-reset, or make dev-restart to pick it up.


Prerequisites

Tool Required? Notes
docker Yes Builds the images (on Docker Desktop it also hosts the cluster node they are imported into)
kubectl Yes dev-up deploys through your active context
A Kubernetes cluster Yes Docker Desktop's built-in Kubernetes (CLUSTER=docker-desktop) or a dedicated k3s cluster (CLUSTER=k3s)
uv Yes The make dev-* targets run scripts/devenv.py through uv run
config-development.yml Yes Your developer-owned config (gitignored); see Configuring Local Development
go Yes dev-up, dev-down, dev-reset, and dev-nuke build bin/tmi-dbtool to snapshot and restore operational settings

Create your config first if you haven't:

cp config-example.yml config-development.yml
# then edit database.url, OAuth, JWT secret, etc. β€” see Configuring Local Development

Cluster setup

Option A β€” Docker Desktop (CLUSTER=docker-desktop)

One-time setup: in Docker Desktop, open Settings β†’ Kubernetes, check Enable Kubernetes, and Apply & Restart. Verify the node is Ready:

kubectl --context docker-desktop get nodes    # one node: desktop-control-plane

No registry and no mirror configuration is needed: dev-up builds each image on the host and imports it directly into the node's containerd. The public Postgres and Redis base images are pulled on the host and imported the same way. The context is docker-desktop.

make dev-up CLUSTER=docker-desktop

make dev-cluster-up CLUSTER=docker-desktop only switches your kube context to docker-desktop; make dev-cluster-down is a no-op because Docker Desktop owns the cluster.

Option B β€” k3s (CLUSTER=k3s)

dev-up does not create or delete a k3s cluster β€” you manage that, and dev-cluster-up CLUSTER=k3s only switches your context to your k3s context. The images are pushed to a registry that dev-up itself deploys into the cluster (deployments/k8s/dev/k3s/registry.yml), exposed on a NodePort over plain HTTP. You need three one-time steps:

  1. Describe your cluster in .local/k3s.json (untracked) in your tmi checkout. Copy the template and fill it in; all three keys are required strings:

    cp deployments/k8s/dev/k3s/k3s.json.example .local/k3s.json
    {
      "context": "<k3s-context>",
      "registry": "<k3s-node>:<registry-nodeport>",
      "node_host": "<k3s-node>"
    }

    context is your kube context for the cluster, registry is where dev-up pushes images, and node_host is the node's hostname. Every CLUSTER=k3s command fails until this file exists. CLUSTER=docker-desktop never reads it.

  2. Docker on your machine must trust the registry: add <k3s-node>:<registry-nodeport> to insecure-registries (Docker Desktop β†’ Settings β†’ Docker Engine), and make sure <k3s-node> resolves (for example a line in /etc/hosts).

  3. Each k3s node must mirror the registry over HTTP. In /etc/rancher/k3s/registries.yaml:

mirrors:
  "<k3s-node>:<registry-nodeport>":
    endpoint:
      - "http://<registry-node-ip>:<registry-nodeport>"    # the node running the registry

Then restart k3s on each node so containerd picks up the change:

sudo systemctl restart k3s

DB=oracle is not supported on CLUSTER=k3s; dev-up exits with an error.

Context guard. dev-up, dev-restart, dev-reset, dev-nuke, and dev-deploy check that the active kube context is the one CLUSTER= expects (docker-desktop or your k3s context) or a recognized local context, and refuse otherwise, to avoid targeting a real cluster. Recognized local contexts: anything starting with kind- or k3d-, and the exact names k3s, default, rancher-desktop, docker-desktop, minikube. To override, pass --yes to the orchestrator directly, since the Makefile does not forward it: uv run scripts/devenv.py --cluster k3s --yes up.


Bringing the environment up

# Postgres (default DB), on Docker Desktop
make dev-up CLUSTER=docker-desktop

# Postgres on the k3s cluster
make dev-up CLUSTER=k3s

# Oracle Autonomous Database target, Docker Desktop only (see Oracle section below)
make dev-up CLUSTER=docker-desktop DB=oracle

make dev-up runs, in order:

  1. Cluster context β€” switches your kube context to the one CLUSTER= names.
  2. Preflight + context guard β€” checks docker and kubectl, that the cluster is reachable, and that the context is recognized (see the guard above).
  3. Registry (k3s only) β€” applies the in-cluster registry and waits for it.
  4. Build + deliver four images: the server (tmi-server for Postgres, or tmi-server-oracle for Oracle), the tmi-component-controller, and the tmi-extractor / tmi-chunk-embed workers. Docker Desktop imports them into the node; k3s pushes them to the registry.
  5. Platform base β€” creates the tmi-platform namespace and the Redis password Secret, then applies cert-manager, Reloader, the internal PKI (pki.yml), NATS, KEDA, and the CRD.
  6. Database β€” applies the in-cluster Postgres StatefulSet and waits for it (Postgres only).
  7. Config + secrets β€” renders your current config-development.yml into the tmi-server-config ConfigMap (a postgres://…@localhost or …@127.0.0.1 URL is rewritten to the in-cluster postgres Service), creates the embedding Secret, the OAuth-provider Secret (from .local/oauth-providers.env, if present), and for Oracle the wallet and DB-connection Secrets.
  8. Deploy β€” applies the dev overlay (controller, Redis, server, the worker component CRs, and on k3s the NetworkPolicies), then force-rolls the controller and server so the just-built images run.
  9. Expose β€” waits for the controller and tmi-server rollouts, starts managed kubectl port-forwards (server on :8080, plus Redis), and prints http://localhost:8080.
  10. Restore settings β€” if a .local/dev-config-<cluster>.yaml snapshot exists, restores the operational settings into the fresh database.

Verify it's up:

curl http://localhost:8080/
# returns the server version JSON. (There is no /health endpoint β€” use /.)

For authentication, use the built-in tmi OAuth provider with the callback stub β€” see Configuring Local Development β†’ Authenticating in Development.


Database configuration

With DB=postgres, dev-up deploys Postgres into the cluster (StatefulSet + PVC, svc/postgres:5432) and the server connects to it. The server still reads database.url from config-development.yml; keep the host as localhost (as for host-side tools) and dev-up rewrites it to the in-cluster postgres Service when it renders the ConfigMap. The rewrite only matches URLs of the form postgres://<user>:<password>@localhost… or …@127.0.0.1…; a postgresql:// URL or one without credentials is left unchanged:

# config-development.yml
database:
  url: "postgres://tmi_dev:dev123@localhost:5432/tmi_dev?sslmode=disable"

The database data survives dev-down, dev-restart, and dev-reset; only dev-nuke deletes it (it removes the PVC with the namespace). Because of that, teardown automatically snapshots the operational settings first, and dev-up restores them (see below).

To reach the in-cluster Postgres from the host, use kubectl port-forward -n tmi-platform svc/postgres 5432:5432.

Cloud Postgres

A database.url whose host is not localhost is left as written. (Not re-verified against the current dev-up, which still deploys the in-cluster Postgres regardless.) Point database.url at the cloud endpoint. This works with the default static server image (the Postgres driver is pure Go):

database:
  url: "postgres://user:[email protected]:5432/tmi?sslmode=require"

OCI Autonomous Database (DB=oracle)

Oracle ADB needs the Oracle server image variant (CGO + Instant Client) and the connection wallet:

  1. Source scripts/oci-env.sh so TMI_DATABASE_URL (an oracle://... URL) and ORACLE_PASSWORD are set; dev-up delivers them to the pod as the tmi-oracle-db Secret and exits if either is missing.

  2. Point TMI_ORACLE_WALLET_ZIP at your downloaded ADB wallet .zip file. dev-up delivers it as the tmi-oracle-wallet Secret, and the Oracle image extracts it at startup:

    export TMI_ORACLE_WALLET_ZIP=/path/to/Wallet_TMIDEV.zip
    make dev-up CLUSTER=docker-desktop DB=oracle

DB=oracle fails if TMI_ORACLE_WALLET_ZIP is unset or doesn't point at a file, and it is not supported on CLUSTER=k3s.

Swapping database targets

Config is delivered dynamically on every deploy β€” the image carries code only. To switch targets, edit database.url (and DB= if changing Postgres↔Oracle) and re-run:

make dev-restart CLUSTER=docker-desktop            # or: ... DB=oracle

A content hash on the ConfigMap forces the pod to roll and pick up the change.

Live edits are overwritten. Because the ConfigMap is re-rendered from config-development.yml on every dev-up/dev-deploy/dev-restart, changes made with kubectl edit/kubectl patch on the cluster are lost on the next deploy. Put bootstrap changes in the file. The ConfigMap carries bootstrap keys only; operational settings such as the OAuth callback allowlist (auth.oauth.client_callback_allowlist, which authorizes browser origins) live in the system_settings table and are managed through PUT /admin/settings/{key} (scripts/set-server-setting.py), then preserved across teardowns by make dev-config-snapshot / dev-config-restore.


Day-to-day commands

All dev-* targets require CLUSTER=docker-desktop|k3s (and accept DB=postgres|oracle, default postgres).

Command What it does
make dev-cluster-up CLUSTER=<c> Switch the kube context to the cluster's context (docker-desktop or your k3s context); does not create anything
make dev-cluster-down CLUSTER=<c> No-op: TMI does not own either cluster
make dev-up CLUSTER=<c> [DB=oracle] Build, deliver, and deploy the full environment; port-forward to :8080
make dev-restart CLUSTER=<c> [DB=oracle] Rebuild all four images, re-apply the platform base and the overlay, and roll the pods; cluster and DB untouched
make dev-down CLUSTER=<c> Tear down the workloads dev-up deployed; keeps DB data and leaves the cluster intact
make dev-reset CLUSTER=<c> Soft known-state: dev-down then dev-up with fresh images; keeps DB data
make dev-nuke CLUSTER=<c> Hard known-state: deletes the whole tmi-platform namespace (including DB data), removes the built images, and rebuilds from scratch
make dev-deploy CLUSTER=<c> Re-run the deploy steps without recreating the cluster or DB
make dev-status CLUSTER=<c> / make dev-logs CLUSTER=<c> Status dashboard / stream the tmi-server logs
make dev-config-snapshot / dev-config-restore / dev-config-status (with CLUSTER=<c>) Save, restore, or check the operational-settings snapshot in .local/dev-config-<cluster>.yaml

What dev-down removes

make dev-down deletes the server and Redis (Deployments + Services), the controller (+ RBAC), the worker component CRs, the config ConfigMap, and the embedding and Oracle Secrets; and stops the managed port-forwards. It snapshots the dev database's operational settings first. It keeps the database and its data, the in-cluster Postgres, the OAuth-provider Secret (tmi-oauth-providers), the platform base (NATS, KEDA, cert-manager), and, on k3s, the in-cluster registry. It does not touch your cluster, and does not stop a local Postgres container or the OAuth stub if you started those separately. To wipe everything including DB data, use make dev-nuke.


Troubleshooting

Symptom Likely cause / fix
CLUSTER is required Every dev-* target needs CLUSTER=docker-desktop or CLUSTER=k3s β€” there is no default
No reachable cluster Enable Kubernetes in Docker Desktop, or start your k3s cluster; make dev-cluster-up CLUSTER=<c> switches the context
Context '…' is not the expected '…' … nor a recognized local dev cluster The context guard fired β€” switch with make dev-cluster-up CLUSTER=<c>, or run uv run scripts/devenv.py --cluster <c> --yes up
Server can't reach the database Check kubectl -n tmi-platform get pods for postgres-0 and the server logs; on DB=oracle, check the wallet and tmi-oracle-db Secrets
ImagePullBackOff On k3s, the node can't pull from <k3s-node>:<registry-nodeport> β€” check /etc/rancher/k3s/registries.yaml and restart k3s, and that your Docker trusts <k3s-node>:<registry-nodeport> for pushes. On docker-desktop there is no registry: re-run make dev-up CLUSTER=docker-desktop to re-import the images
DB=oracle exits immediately TMI_ORACLE_WALLET_ZIP is unset or not a file, TMI_DATABASE_URL/ORACLE_PASSWORD are unset, or you used CLUSTER=k3s (unsupported for Oracle)
.local/k3s.json not found or missing or empty string key CLUSTER=k3s needs .local/k3s.json in your tmi checkout; copy deployments/k8s/dev/k3s/k3s.json.example and fill in all three keys
Lost connection to localhost:8080 The managed port-forward dropped; make dev-restart CLUSTER=<c> re-establishes it, or restart kubectl port-forward -n tmi-platform svc/tmi-server 8080:8080

Server logs are inside the pod:

kubectl -n tmi-platform logs deploy/tmi-server -f

See Also

⚠️ **GitHub.com Fallback** ⚠️