Local Development Cluster - ericfitz/tmi GitHub Wiki
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).
make dev-upno longer runs the server and workers as host Go processes. It now builds container images, delivers them to the cluster you name withCLUSTER=(imported into the Docker Desktop node, or pushed to the k3s in-cluster registry), and deploys them there. There is no longer a hosttmi-serverprocess or amake start-serverstep in the dev loop.If you have an old workflow that expected
dev-upto launch a bare Go binary, that path is gone. The standalonemanage-database.pylocal Postgres container (make start-database) still exists, butdev-upno longer uses it β the dev Postgres now runs inside the cluster (see Database configuration).
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.
| 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 DevelopmentOne-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-planeNo 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-desktopmake 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.
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:
-
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>" }contextis your kube context for the cluster,registryis wheredev-uppushes images, andnode_hostis the node's hostname. EveryCLUSTER=k3scommand fails until this file exists.CLUSTER=docker-desktopnever reads it. -
Docker on your machine must trust the registry: add
<k3s-node>:<registry-nodeport>toinsecure-registries(Docker Desktop β Settings β Docker Engine), and make sure<k3s-node>resolves (for example a line in/etc/hosts). -
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 registryThen restart k3s on each node so containerd picks up the change:
sudo systemctl restart k3sDB=oracle is not supported on CLUSTER=k3s; dev-up exits with an error.
Context guard.
dev-up,dev-restart,dev-reset,dev-nuke, anddev-deploycheck that the active kube context is the oneCLUSTER=expects (docker-desktopor your k3s context) or a recognized local context, and refuse otherwise, to avoid targeting a real cluster. Recognized local contexts: anything starting withkind-ork3d-, and the exact namesk3s,default,rancher-desktop,docker-desktop,minikube. To override, pass--yesto the orchestrator directly, since the Makefile does not forward it:uv run scripts/devenv.py --cluster k3s --yes 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=oraclemake dev-up runs, in order:
-
Cluster context β switches your kube context to the one
CLUSTER=names. -
Preflight + context guard β checks
dockerandkubectl, that the cluster is reachable, and that the context is recognized (see the guard above). - Registry (k3s only) β applies the in-cluster registry and waits for it.
-
Build + deliver four images: the server (
tmi-serverfor Postgres, ortmi-server-oraclefor Oracle), thetmi-component-controller, and thetmi-extractor/tmi-chunk-embedworkers. Docker Desktop imports them into the node; k3s pushes them to the registry. -
Platform base β creates the
tmi-platformnamespace and the Redis password Secret, then applies cert-manager, Reloader, the internal PKI (pki.yml), NATS, KEDA, and the CRD. - Database β applies the in-cluster Postgres StatefulSet and waits for it (Postgres only).
-
Config + secrets β renders your current
config-development.ymlinto thetmi-server-configConfigMap (apostgres://β¦@localhostorβ¦@127.0.0.1URL is rewritten to the in-clusterpostgresService), creates the embedding Secret, the OAuth-provider Secret (from.local/oauth-providers.env, if present), and for Oracle the wallet and DB-connection Secrets. - 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.
-
Expose β waits for the controller and
tmi-serverrollouts, starts managedkubectl port-forwards (server on:8080, plus Redis), and printshttp://localhost:8080. -
Restore settings β if a
.local/dev-config-<cluster>.yamlsnapshot 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.
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.
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"Oracle ADB needs the Oracle server image variant (CGO + Instant Client) and the connection wallet:
-
Source
scripts/oci-env.shsoTMI_DATABASE_URL(anoracle://...URL) andORACLE_PASSWORDare set;dev-updelivers them to the pod as thetmi-oracle-dbSecret and exits if either is missing. -
Point
TMI_ORACLE_WALLET_ZIPat your downloaded ADB wallet .zip file.dev-updelivers it as thetmi-oracle-walletSecret, 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.
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=oracleA 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.ymlon everydev-up/dev-deploy/dev-restart, changes made withkubectl edit/kubectl patchon 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 thesystem_settingstable and are managed throughPUT /admin/settings/{key}(scripts/set-server-setting.py), then preserved across teardowns bymake dev-config-snapshot/dev-config-restore.
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
|
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.
| 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- Configuring Local Development β config file layout, secrets, and authenticating with the tmi OAuth provider
- Getting Started with Development β overall dev onramp (server + web app)
- Database Setup β Postgres container details and schema migration
- Oracle Container Images β building and running the Oracle ADB server image variant
-
Component Integration β the
TMIComponentplatform that runs in the cluster