Skip to content
6.8. Platform Operations

6.8. Platform Operations

In one glance

  • You will: Fix the four things that actually go wrong on a laptop cluster: an unreachable model, a machine out of memory, a session you want to pause, and a control plane you want gone.
  • You need: The k3d cluster from 6.2. Platform Install, or a memory of it going wrong.
  • Time: about 12 minutes, reference.

Free port 11434 before the cluster can reach Ollama

A cluster on your own workstation is a tenant: it competes for memory and ports with the model, the browser, and the editor. It also has to pause and disappear on request. That tenancy produces four failures, none a bug in the agent, and each presents as an application fault, so the time goes into the wrong layer. This page gives the diagnosis, the fix, and the undo for each.

The first appears in 6.2. Platform Install, where you start ollama serve yourself so pods can reach the model:

Error: listen tcp 127.0.0.1:11434: bind: address already in use

Something already owns port 11434. On Linux it is almost always the systemd unit the Ollama installer shipped, which has been serving your loopback since 1.4. Providers. Occasionally it is mise run model:fake, the deterministic stand-in load tests use, which takes the same port on purpose; if you started it earlier, stop it.

Make the model reachable from inside the cluster

Pods cannot reach a loopback listener on your host. k3d puts the cluster on its own Docker bridge network, so the model has to listen on the bridge’s gateway address, which you read rather than guess:

docker network inspect k3d-local --format '{{(index .IPAM.Config 0).Gateway}}'

That address changes whenever Docker recreates the network, so re-read it each session rather than writing it down. On Linux, pick one of two ways to hand the port over, and undo it when the lab ends:

  1. Borrow the port for the session. sudo systemctl stop ollama, run ollama serve in its own terminal with OLLAMA_HOST exported to <bridge address>:11434, then sudo systemctl start ollama when you are done. Nothing on disk changes.
  2. Rebind the unit itself, if you want systemd to keep managing Ollama. Run sudo systemctl edit ollama, add Environment="OLLAMA_HOST=<bridge address>:11434" under [Service], then sudo systemctl restart ollama. Undo it with sudo systemctl revert ollama && sudo systemctl restart ollama, which deletes the drop-in and restores the vendor unit.

On macOS the container engine runs inside a VM, so the Linux bridge address is not a valid macOS bind address. Follow Ollama’s macOS bind configuration, quit and reopen Ollama.app, then prove it from a short-lived pod outside agentops, whose restricted Pod Security Standard would reject a plain kubectl run:

launchctl setenv OLLAMA_HOST "0.0.0.0:11434"
kubectl run ollama-check --rm -i --restart=Never \
  --image=docker.io/library/busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 -- \
  wget -qO- http://host.k3d.internal:11434/api/tags
launchctl unsetenv OLLAMA_HOST

k3d injects host.k3d.internal for pod-to-host access, which is why that URL works from a pod and the bridge IP is what the host itself must bind. launchctl setenv writes OLLAMA_HOST into launchd’s environment, where it persists until removed, so the last line takes it out again.

A rebound Ollama has no authentication

Every path above moves Ollama off loopback, and Ollama does not authenticate callers on that listener. Once it is bound to the bridge, every container on the k3d-local network — and anything that can route to that address — can use your model, read whatever you send it, and spend your GPU. Do this on a trusted machine, on a trusted network, and undo it when the lab ends.

If neither path suits your machine, stopping here is a legitimate outcome. The Chapter 5 host profile is a supported checkpoint: same agent, same gateway, same protocols, with only the Kubernetes packaging missing. The validated cluster path is Linux with Docker Engine; GKE stays plan-only unless an approved deployment is explicitly requested.

Tell a Pending pod, an OOMKill, and a dead node apart

Memory exhaustion is the most common cold-laptop failure here, and it never announces itself as “out of memory”. It arrives as a pod stuck Pending, a container in CrashLoopBackOff, a Skaffold build that stops moving, or a kubectl that cannot reach the API server. Three failures look alike and take different fixes, so collect evidence in this order, and predict which of the three means your manifests are at fault before you read the table:

kubectl -n agentops get pods
kubectl -n agentops describe pod <pod> | grep -A6 "Last State"
kubectl get events -A --sort-by=.lastTimestamp | tail -20
docker stats --no-stream

Each command rules something out: get pods gives the phase, describe the last exit, get events what the scheduler refused, docker stats the host itself — the k3d node is a container too, so if it is missing the engine killed it.

