Kubernetes is the system most teams now use to run containers in production. In the CNCF annual survey announced on January 20, 2026, 82% of container users reported running Kubernetes in production, up from 66% in 2023. Operating it well is now a core engineering skill, and this post teaches it in three layers.
The first layer explains what Kubernetes is, the problem it solves, and the objects you work with every day. The second shows how a cluster is built and what happens between typing kubectl apply and a running container. The third is a production baseline for a stateless HTTP service: seven manifests, each explained, plus the 2026 changes that can break a cluster you have not touched in a year.
Kubernetes 1.37 shipped on August 26, 2026, and the project now supports three minor versions at once. Every feature below carries its maturity level and the version that set it. Where a claim comes from a release note, a blog post or a survey, it links to that source. Where the baseline depends on something your cluster must provide, such as a Gateway API controller or a CNI plugin that enforces NetworkPolicy, the text says so.
What Kubernetes is, and the problem it solves
In the project's own words, Kubernetes is a portable, extensible, open source platform for managing containerized workloads and services, with both declarative configuration and automation. The name comes from the Greek word for helmsman or pilot, and K8s abbreviates it by counting the eight letters between the K and the s. Google open sourced the project in 2014, and it combines over 15 years of Google's experience running production workloads at scale with ideas from the community.
To see why it exists, follow how deployment changed. Teams first ran applications on physical servers, where one application could take most of a machine's resources and starve the others. Virtual machines fixed that by isolating applications, but each VM runs a full operating system. Containers relax that isolation and share the host operating system, so they are lighter and portable across clouds and Linux distributions.
Containers solve packaging, not operations. In production something must restart a container that dies, spread containers across many machines, send traffic only to the healthy ones, and roll out new versions without downtime. The overview puts it plainly: if a container goes down, another container needs to start, and it would be easier if a system handled that. Kubernetes is that system.
The overview lists what you get from it, summarized here.
| Capability | What it does |
|---|---|
| Service discovery and load balancing | Exposes a container by DNS name or IP address and spreads traffic across copies |
| Storage orchestration | Mounts the storage system you choose, from local disks to public cloud storage |
| Automated rollouts and rollbacks | Moves the actual state to the state you describe, at a controlled rate |
| Automatic bin packing | Places containers on nodes using the CPU and memory you declare for each |
| Self-healing | Restarts failed containers and withholds traffic until a container is ready |
| Secret and configuration management | Updates secrets and configuration without rebuilding images |
| Batch execution | Runs batch and CI workloads and replaces failed containers if you want |
| Horizontal scaling | Scales up or down by command, in a UI, or automatically on CPU usage |
It also helps to know what Kubernetes leaves out. It is not an all-inclusive platform as a service: it does not build your code, run your CI/CD pipeline, or ship databases, message buses and caches as built-in services, and it does not choose your logging or monitoring tools. It is also not orchestration in the strict sense of running step A, then B, then C. It is a set of independent control processes that keep driving the current state toward the state you declared.
The core idea: you declare, controllers converge
Everything in Kubernetes is an object, a persistent record in the cluster. The objects documentation calls an object a record of intent: once you create it, the system works constantly to make it exist. Almost every object has a spec, which is the state you want, and a status, which Kubernetes fills in with the state it observes.
Take a Deployment whose spec asks for three replicas. Kubernetes reads the spec and starts three instances. If one fails, the status no longer matches the spec, and Kubernetes starts a replacement. The component doing this work is a controller, which runs a control loop, a non-terminating loop that regulates the state of a system. The docs compare it to a thermostat that switches equipment on and off to bring a room toward the temperature you set.
The practical consequence is that you rarely issue instructions such as "start this container". You change the declared state, and controllers work out the steps. That is why this post is built from YAML files you apply, not scripts you run.
The objects you use every day
A handful of object types cover most applications. The table lists them in the order you meet them.
| Object | What it is | How you use it |
|---|---|---|
| Pod | The smallest deployable unit: one or more tightly coupled containers with shared storage and network | The unit that actually runs, normally created through a workload resource and not by hand |
| ReplicaSet | Keeps a stable set of replica Pods running | Rarely touched directly, because a Deployment manages it for you |
| Deployment | Declarative updates for Pods and ReplicaSets, usually for apps that keep no state | Running and updating a stateless service |
| Service | A stable network endpoint for a changing set of Pods | Giving clients one name and address to call |
| ConfigMap | Non-confidential configuration stored as key-value pairs | Settings consumed as environment variables, arguments or files |
| Secret | A small amount of sensitive data such as a password, token or key | Credentials kept out of images and Pod specs |
| Namespace | A scope that isolates groups of resources inside one cluster | Separating teams or projects, since names must be unique only within a namespace |
| Node | A worker machine, virtual or physical, that runs Pods | The capacity underneath everything, managed for you by the control plane |
Other workload resources cover other shapes of work. A StatefulSet gives each Pod a sticky identity for applications that need persistent storage or a stable network identity. A DaemonSet runs node-local facilities such as a networking helper. A Job runs a one-off task to completion.
Pods are ephemeral. The Service documentation says you should not expect any individual Pod to be reliable or durable, because Pods are created and destroyed to match the desired state. Each Pod gets its own IP address, but the set of healthy Pods keeps changing. That is the reason Services exist: clients need one stable name in front of Pods that come and go.
One caution about Secrets. By default they are stored unencrypted in etcd, and anyone with API access can read them, as can anyone allowed to create a Pod in the namespace. The Secret documentation tells you to enable encryption at rest and to restrict access with RBAC. A ConfigMap provides no secrecy at all.
Here is a minimal Deployment, the object you will write most often.
# hello.yaml apiVersion: apps/v1 kind: Deployment metadata: name: hello-node spec: replicas: 2 selector: matchLabels: app: hello-node template: metadata: labels: app: hello-node spec: containers: - name: agnhost image: registry.k8s.io/e2e-test-images/agnhost:2.53 command: ["/agnhost", "netexec", "--http-port=8080"] ports: - containerPort: 8080 Read it from the top. apiVersion and kind name the API and the object type, and metadata.name names the object. Everything under spec is desired state: two replicas of a Pod built from the template. The selector tells the Deployment which Pods it owns by matching the template's labels, and the Deployment documentation says the API rejects a Deployment whose two do not match. Labels are plain key-value pairs that Services, Deployments and policies use to find Pods.
How a cluster is built
A cluster has a control plane and one or more worker nodes. The components overview says the control plane components manage the overall state of the cluster, and the node components run on every node, maintaining running Pods and providing the runtime environment.
| Component | Runs on | Job |
|---|---|---|
| kube-apiserver | Control plane | Serves the Kubernetes HTTP API as the front end of the control plane, and scales horizontally by running more instances |
| etcd | Control plane | Consistent, highly available key-value store for all cluster data, which you need a backup plan for |
| kube-scheduler | Control plane | Watches for newly created Pods with no node and selects a node for each |
| kube-controller-manager | Control plane | Runs the controller processes, compiled into one binary and run as one process |
| cloud-controller-manager | Control plane, optional | Integrates with the underlying cloud provider |
| kubelet | Every node | Makes sure the containers described in PodSpecs are running and healthy, and ignores containers Kubernetes did not create |
| kube-proxy | Every node, optional | Maintains the network rules that implement Services |
| Container runtime | Every node | The software that actually runs containers |
Every arrow runs through the API server. The Kubernetes API documentation says users, the different parts of your cluster and external components all communicate with one another through it. The scheduler, the controllers and the kubelets never call each other. Each watches the API for work and writes its results back, so a component that restarts can read the current state again.
Three consequences shape every manifest in this post. The scheduler reads requests, so wrong requests mean wrong placement. The kubelet enforces limits, so wrong limits mean CPU throttling or out-of-memory kills. Controllers retry forever, so a bad rollout never fixes itself unless a probe tells the truth.
What happens when you run kubectl apply
Follow one command through the system. The CLI turns your file into API calls, as the objects documentation describes, and from there the work passes between components without any of them calling another directly.
A chain of hand-offs turns that file into running containers. The API server accepts and stores the Deployment. The Deployment controller creates a ReplicaSet, and the ReplicaSet creates the Pods, which the Deployment documentation describes as happening in the background. The scheduler sees Pods without a node and picks one for each. The kubelet on that node starts the containers through the runtime and reports status back.
Try it on a local cluster
You need a cluster and kubectl. The learning environment page lists kind, which runs cluster nodes as Docker containers, and minikube, which runs a single-node cluster on your machine, among other options. Install one of them and kubectl, then confirm that kubectl get nodes shows a node in the Ready state.
kubectl apply -f hello.yaml kubectl get deployments kubectl get pods kubectl scale deployment/hello-node --replicas=4 kubectl get pods kubectl delete pod <one-of-the-pod-names> kubectl get pods kubectl get events kubectl port-forward deployment/hello-node 8080:8080 Run them in order. The first creates the Deployment and, through it, two Pods. Scaling to four changes the desired state, and Kubernetes starts two more. Deleting a Pod does not lower the count: the ReplicaSet notices the gap and creates a replacement, the behavior the docs list as replica replacement under self-healing. kubectl get events shows the hand-offs from the previous section as they happened.
The port-forward command tunnels local port 8080 to a Pod, so you can open http://localhost:8080 and see the test image echo your request back, the same image the official Hello Minikube tutorial uses. It does not return until you stop it with Ctrl+C. When you are done, run kubectl delete -f hello.yaml to remove the Deployment and its Pods.
Where Kubernetes stands in October 2026
That demo skipped everything production needs: pinned versions, resource limits, probes, security controls and a safe way to take traffic. The rest of this post adds them, starting with the versions you can choose from.
The project maintains release branches for the three most recent minor releases and supports each patch series for roughly fourteen months: twelve in the standard period, then two in maintenance mode, when new releases cover only CVE fixes, dependency issues and critical core component issues. As of October 10, 2026, the releases page lists the following.
| Minor | Latest patch listed | End of life |
|---|---|---|
| 1.37 | 1.37.1 (September 15, 2026) | October 28, 2027 |
| 1.36 | 1.36.5 | June 28, 2027 |
| 1.35 | 1.35.9 | February 28, 2027 |
| 1.34 | 1.34.12 | October 27, 2026 |
The next patch releases for all four lines are scheduled for October 13, 2026, so the patch numbers above will be stale within days. The fourth row, 1.34, is in maintenance mode, which began on August 27, 2026, and its end of life is under three weeks away. Plan the move to 1.35 or later now, and upgrade one minor version at a time, because kubeadm does not support skipping minor versions.
The last two releases were large. Kubernetes 1.36 (Haru), released April 22, 2026, had 70 enhancements: 18 Stable, 25 Beta and 25 Alpha. Kubernetes 1.37 (Garhwal) had 67: 16 Stable, 23 Beta, 27 Alpha and one deprecation or removal. You do not need most of them. You need the handful that change how you write manifests.
The baseline, one manifest at a time
The target is one service called api in a namespace called shop, behind a shared Gateway, with three replicas spread across zones. Replace the names, the host, the image digest and the GatewayClass. Everything else is meant to be applied as written.
Save each block below as its own file, in order, using the file name in the first comment line.
1. Namespace: enforce Pod Security Admission
# 00-namespace.yaml apiVersion: v1 kind: Namespace metadata: name: shop labels: pod-security.kubernetes.io/enforce: restricted pod-security.kubernetes.io/enforce-version: latest pod-security.kubernetes.io/warn: restricted pod-security.kubernetes.io/audit: restricted shared-gateway-access: "true" Pod Security Admission has been stable since 1.25. The restricted profile rejects Pods that run as root, allow privilege escalation, keep default Linux capabilities or lack a seccomp profile. Setting enforce, warn and audit together means you see violations in three places. The last label is used by the Gateway in step 4.
2. Deployment
# 01-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: api namespace: shop labels: app.kubernetes.io/name: api spec: # replicas is omitted on purpose: the HPA in step 6 owns it revisionHistoryLimit: 3 strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 maxSurge: 1 selector: matchLabels: app.kubernetes.io/name: api template: metadata: labels: app.kubernetes.io/name: api spec: hostUsers: false automountServiceAccountToken: false terminationGracePeriodSeconds: 45 securityContext: runAsNonRoot: true runAsUser: 10001 runAsGroup: 10001 seccompProfile: type: RuntimeDefault topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: ScheduleAnyway labelSelector: matchLabels: app.kubernetes.io/name: api containers: - name: api # Pin by digest. Replace this placeholder with your real image digest. image: registry.example.com/shop/api@sha256:0000000000000000000000000000000000000000000000000000000000000000 ports: - name: http containerPort: 8080 resources: requests: cpu: 250m memory: 256Mi limits: memory: 256Mi resizePolicy: - resourceName: cpu restartPolicy: NotRequired - resourceName: memory restartPolicy: RestartContainer securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] startupProbe: httpGet: path: /healthz port: http periodSeconds: 2 failureThreshold: 30 readinessProbe: httpGet: path: /readyz port: http periodSeconds: 5 failureThreshold: 2 livenessProbe: httpGet: path: /healthz port: http periodSeconds: 10 failureThreshold: 3 lifecycle: preStop: sleep: seconds: 10 volumeMounts: - name: tmp mountPath: /tmp volumes: - name: tmp emptyDir: sizeLimit: 64Mi Four settings carry most of the weight. hostUsers: false puts the Pod in its own user namespace, so root inside the container maps to an unprivileged user on the node. User namespaces reached GA in 1.36 and are Linux-only. The concept page lists the node requirements: Linux 6.3 or later for idmap mounts on tmpfs, runc 1.2 or later or crun 1.9 or later, and containerd 2.0 or later or CRI-O 1.25 or later. Check your nodes before relying on the setting.
maxUnavailable: 0 with maxSurge: 1 makes a rollout add one new Pod and wait for it to be Ready before removing an old one. The topology spread constraint keeps replicas balanced across zones. ScheduleAnyway means a zone outage does not leave Pods Pending. Switch to DoNotSchedule only if imbalance is worse than reduced capacity for your service.
The three probes answer three different questions. The startup probe allows a slow boot of up to 60 seconds (30 failures at 2 second intervals) before the other probes begin. Readiness decides whether the Pod receives traffic, and should check dependencies you cannot serve without. Liveness should check only that the process is not wedged. Never make liveness depend on a database, or a database outage becomes a restart storm.
The preStop sleep and the 45 second grace period solve a race. Endpoint removal and SIGTERM happen at about the same time, so for a few seconds a terminating Pod can still receive requests. Sleeping 10 seconds before SIGTERM gives proxies time to stop sending traffic. Your application must still handle SIGTERM by finishing in-flight requests inside the remaining 35 seconds.
The sleep hook action is Stable since 1.34, when the PodLifecycleSleepAction feature gate became locked on, so every supported minor has it. The lifecycle hooks page lists it beside the exec and HTTP handlers. The kubelet runs it, so your image needs no sleep binary.
Memory request equals memory limit, and there is no CPU limit. Memory cannot be compressed, so an equal limit makes OOM behavior predictable. CPU can be throttled, and a CPU limit often throttles bursts the node could have served. The Pod still gets a CPU guarantee through its request. Revisit this choice if you run a multi-tenant cluster that needs hard caps.
3. Service
# 02-service.yaml apiVersion: v1 kind: Service metadata: name: api namespace: shop spec: type: ClusterIP selector: app.kubernetes.io/name: api ports: - name: http port: 80 targetPort: http The Service gives the Pods one stable virtual IP and a DNS name, api.shop.svc. It is ClusterIP on purpose. Traffic from outside the cluster enters through the Gateway, not through a Service of type LoadBalancer per application, and not through the deprecated externalIPs field covered later.
4. Gateway and HTTPRoute
# 03-gateway-and-route.yaml apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: public namespace: gateway-system spec: gatewayClassName: example-gateway-class listeners: - name: https protocol: HTTPS port: 443 hostname: api.example.com tls: mode: Terminate certificateRefs: - kind: Secret name: api-example-com-tls allowedRoutes: namespaces: from: Selector selector: matchLabels: shared-gateway-access: "true" --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: api namespace: shop spec: parentRefs: - name: public namespace: gateway-system sectionName: https hostnames: - api.example.com rules: - matches: - path: type: PathPrefix value: / backendRefs: - name: api port: 80 Gateway API splits ownership. The platform team owns the Gateway and its listeners, and application teams own the routes that attach to it. Gateway and HTTPRoute have been GA since Gateway API v1.0. Gateway API v1.6.0, released June 30, 2026, also moved TCPRoute and UDPRoute to the Standard channel at API version v1.
Gateway API is not built into the API server. You install its CRDs and a controller that implements your GatewayClass, and example-gateway-class is a placeholder for that controller's class name. The allowedRoutes selector is why step 1 labeled the shop namespace: a route from an unlabeled namespace is not allowed to attach.
The manifest assumes two objects already exist: the gateway-system namespace, and a kubernetes.io/tls Secret named api-example-com-tls inside it. Per the Gateway CRD at v1.6.0, a certificate reference into another namespace is invalid unless a ReferenceGrant allows it, and the listener then reports ResolvedRefs as False with the reason RefNotPermitted. Create both before you apply this file.
5. PodDisruptionBudget
# 04-pdb.yaml apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: api namespace: shop spec: maxUnavailable: 1 selector: matchLabels: app.kubernetes.io/name: api A PodDisruptionBudget limits voluntary evictions, such as a node drain during an upgrade. With it, a drain can take one api Pod at a time and must wait for the replacement to be Ready. It does not protect against crashes or node failures. Prefer maxUnavailable over minAvailable, so the budget keeps working when the HPA changes the replica count.
6. HorizontalPodAutoscaler
# 05-hpa.yaml apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: api namespace: shop spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: api minReplicas: 3 maxReplicas: 12 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 behavior: scaleDown: stabilizationWindowSeconds: 300 policies: - type: Percent value: 25 periodSeconds: 60 Leaving spec.replicas out of the Deployment makes the HPA the only writer of the replica count. If both write, every kubectl apply resets it. On the first apply the Deployment starts at one replica until the HPA raises it to minReplicas, so apply both in the same command. CPU utilization is measured against the request, which is why requests must be honest.
Resource metrics come from the metrics.k8s.io API, usually served by metrics-server. That API graduated to Stable in 1.37 after nearly nine years in Beta. The scale-down policy above removes at most 25% of replicas per minute, and only after five minutes of consistently lower demand, which avoids flapping on spiky traffic.
7. NetworkPolicy
# 06-networkpolicy.yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny namespace: shop spec: podSelector: {} policyTypes: - Ingress - Egress --- apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-gateway-to-api namespace: shop spec: podSelector: matchLabels: app.kubernetes.io/nameSource:https://dev.to/devstories_fca96eb46af114/kubernetes-in-2026-what-it-is-how-it-works-and-a-production-baseline-you-can-apply-today-28o9