DEV Community

Menshikov Vasil
Menshikov Vasil

Posted on

The Kubernetes Inner Dev Loop That Was Quietly Eating My Sprint - and How I Got Back to Seconds

For about six months, my Kubernetes inner dev loop was a productivity black hole, and I mean that almost literally: time went in and nothing came out. I was the backend person on a small payments squad, and I got to the point where I quietly dreaded touching the cluster at all. This is the story of what was actually wrong, why it bugged me for so long, and the setup that finally gave me back a dev loop measured in seconds instead of minutes.

Why this bugged me for so long

Our service - I'll just call it myapp, a FastAPI HTTP API on port 8080 backed by PostgreSQL - ran beautifully on a laptop. uvicorn --reload picked up every change in a fraction of a second, the way good local dev should feel. Then we "just" shipped it to Kubernetes, and the instant loop turned into this:

# 1. build the image
docker build -t registry.internal/myapp:dev .
# 2. push to the registry
docker push registry.internal/myapp:dev
# 3. apply the manifests
kubectl apply -f k8s/
# 4. wait for the new Pod and check it's alive
kubectl rollout status deployment/myapp
kubectl logs -f deployment/myapp
Enter fullscreen mode Exit fullscreen mode

Four steps that simply did not exist when we ran things locally. Every single pass was two to five minutes of build, push, wait, check. Multiply that by dozens of iterations a day across the team and you get whole afternoons of people staring at kubectl rollout status. And here's the part that really got under my skin: the wait was juuust long enough that I'd flip to Slack every time. So the true cost was never the four minutes. It was the train of thought that derailed on every deploy, and never quite got back on the rails.

A slow loop is only half the misery, though. The other half is the bugs you genuinely cannot see on a laptop. We shipped a change that passed every local test and then watched the Pod get OOMKilled in staging, because we'd never set a memory limit locally - memory is effectively infinite on a dev box. Another release sat there "running but not responding" because a readiness probe was failing and Kubernetes had quietly yanked the Pod out of the Service endpoints. Nothing in my local world had prepared me for either.

The idea that reframed everything: shift-left, but with fidelity

I got tired of band-aids and went looking for a systematic answer. Someone had written up exactly why the Kubernetes inner loop gets slow and how to close the local-versus-cluster gap, and it put a name to the thing I'd been feeling in my gut: the more your test environment differs from prod, the more bugs leak downstream, where each one costs an order of magnitude more to fix.

The line that really stuck with me was this - testing earlier in a CI container that doesn't match your cluster isn't shift-left. It's just failing faster in the wrong environment. Real shift-left for Kubernetes means validating against real cluster conditions: real resource limits, real probes, real services in the namespace, early, on your own machine. That reframed the whole problem for me, and it pointed at two concrete tools instead of a vague "do better."

The first is k3d, a lightweight wrapper that runs CNCF's k3s inside Docker. Not an emulation of a cluster - an actual one, small enough to live on a laptop. The second is Tilt, whose live-update performs an in-place update of the containers in your cluster, syncing your code straight into a running Pod so iteration drops back to seconds.

What I actually rolled out

I named the cluster dev, put everything in a myapp namespace, and used k3d's built-in registry at k3d-registry.localhost:5000. My first lesson landed the hard way: the local cluster cannot see images from your local Docker daemon. I built myapp:dev, expected the cluster to pick it up, and got a Pod wedged in ImagePullBackOff instead. k3d nodes run their own containerd, isolated from your Docker. You either push to a registry the cluster can reach or import the image explicitly - the k3d registries guide walks through wiring up a local registry the cluster can actually pull from.

The second lesson was the :latest tag trap. If your manifest references myapp:latest, Kubernetes defaults imagePullPolicy to Always and the kubelet re-pulls on every launch even when the image is sitting right there. The Kubernetes image docs spell it out: omit imagePullPolicy with a :latest tag and it becomes Always, while a fixed tag defaults to IfNotPresent. I switched to specific tags - myapp:dev, myapp:<gitsha> - and imagePullPolicy: IfNotPresent, and a whole category of mysterious slowness just evaporated.

Then Tilt took over the loop, and this is the part I still find a little magical. Instead of build then push then apply then wait, saving a file synced the change straight into the running Pod. The numbers matched exactly what the tool promised - Tilt's own writeup frames live-update as deploying code to running containers in seconds, not minutes, and syncing files into a running Pod with hot reload brought my iteration back down to roughly one to five seconds, better than a 95% cut. One detail I'm glad I got right: in the cluster image I run fastapi run, not uvicorn --reload. Reload is dev-only overhead and a leak risk; the speed comes from Tilt's live-update, not from reload inside the container.

How it feels now

The headline is easy to quote - two-to-five-minute iterations became one-to-five-second ones, and "build, push, apply, wait, check" collapsed into "save the file." But honestly the second-order effects were the ones that changed how I work. Because a change now took seconds to see, I started testing smaller increments, one hypothesis at a time, instead of batching five changes into a single painful deploy and then bisecting which one broke. Pull requests got smaller. Review comments got sharper, because a reviewer could pull the branch and watch it run in a real cluster in under a minute.

And the OOMKilled and readiness-probe bugs that used to ambush us in staging now surfaced on the author's laptop, where the person with all the context was sitting right there to fix them. That's the whole economic argument for shift-left in one sentence: a bug caught in the inner loop costs a coffee's worth of attention, while the same bug caught after git push costs a CI run, a reviewer's time, and sometimes a rollback. I didn't measure it to the decimal, but the number of "why is staging broken?" Slack threads dropped to nearly zero within a sprint, and I stopped bracing every time I opened the cluster.

A few things bit hard enough to cost a day each, and I'll pass them on so they don't cost you one. ImagePullBackOff on day one had nothing to do with a bad image - the cluster just couldn't see it, and understanding image delivery into k3d up front would have saved me an afternoon. Running --reload inside the container "because it was fast locally" fought Tilt and masked the real loop, so kill it. Don't confuse the inner and outer loops: this whole exercise is about the inner loop, the single-developer edit-build-run cycle before git push, and the outer loop of CI, GitOps, and integration tests is a genuinely separate beast. And don't skip resource limits locally - the entire point of a production-like local setup is that limits and probes exist on your laptop, so leaving them out just reopens the gap you were trying to close.

I had to actively resist the itch to also "fix" that outer loop in the same push. It's real work, but it's a different problem with different tools, and bundling it would have stalled everything. I fixed the inner loop first, shipped it, and let the win speak before touching anything downstream. Once the loop was fast, I layered the rest of a production-like setup on piece by piece - real Deployment and Service manifests, PostgreSQL inside the cluster, ConfigMaps and Secrets instead of hardcoded values, health probes - one production-like layer at a time (there's a full end-to-end assembly linked in Sources below).

What I keep coming back to isn't the speed number, satisfying as it is. It's that I started using the cluster again instead of avoiding it. A dev loop you dread is a dev loop you route around, and every workaround quietly costs you the fidelity you were supposed to be buying. Getting seconds back didn't just make me faster - it made me willing, and that turned out to be the whole point.

Sources & further reading

Top comments (0)