What you observeWhat it actually isWhat to do
Pending, with Insufficient memory or Insufficient cpu in the eventsthe node has no room left to schedule; nothing is broken or misconfiguredfree the host first — stop the Chapter 5 and 7 host Compose stacks and any unrelated containers, then re-check
OOMKilled, or exit code 137 under Last Statethat one container exceeded its own limit; the rest of the cluster is fineread the limit in the owning manifest and raise it deliberately, or give that pod less work
kubectl stops answering, or the k3d node container is gone from docker psthe engine (or the Docker Desktop/Colima VM) hit the machine’s ceiling and killed the noderaise the VM’s memory allocation, or run fewer tenants: the model, the cluster, and a browser do not share 8 GiB comfortably

Those three are the memory story. A pod that stays Pending or CrashLoop on a machine with room left has a different cause — an image the local registry cannot serve, a /healthz that reports unready because the seed dataset or state directory is unreachable, or the default-deny egress policy — and 0.7. Troubleshooting separates them.

Two structural savings come before tuning any limit. Never run the host Compose observability stack and the in-cluster stack at the same time: they duplicate every component and contend for the same local ports, which is why dashboards go flat and port-forwards return nothing when both are up. And stop the cluster when you are not using it: the model is the other large tenant, and Ollama keeps qwen3:4b-instruct resident while it serves.

Diagnose an accepted Agent that never becomes a pod

The API server accepting your object proves one thing: it matched the schema. Turning that object into a pod is a separate process on the other side of a watch, and it can be crash-looping, watching the wrong namespace, or stuck on a reference it cannot resolve — none of which appears as an error on the apply.

Ask the object what it thinks, then ask the controller:

kubectl -n agentops get agent.kagent.dev/agentops-agent -o jsonpath='{.status.conditions}'
kubectl -n kagent logs deploy/kagent-controller --tail=50

The conditions are the controller’s report about your object; empty conditions mean it has not looked at it yet, which the second command explains. Check the namespace first when a resource is being ignored outright: infra/kagent/values.yaml scopes controller.watchNamespaces to agentops alone, so an Agent applied anywhere else is valid, accepted, and never reconciled.

Stop the cluster between sessions, and remove kagent last

k3d cluster stop stops the node containers and leaves the cluster, its images, its volumes, and every deployed workload exactly as they are:

k3d cluster stop local

Make that the normal end of a session. It returns the memory k3s and the course workloads were holding while keeping every byte of state, and mise run cluster:start resumes the same cluster in seconds. Deleting the cluster destroys its volumes, which is why that command sits behind the teardown review in 6.6. Platform Delivery.

Where kagent is going, and why it validates the shape you just deployed

One paragraph of forward-looking context, dated because it will move. Read on 13 August 2026, the kagent documentation describes Agent Substrate, a Kubernetes-native runtime that runs agents inside per-actor sandboxes with fast startup and suspend-and-resume, and two resource kinds that sit on it: an AgentHarness, “a Kubernetes custom resource that asks kagent to provision a long-running remote execution environment on Agent Substrate”, and a SandboxAgent, which “runs a (Go) declarative agent runtime inside a sandboxed Agent Substrate actor” (Agent Harness, Agent Substrate).

That direction is worth noticing for two reasons, and neither is a reason to change anything here. The first is that a CNCF agent controller running a Go declarative runtime is this course’s thesis arriving from the other side: the argument for a typed, compiled agent was never about Go, it was about an agent being an ordinary workload with an ordinary contract, and a control plane that can sandbox one has taken the same position. The second is the sandbox itself. Everything Chapter 4 does — the tool allowlist, the guarded write, the policy plugin — bounds what the agent’s code may do, and none of it bounds what the agent’s process may do; a per-agent sandbox is the layer under all of them, and it is the answer to the question this chapter’s NetworkPolicies can only partly answer. The BYO Agent you deployed remains the right shape for a course: you own the image, you can read every line of what runs, and nothing between you and the container needs explaining.

Removing kagent is a separate decision, and the order matters: remove the course workloads with Skaffold first, then the Helm releases. Deleting the controller and its CRDs first would leave a BYO Deployment nothing owns and nothing can clean up. The kagent control plane is cluster-wide, so on a shared local cluster, prefer removing the course workloads and leaving the controller for other namespaces. When you own the cluster outright, helmfile --file infra/helmfile.yaml destroy removes both releases; 6.6. Platform Delivery owns that sequence, including how to prove nobody else is using it.

What you can do now

  • You can read the k3d bridge address and bind the model to it, on Linux or macOS, and undo the change afterwards.
  • You can tell an unschedulable pod, an OOMKilled container, and a dead node apart from the same four commands.
  • k3d cluster stop local ends a session without losing a single volume, and mise run cluster:start brings the same cluster back.
  • You can say why the controller has to be removed after the workloads it manages, not before.

Continue to 6.9. Scale Out once the platform starts, pauses, and disappears on command; it asks what changes when one replica becomes two.