Local Kubernetes Dev — Part 2: Production-like environments — what and why
What a production-like local setup really means: what to reproduce on purpose, what to simplify deliberately, and where imitating prod locally becomes harmful.
In the previous chapter we agreed to develop myapp (an HTTP API in Python 3.12 + FastAPI, port 8080, depends on PostgreSQL) inside a real Kubernetes cluster right on your laptop. But before spinning up a cluster, it's important to understand what kind of local setup we're building and why. This chapter is about the idea of "production-like": what we reproduce on purpose, what we deliberately simplify, and where the line is, past which imitating prod locally becomes harmful.
What "production-like" means and why it matters to a beginner
A "production-like environment" is not "an exact copy of prod." Copying prod wholesale onto a laptop is impossible (more on that below), and you don't need to anyway. The goal is more modest and more useful: to deliberately narrow the gap between how your service behaves on your machine and how it will behave in prod — so that bugs related to packaging and deployment get caught on your machine, and not on the production cluster under live traffic.
Where does this gap come from in the first place? The classic formulation comes from the The Twelve-Factor App methodology (the twelve factors — a set of practices for designing services that we'll return to more than once). Its tenth factor, "Dev/prod parity," singles out three gaps between dev and prod:
- The time gap: code takes days or weeks to go from being written to reaching prod — the goal is to shrink that to hours and minutes.
- The personnel gap: code is written by some people and deployed and operated by others.
- The tools gap: the developer's local stack doesn't match the production one.
It's the third gap that breeds an insidious problem. The Twelve-Factor App states it directly: "tiny incompatibilities crop up, causing code that worked and passed tests in development or staging to fail in production." The same idea is broken down for beginners in the KodeKloud notes on Dev/Prod Parity: if you develop against SQLite locally while prod runs PostgreSQL, sooner or later you'll step on a difference in their behavior — and you'll find out about it at the worst possible moment.
For a beginner who is preparing a service for Kubernetes for the first time, this is exactly where a local cluster proves its worth. A whole class of problems exists only at the level of Kubernetes manifests and the deployment itself: misconfigured probes (health checks), insufficient permissions (RBAC), service discovery errors (locating services by their DNS name inside the cluster), forgotten resource limits. By running myapp locally in a real cluster, you walk exactly the same packaging and rollout path as in prod — and you catch these errors before they reach the production environment.
Environment parity: the same Kubernetes version, addons, real manifests
In practice, parity comes down to three things: the same Kubernetes minor version, the same backing services (external dependencies — databases, queues, caches) by type and version, and the same real manifests used to roll the service out in prod.
Let's start with the version. Kubernetes is a fast-moving project, and behavior can change between minor versions (for example, 1.30 and 1.33): APIs get deprecated and removed, defaults change. That's why you need to pin the cluster version explicitly, rather than relying on latest, which drifts over time.
In k3d (our main tool, see chapter 5) the version is set by the k3s image — via the --image flag. Here we just show the pinning principle; you'll see it "live" — with the flags explained and a real k3d cluster create dev — in chapter 5:
1# One important wrinkle with the tag: Docker tags can't use '+',
2# so we write 'v1.31.5-k3s1', NOT 'v1.30.2+k3s1'
3k3d cluster create dev --image rancher/k3s:v1.31.5-k3s1The same thing, more tidily, via a k3d config file (which is convenient to keep in the repo next to the code):
1apiVersion: k3d.io/v1alpha5
2kind: Simple
3metadata:
4 name: dev
5servers: 1
6agents: 2
7image: rancher/k3s:v1.31.5-k3s1If you prefer kind (Kubernetes IN Docker, the alternative from chapter 5), the version is pinned by the node image, and the kind documentation recommends specifying not just the tag but also the sha256 digest (taken from the release notes of the corresponding kind release) — that way the image is fixed unambiguously:
1kind: Cluster
2apiVersion: kind.x-k8s.io/v1alpha4
3nodes:
4- role: control-plane
5 image: kindest/node:v1.33.0@sha256:<digest-from-release-notes>You can (and should) verify that the versions match — compare the output with what's running in prod:
1kubectl version --output=yamlThe second element of parity is real manifests. This means: locally you roll myapp out with the same declarative descriptions as in prod, and not via some separate "local" method. The official Kubernetes documentation Managing Workloads shows the standard techniques for working with such manifests: grouping several resources in one file with the --- separator, recursively applying an entire directory, and Kustomize built into kubectl (a tool for layering settings on top of base manifests):
1# apply all manifests from the directory, including nested ones
2kubectl apply -f k8s/overlays/dev --recursive
3
4# or via Kustomize, built right into kubectl
5kubectl apply -k k8s/overlays/devBesides kubectl and Kustomize, prod configuration is often packaged with Helm — the package manager for Kubernetes (it's also mentioned in Managing Workloads as a way to manage applications). We'll write the actual myapp manifests in chapter 7, and we'll cover splitting them by environment with Kustomize/Helm in chapter 13. What matters here is the principle: the local setup uses the same rollout artifacts as prod — otherwise parity is broken at the most important level.
What we reproduce, and what we deliberately simplify
Production-like is always a balance. Part of prod we reproduce, because that's exactly where bugs hide; part we simplify, because an exact imitation is impossible or harmful.
Reproduce, no exceptions:
- The Kubernetes version and the type/version of backing services. For
myappthis means: PostgreSQL locally, not SQLite (a direct consequence of the tenth factor — don't substitute backing services). How to bring up dependencies locally is in chapter 9. - Resource
requestsandlimits(the requested and maximum CPU/memory for the container) — so that scheduler and constraint behavior is realistic. - Probes (health checks) — liveness, readiness, and, when needed, startup.
- ConfigMap and Secret for configuration and secrets instead of hardcoding — that's chapter 10.
- Ingress and service discovery — routing from outside and locating services by DNS inside the cluster (chapter 11).
Beginners regularly confuse the probes, but a detailed breakdown of the three types belongs where we actually configure them — in chapter 13. Here the essentials are enough: the three probes behave differently — liveness restarts the container, readiness merely removes the Pod from the Service's endpoints (no traffic goes to it, but there's no restart), and startup covers a slow start. This is stable behavior from the official Kubernetes documentation, and reproducing it locally is part of parity with prod.
The practical takeaway: mixed-up probes give you either restart loops (if you slap an aggressive liveness probe instead of a startup probe on a slow start) or "dead" endpoints under traffic (if you mix up readiness and liveness). For myapp the logic is simple: /healthz as liveness responds as long as the process is alive; /ready as readiness checks that the connection to PostgreSQL is established — and while the database is unavailable, the Pod receives no requests. We use this pair of endpoints (/healthz + /ready) throughout the article, and we'll write the probe manifests themselves in chapter 13.
Deliberately simplify:
- Scale and production topology. Your machine is limited in CPU, memory, and disk. As the Local Kubernetes overview from Plural notes, local environments are ideal for prototyping, but the move to production and multi-region brings new challenges that simply can't be reproduced on a laptop. Dozens of replicas, multiple availability zones, production autoscaling — that's not a local task.
- Managed cloud services. Amazon RDS, cloud queues, and caches don't replicate locally — you bring up their open-source equivalents (PostgreSQL in a container instead of RDS). The behavior is close, but not identical; this is a conscious compromise.
- Load testing. Running a serious load test on the same nodes where the application runs is a bad idea: the load-generating Pods compete for CPU and memory with
myappitself on the same machine, skewing the results. You run load in a separate environment, not on your local setup.
A handy resource-saving trick is to bring up, per session, only what you actually need: a dedicated namespace for the task and just the services you're currently working with, instead of dragging the entire prod zoo along.
Anti-pattern: docker-compose as a "replacement" for prod
A very common beginner's misconception: "I already have a docker-compose.yaml, it brings up the service with a database anyway — why do I need Kubernetes locally?" The problem is that a Compose file is structurally not equivalent to Kubernetes manifests (Helm/Kustomize charts), and "works in Compose" by no means guarantees "works in k8s."
The first difference is architectural. Docker Compose is, as the Docker Compose vs Kubernetes breakdown from Spacelift notes, essentially a single-host tool, whereas Kubernetes is a cluster orchestrator. They have a different intent and a different scope of responsibility.
The second difference is the granularity of abstractions. A single service in Compose is deployed in Kubernetes as several separate objects: a Deployment (how to run the Pods), a Service (how to reach them), a ConfigMap and a Secret (configuration and secrets), an Ingress (external access). This isn't cosmetic — each object carries its own piece of production behavior.
Just how incompletely one translates into the other is visible from the official conversion tool — Kompose (a project under the Kubernetes umbrella). Its authors warn honestly: "our conversions are not always 1-1... but will get you 99% of the way there." The breakdown of converting Compose → Kubernetes with Kompose (oneuptime, 2026) lists the specific places where the mapping falls down:
depends_onis ignored — Kubernetes has no direct equivalent for "start B after A." Startup order is handled differently: with init containers or with retries at the application level (formyapp— reconnecting to PostgreSQL until the database comes up).build:doesn't build the image — Kubernetes can't build images from source; the image must be built and pushed to a registry ahead of time (for us — the built-in k3d registryk3d-registry.localhost:5000, see chapter 6).network_mode: hostand custom networks map onto the Kubernetes networking model poorly or are ignored.- Bind mounts are not preserved — instead of mounting local files, a ConfigMap/Secret is recommended.
And the main point, the whole reason for this chapter: manifests generated from Compose come by default without requests/limits, without health probes, and with environment variables in plaintext instead of a Secret. In other words, the Kompose output is a starting point, not a ready-made prod manifest; the very production-like aspects for which we're building a local setup are exactly what Compose skips.
If you want a quick look at what your Compose file turns into, you can run the conversion — but only as a draft for further refinement:
1# only as a starting point, NOT a ready-made prod manifest
2kompose convert -f compose.yamlThe conclusion: Compose works great as a quick local launch for the earliest steps, but it doesn't validate your Kubernetes manifests. Manifest problems only surface after a real rollout to k8s — so when you're preparing a service for Kubernetes, you should also be testing it in Kubernetes.
Checklist: "how production-like is my local setup"
Let's roll it all up into a practical list. The more boxes you check, the fewer surprises when rolling myapp out to prod.
- The same Kubernetes minor version as in prod — pinned explicitly via
--imagein k3d/kind (for kind — plus thesha256digest). Verified withkubectl version. - Deployment with real manifests (Kustomize/Helm), not a separate docker-compose or manual commands.
- Resource
requestsandlimitsare set for the container. - Probes are configured: liveness and readiness (plus startup, if the start isn't fast), and configured deliberately — readiness does not restart the container, liveness does.
- Configuration in a ConfigMap, secrets in a Secret — not hardcoded and not plaintext env in the manifest.
- Ingress and routing are reproduced — external access to
myappgoes through the same path as in prod. - The same backing services by type and version — for
myappthat's PostgreSQL, not SQLite. - Critical addons/controllers (ingress controller, and cert-manager if needed) are either brought up or the simplification is explicitly noted.
- Deliberate simplifications are documented — exactly what you do NOT reproduce (scale, managed services, load testing) and why.
The last item is underrated, but it's important: a production-like environment is not one where "everything is just like prod," but one where you know exactly what matches, what doesn't, and why. From here on we'll be closing off the items on this checklist step by step: starting with the next chapter, we'll go through which tool is responsible for what.
Sources
- The Twelve-Factor App — X. Dev/prod parity
- Dev/Prod Parity — KodeKloud Notes
- kind — Configuration
- k3d — Config File
- Managing Workloads — Kubernetes
- Configure Liveness, Readiness and Startup Probes — Kubernetes
- Kompose — Convert Docker Compose to Kubernetes
- How to Convert Docker Compose Files to Kubernetes Manifests with Kompose — oneuptime (2026)
- Docker Compose vs Kubernetes — Spacelift
- Local Kubernetes: A Comprehensive Guide — Plural