Docker, Kubernetes, and the question of whether you need both

October 30, 2025

Containers are a delivery primitive. Kubernetes is an operational commitment. The gap between them is where most teams get into trouble.


Containers solve one problem well

Docker standardizes how software is packaged and delivered. The image is the artifact — it carries its dependencies, runs the same locally and in CI and in production, and can be versioned and rolled back cleanly.

That value is real and available to every project, regardless of scale. A Dockerfile should be written like application code: reviewed, minimal, and explicit.

FROM node:20-slim AS base
WORKDIR /app

FROM base AS deps
COPY package*.json ./
RUN npm ci --only=production

FROM base AS release
COPY --from=deps /app/node_modules ./node_modules
COPY . .
USER node
CMD ["node", "server.js"]

Non-root by default, minimal image, dependency stage separated from the release stage. These aren't cargo-cult practices — they reduce surface area for both security issues and build cache misses.

Kubernetes: the commitment question

Kubernetes solves a different problem: scheduling, scaling, and operating many workloads across many machines with consistency. It's excellent at this. It's also expensive — in complexity, in operational overhead, and in the learning curve it imposes on every engineer who touches it.

The honest question before adopting Kubernetes isn't "how do we deploy with Kubernetes?" It's "do we have the problems Kubernetes is designed to solve?"

Those problems look like:

  • Multiple services with independent scaling requirements
  • Teams that need deployment isolation from each other
  • Workloads with variable resource demands that benefit from bin-packing
  • A need for consistent networking and security policy across many services

If you're running two or three services, a managed container platform (Railway, Render, Fly, Cloud Run) probably gives you 90% of the benefit with 10% of the operational overhead.

The basics that actually matter

When Kubernetes is the right choice, the difference between a stable cluster and a miserable one usually comes down to configuration discipline rather than architectural decisions:

resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "500m"
    memory: "512Mi"
readinessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10

Requests and limits make scheduling predictable. Probes that reflect reality prevent traffic from reaching pods that aren't ready. Pod disruption budgets keep upgrades from taking everything down at once. None of this is exciting. All of it matters.

Service mesh: earn it twice

A service mesh (Istio, Linkerd) adds mutual TLS, traffic shaping, and uniform telemetry across services. It also adds failure modes, operational complexity, and a significant learning curve.

The pattern I've seen succeed: adopt a mesh after you have a working observability setup and a concrete need it solves — usually mTLS for compliance requirements or fine-grained traffic control for canary deployments. Adopting it speculatively tends to produce a mesh that nobody fully understands managing problems it was installed to prevent.

The telemetry argument for a mesh is weaker than it used to be. eBPF-based tools (Cilium, Pixie) can give you cross-service visibility without the proxy sidecar overhead.

Debug like the cluster will lie to you

It will. Ephemeral containers and kubectl debug are useful when a pod is misbehaving and you can't reproduce it locally:

kubectl debug -it pod/my-pod --image=busybox --target=app

Build cluster observability into your standard setup, not as an afterthought. If diagnosing a restart requires guessing, you'll pay for that in incidents.

References

Hi, I'm Martin Duchev. You can find more about my projects on my GitHub.