Skip to content
Control Plane Labs

kubectl cheatsheet (2026 edition)

A working kubectl reference for Kubernetes 1.36: output formats, JSONPath, debug workflows, server-side apply, drain safety, and the version skew rules.

Ben Ennis

Published July 27, 2026

Most kubectl cheatsheets are a dump of every subcommand in alphabetical order, which is useless when you are mid-incident. What follows is organized the way you actually work: find the object, understand the object, change the object, debug the object, then get off the node safely. The official reference is exhaustive and worth bookmarking separately; this is the opinionated subset, plus the behavior changes worth knowing about in the current release.

What is different in 2026

Kubernetes ships three minor releases a year and supports the three most recent minors at any time. As of this writing that is v1.34, v1.35, and v1.36, with v1.36 (“Haru”) released on 22 April 2026 according to the release announcement and the releases page. Each minor gets roughly a year of patch support, so a cluster more than three minors behind is running unpatched control-plane code.

Version skew is the rule people break most often. The version skew policy states that kubectl is supported within one minor version, older or newer, of kube-apiserver. A v1.36 kubectl against a v1.33 API server is out of support, and the symptoms are not a clean error message: you get missing fields, silently dropped flags, and confusing validation output. If you administer clusters on different versions, install per-cluster binaries rather than one global kubectl.

Two v1.36 changes are worth putting in muscle memory. kubectl kuberc graduated to beta, which means the preferences file described in the kuberc documentation is now a supported way to set default flags and aliases without wrapping kubectl in shell functions. And kubectl diff gained a --show-secret flag, so diffing a Secret no longer forces you to choose between redacted output and piping YAML through your own diff. Both changes are listed in the v1.36 changelog.

Context and namespace, once, correctly

Almost every kubectl mistake with real consequences starts with the wrong context. Make the current context visible in your shell prompt, and set the namespace on the context rather than typing -n every time.

kubectl config get-contexts
kubectl config use-context prod-eu-west-1
kubectl config set-context --current --namespace=payments
kubectl config view --minify --output 'jsonpath={..namespace}'

Kubeconfig files merge. If KUBECONFIG lists several paths separated by colons, kubectl builds one logical config from all of them, and the first occurrence of a duplicated key wins — the details are in the kubeconfig documentation. This is why a stale file in ~/.kube/config can shadow the credentials your CI helper just wrote.

Finding objects

kubectl get with a good output format answers most questions without opening a dashboard.

# Wide output plus node placement, sorted by restart count
kubectl get pods -o wide --sort-by='.status.containerStatuses[0].restartCount'

# Everything not in a healthy phase, across all namespaces
kubectl get pods -A --field-selector='status.phase!=Running'

# Only the fields you care about
kubectl get pods -o custom-columns='NAME:.metadata.name,NODE:.spec.nodeName,IMAGE:.spec.containers[*].image'

# Machine-readable, for scripts
kubectl get deploy -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.replicas}{"\n"}{end}'

Two things trip people up here. Field selectors are not label selectors and they are not general-purpose: the API server only supports a small set of indexed fields per resource type, documented under field selectors. status.phase and metadata.name work on pods; arbitrary paths do not. And kubectl’s JSONPath is its own dialect with documented differences from the original spec, described in the JSONPath support page — notably {range} and {end} for iteration, which no other JSONPath implementation has.

Labels are the primary selection mechanism and the one most teams under-invest in. A cluster where every workload carries app.kubernetes.io/name, app.kubernetes.io/component, and an owner label is a cluster you can query during an incident; a cluster where labels were an afterthought forces you into name-prefix guessing. The well-known labels reference lists the names Kubernetes itself and most controllers understand, and it is worth conforming to them rather than inventing a local scheme.

kubectl get pods -l 'app.kubernetes.io/name=checkout,environment in (prod,canary)'
kubectl get pods -l '!app.kubernetes.io/managed-by'
kubectl label deploy/checkout owner=payments-team --overwrite
kubectl annotate deploy/checkout change-ticket=CHG-4821 --overwrite

Set-based selectors (in, notin, and existence checks with !) are handled by the API server, so they cost nothing extra and they scale. Object names are constrained more tightly than people expect: most resources require a lowercase RFC 1123 subdomain, which is why an underscore in a Deployment name fails validation while the same string works fine as a label value. The object names page lists the rules per resource type.

For “what is happening right now”, kubectl events beats kubectl describe because it sorts and filters properly:

kubectl events --for pod/checkout-7d8f9 --watch
kubectl events -A --types=Warning

The kubectl events reference covers the filters. Remember that events are ephemeral; the default retention is one hour, so “no events” during a post-incident review means nothing.

Understanding a resource without leaving the terminal

kubectl explain reads the OpenAPI schema from your actual API server, which makes it more trustworthy than any web reference for the cluster in front of you.

kubectl explain pod.spec.tolerations
kubectl explain deployment.spec.strategy --recursive
kubectl api-resources --namespaced=true --verbs=list
kubectl api-versions

That last pair is how you discover what CRDs a cluster has and what their short names are. See the kubectl explain reference for the flag list.

Changing things without surprises

The declarative path is apply, and in 2026 there is little reason not to use server-side apply for anything a controller or a second human might also touch.

kubectl diff -f deploy.yaml
kubectl apply --server-side --field-manager=ci-pipeline -f deploy.yaml
kubectl apply -k overlays/prod/
kubectl rollout status deploy/checkout --timeout=180s
kubectl rollout undo deploy/checkout

Server-side apply moves merge logic into the API server and records field ownership, so two actors editing the same object produce an explicit conflict instead of a silent overwrite. When you do hit a conflict, --force-conflicts takes ownership; read the conflict message first, because it names the other manager.

Kustomize is built in, so -k works without a separate binary; the kustomization task page is the reference. For quick one-offs, generate YAML rather than creating objects imperatively:

kubectl create deployment web --image=nginx:1.27 --dry-run=client -o yaml > web.yaml
kubectl create job --from=cronjob/nightly-report manual-run-2026-07-27

The second command is the safe way to test a CronJob: it creates a Job from the CronJob template immediately, without touching the schedule.

When you need to block until something is true, use kubectl wait rather than a sleep loop. It watches the object and exits non-zero on timeout, which makes it usable in CI:

kubectl wait --for=condition=Available deploy/checkout --timeout=120s
kubectl wait --for=delete pod/migration-job-xyz --timeout=60s
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded job/db-migrate --timeout=10m

The jsonpath form is the general escape hatch and works against custom resources, which is useful when a CRD does not publish standard conditions.

One warning about kubectl apply --prune: it deletes objects that match its selector and are absent from the input, which sounds like the reconciliation feature you wanted and behaves like a foot-gun in practice, because the selector semantics changed across versions and a mistyped label can delete unrelated objects. If you want prune semantics, use a GitOps controller that computes the diff from a recorded inventory. If you must use it, run it with --dry-run=server first and read every line of output.

Debugging a running workload

kubectl logs deploy/checkout --all-containers --since=15m
kubectl logs pod/checkout-7d8f9 -c sidecar --previous
kubectl exec -it pod/checkout-7d8f9 -c app -- sh
kubectl port-forward svc/checkout 8080:80
kubectl cp checkout-7d8f9:/tmp/heap.prof ./heap.prof

--previous reads the logs of the last terminated container, which is the only way to see why a CrashLoopBackOff pod died. If the image is distroless and has no shell, kubectl exec is useless and kubectl debug is the answer: it attaches an ephemeral container that shares the pod’s namespaces.

# Attach a debug container to a running pod
kubectl debug -it pod/checkout-7d8f9 --image=busybox:1.36 --target=app

# Copy a pod and change its command, leaving the original untouched
kubectl debug pod/checkout-7d8f9 --copy-to=checkout-debug --set-image='*=busybox:1.36' -- sleep 1d

# Get a root shell on a node
kubectl debug node/ip-10-0-1-23 -it --image=busybox:1.36

The debug-running-pod task walks through all three modes and their permission requirements. Node debugging is effectively node root access, so it should be an audited, break-glass operation rather than a habit.

Ephemeral containers have two properties that surprise people. They cannot be removed once added — the pod carries them until it is replaced — and they cannot declare resource requests, so they run inside whatever headroom the pod’s existing limits leave. On a pod that is already at its memory limit, attaching a debug container can be the thing that triggers the OOM kill you were investigating. Prefer --copy-to on production pods that are still serving traffic, and reserve in-place debugging for pods that are already broken.

For resource questions, kubectl top needs metrics-server installed — it reads the Metrics API, not the API server’s object store, as described in the resource usage monitoring docs. If kubectl top returns an error about the metrics API, the cluster is missing an addon, not misconfigured.

Node operations that will not page someone

kubectl cordon ip-10-0-1-23
kubectl drain ip-10-0-1-23 --ignore-daemonsets --delete-emptydir-data --timeout=300s
kubectl uncordon ip-10-0-1-23

drain respects PodDisruptionBudgets, which means it can block indefinitely, which is the correct behavior — a drain that ignores PDBs is an outage. The safely drain a node task and the disruptions concept page explain the interaction. Always set --timeout so an automation loop fails loudly instead of hanging, and check for single-replica Deployments with a minAvailable: 1 PDB before you start, because that combination can never be satisfied.

Permissions and identity

kubectl auth whoami
kubectl auth can-i create deployments --namespace payments
kubectl auth can-i --list --namespace payments
kubectl auth can-i delete pods --as=system:serviceaccount:payments:runner

kubectl auth whoami prints the identity the API server actually attributes to your credentials, which resolves most “why is this forbidden” questions in one command — see the auth whoami reference. --as impersonation is the fastest way to test a ServiceAccount’s RBAC before shipping a workload that depends on it.

Making kubectl faster to type

Set defaults in kuberc rather than aliasing every command, keep kubectl and k both working, and install plugins with krew instead of copying binaries into /usr/local/bin. Two conventions from the kubectl conventions page are worth adopting: prefer --dry-run=server over --dry-run=client when you want real admission-controller feedback, and never rely on the default namespace in scripts.

Shell completion is the other high-value setup step, because it turns resource names and namespaces into tab-completable values and removes an entire class of typo. It is installed per shell with kubectl completion bash or kubectl completion zsh, documented alongside the install instructions. Pair it with a prompt that prints the current context and namespace. The cost of a wrong-cluster delete is high enough that a permanently visible context string pays for itself the first week.

The habit that saves the most time is not an alias. It is running kubectl diff before every kubectl apply, on every cluster, every time. It costs two seconds and it catches the class of mistake that no amount of YAML review does.

Frequently asked questions

Is it safe to use a newer kubectl against an older cluster?+
Only within one minor version. The version skew policy supports kubectl at one minor older or newer than kube-apiserver. Outside that window, expect dropped fields and misleading validation errors rather than a clean refusal.
Why does kubectl get pods not show the field I filter on?+
Field selectors only work on fields the API server indexes for that resource, listed under field selectors. Everything else must be filtered client-side with JSONPath, jq, or labels.
What is the difference between kubectl apply and kubectl apply --server-side?+
Client-side apply computes the merge patch locally from a last-applied annotation. Server-side apply sends your intent to the API server, which tracks per-field ownership and reports conflicts when another manager owns a field you are trying to change.
How do I debug a pod whose image has no shell?+
Use kubectl debug with an ephemeral container: kubectl debug -it pod/name --image=busybox --target=app. It joins the running pod's namespaces without restarting it. See the debug-running-pod task.
Why does kubectl drain hang?+
It is waiting on a PodDisruptionBudget that cannot be satisfied, most often a single-replica Deployment with minAvailable set to 1. That is the PDB doing its job. Scale up, fix the budget, or accept the disruption explicitly — do not reach for a force flag first.

Tags: #kubernetes, #kubectl, #cli, #operations