kubectl Essentials

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

kubectl is the primary way a human drives a Kubernetes cluster, and this page collects the commands and flags used on almost every other page in this section: how kubectl structures a command, the three ways to manage objects, the read commands (get, describe, explain), the everyday operational commands (logs, exec, cp, port-forward, top, events, wait), the write commands that generate and reconcile manifests (--dry-run, diff, server-side apply, patch, label, annotate), and the discovery and extension commands (api-resources, auth can-i/auth whoami, plugins and krew). It is written against kubectl 1.37 talking to a Kubernetes 1.37 API server (kubectl supports servers one minor version above or below itself); every command works unmodified against the Bookshelf cluster built on Getting Started with Kubernetes. Where a command corrects something either book gets wrong for the current release, that correction is called out in prose next to the command it replaces.

Command structure and the three ways to manage objects

Every kubectl invocation has the same shape:

kubectl [command] [TYPE] [NAME] [flags]

command is a verb (get, apply, delete, …), TYPE is a resource type (pods, deploy, svc, or a plural, singular or short form — kubectl api-resources lists every valid form), NAME is optional and, when omitted, means "all objects of that type in scope". Global flags such as -n/--namespace, -A/--all-namespaces, --context and --kubeconfig are accepted by almost every command, because they belong to the client-side connection, not the verb.

Kubernetes recognizes three distinct techniques for getting objects onto the cluster, and mixing them on the same object is the single most common source of confusion:

Technique How it works Example Trade-offs

Imperative commands

A single kubectl command tells the server exactly what to do, right now (create, expose, scale, set image, run, delete, label, annotate without -f). No manifest file is required or produced.

kubectl create deployment web --image=ghcr.io/example/bookshelf-web:1.0.0

Fastest for one-off changes and demos; nothing in version control describes the object, so the cluster’s actual state and its intended state can drift silently. Best for throwaway objects and generating a manifest to edit (see --dry-run below), not for anything that matters.

Imperative object configuration

A manifest file is required, but the command still names the specific action (create -f, replace -f, delete -f). The file is the source of truth for one operation, not for reconciliation.

kubectl create -f k8s/base/deployment-web.yaml

The file can live in version control, but replace -f overwrites the whole object (it fails if the object does not already exist, and a concurrent field set by something else, such as an autoscaler’s replicas, is clobbered). Rarely the right default for day-to-day operation.

Declarative configuration (apply)

kubectl apply -f computes a three-way diff (the file’s last applied configuration, the file’s current content and the object’s live state) and sends only the changes; the object is created if it does not exist. Directories of manifests, and kubectl diff / kubectl apply --prune for a whole set, are first-class.

kubectl apply -f k8s/base/

The technique every Bookshelf example in this section uses from Kustomize onward. It composes safely with other actors changing the same object (see server-side apply below), but the first apply against an object created another way needs kubectl apply --server-side or an explicit last-applied-configuration annotation to behave correctly; Objects and the Kubernetes API covers resourceVersion and optimistic concurrency underneath all three techniques.

Client-side apply (the default, without --server-side) stores the last-applied configuration as the kubectl.kubernetes.io/last-applied-configuration annotation on the object and diffs against it locally; it is what every kubectl apply -f in the earlier pages of this section has been doing. Server-side apply, covered later on this page, replaces that local diff with a request the API server itself resolves, and is what apply should move to for anything with more than one writer.

Reading objects: get, describe and explain

kubectl get lists objects, kubectl describe renders a detailed, human-formatted view including recent Events, and kubectl explain prints an object type’s schema straight from the API server’s OpenAPI/discovery document, so it is always in sync with the exact cluster version in front of you — more reliable for "what field goes where" than searching the web docs for one API version.

kubectl get deployments -n bookshelf
kubectl describe deployment catalog-api -n bookshelf
kubectl explain deployment.spec.strategy
kubectl explain pod.spec.containers.livenessProbe --recursive
NAME           READY   UP-TO-DATE   AVAILABLE   AGE
catalog-api    3/3     3            3           4d2h
reviews-api    3/3     3            3           4d2h
web            2/2     2            2           4d2h

--recursive expands every nested field of the requested path instead of stopping one level down, which is the fastest way to see an entire embedded object’s shape (a probe, a volume, a full container spec) in one screen.

Output formats

-o/--output reshapes what get prints. Each format solves a different problem:

Format Use it for

-o wide

A few extra columns on top of the default table — node name, Pod IP, container images — without switching to a structured format. kubectl get pods -o wide is the everyday troubleshooting command; Book 2’s chapter 5 uses the same flag to check which node each Redis Pod landed on.

-o yaml, -o json

The full object, including server-populated fields (status, metadata.managedFields, metadata.uid). The starting point for --dry-run=client -o yaml manifest generation below, and for `diff`ing against a file.

-o name

Just <type>/<name>, one per line — built for piping into another command: kubectl delete $(kubectl get pods -o name -l app.kubernetes.io/name=notifier).

-o jsonpath='{…​}'

A JSONPath expression evaluated against the object(s), for extracting one or two fields into a script.

-o custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image

A table with columns you name explicitly, when -o wide does not show the field you need but a full dump is too much.

-o go-template='…​'

A Go text/template expression, for formatting kubectl cannot do with JSONPath alone (loops with intermediate variables, arithmetic, custom separators).

kubectl get pods -n bookshelf -o wide
kubectl get deploy catalog-api -o jsonpath='{.spec.template.spec.containers[0].image}'
kubectl get pods -n bookshelf \
  -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,RESTARTS:.status.containerStatuses[0].restartCount
kubectl get pods -n bookshelf -o go-template='{{range .items}}{{.metadata.name}}{{"\t"}}{{.status.phase}}{{"\n"}}{{end}}'
NAME                           READY   STATUS    RESTARTS   AGE     IP            NODE                 NOMINATED NODE   READINESS GATES
catalog-api-7b8f9c9d6f-2n7kq   1/1     Running   0          4d2h    10.244.1.12   bookshelf-worker     <none>           <none>
catalog-api-7b8f9c9d6f-8qr2m   1/1     Running   0          4d2h    10.244.2.9    bookshelf-worker2    <none>           <none>

Book 2’s kubectl get … -a (--show-all) has no replacement to reach for: the flag was removed once completed and failed Pods stopped being hidden from the default get pods output, so today’s plain kubectl get pods already shows everything a Job left behind.

Selecting, watching and sorting

Flag Meaning

-l app.kubernetes.io/name=catalog-api, -l 'environment in (dev,staging)'

Label selector: equality-based (key=value, key!=value) or set-based (in, notin, key, !key), comma meaning logical AND. Objects and the Kubernetes API covers the syntax and length limits in full; every Bookshelf Deployment carries the recommended app.kubernetes.io/name and app.kubernetes.io/part-of=bookshelf labels this selector matches against.

--field-selector status.phase=Running,spec.nodeName=bookshelf-worker

Field selector: matches on a fixed, resource-specific set of fields (not arbitrary JSONPath), combined with commas as AND. Fewer fields are selectable than labels, but it reaches into status and spec, which labels cannot.

--watch / -w

Keep the connection open and stream further changes as additional table rows, instead of exiting after one snapshot. --watch-only suppresses the initial list.

--sort-by='{.metadata.creationTimestamp}', --sort-by='{.status.containerStatuses[0].restartCount}'

A JSONPath expression to sort the listed objects by, most useful for finding the oldest or the most-restarted Pod in a long list.

-A / --all-namespaces

Ignore the current namespace and list across every namespace; adds a NAMESPACE column.

kubectl get pods -n bookshelf -l app.kubernetes.io/name=catalog-api
kubectl get pods -A --field-selector status.phase=Running
kubectl get pods -n bookshelf -w
kubectl get pods -n bookshelf --sort-by='{.status.containerStatuses[0].restartCount}'

Working with running Pods

Command Purpose

kubectl logs -f -p -c reviews-api --since=10m --tail=200 reviews-api-7f6d8[…​]

Stream a container’s stdout/stderr. -f follows, -p/--previous reads the last terminated container’s log (the one command to see why something CrashLoopBackOff`ed after it already restarted), `-c names a container in a multi-container Pod, --since bounds by time, --tail bounds by line count.

kubectl logs -f -l app.kubernetes.io/name=reviews-api --max-log-requests=10 --prefix

-l streams every matching Pod at once (fan-out across replicas), capped by --max-log-requests; --prefix labels each line with its source Pod/container so the interleaved stream stays readable.

kubectl exec -it catalog-api-7b8f9c9d6f-2n7kq — sh

Run a process inside a container’s namespaces and attach a TTY. Book 2’s kubectl exec pod cmd (no --) breaks as soon as cmd starts with a flag kubectl exec would otherwise try to parse itself; -- marks the end of kubectl’s own flags, so it belongs on every `exec invocation, and -c selects the container the same way it does for logs: kubectl exec -it redis-0 -c redis — redis-cli ping.

kubectl cp bookshelf/catalog-api-7b8f9c9d6f-2n7kq:/app/logs/app.log ./app.log

Copy files between the local machine and a container, using tar under the hood — fine for a one-off pull, not a substitute for a volume or a log pipeline.

kubectl port-forward svc/reviews-api 8081:80 -n bookshelf

Forward a local port to a Service, Pod or Deployment, for reaching something with no external entry point yet. Every earlier page’s port-forward examples are this same command.

kubectl proxy --port=8080

Run a local HTTP proxy that authenticates to the API server once and re-exposes it (with CORS handling) on localhost, so curl localhost:8080/api/v1/namespaces/bookshelf/pods needs no token of its own —  Objects and the Kubernetes API uses exactly this to watch the API with curl.

kubectl top pods -n bookshelf, kubectl top nodes

Current CPU/memory usage from the Metrics API (needs metrics-server installed; see Autoscaling) — a live snapshot, not a history, unlike Prometheus.

kubectl events -n bookshelf --for pod/catalog-api-7b8f9c9d6f-2n7kq

List Events for one object, or the whole namespace, newest last by default; the same data describe shows inline at the bottom of its output, queryable and sortable on its own.

kubectl wait --for=condition=Available deployment/catalog-api --timeout=120s, kubectl wait --for=delete pod/…

Block until a condition is met (or a timeout elapses) — the scriptable alternative to polling get in a loop, used to gate a CI/CD pipeline on a rollout actually finishing (see GitOps and CI/CD).

kubectl logs -f --tail=50 -l app.kubernetes.io/name=catalog-api -n bookshelf
kubectl exec -it -n bookshelf redis-0 -c redis -- redis-cli ping
kubectl wait --for=condition=Available=true deployment/web -n bookshelf --timeout=90s
deployment.apps/web condition met

Generating and reconciling manifests

--dry-run=client runs every validation kubectl can do locally and, combined with -o yaml, turns an imperative command into a starting manifest instead of an object — the fastest way to get a syntactically correct skeleton to then hand-edit and commit:

kubectl create deployment catalog-api --image=ghcr.io/example/bookshelf-catalog-api:1.0.0 \
  --dry-run=client -o yaml > k8s/base/deployment-catalog-api.yaml
kubectl create configmap catalog-api-config --from-literal=LOG_LEVEL=info \
  --dry-run=client -o yaml

--dry-run=server instead sends the request to the API server with the persist step skipped, so admission webhooks, ValidatingAdmissionPolicy (see Admission Control and Policy) and defaulting all run for real — a stronger check than client, at the cost of one round trip to a live cluster.

kubectl diff -f k8s/base/ shows what kubectl apply -f k8s/base/ would change, without changing anything; it shells out to diff (or KUBECTL_EXTERNAL_DIFF) over the same three-way comparison apply itself computes, and belongs in front of every apply run against something that matters.

Server-side apply, field managers and conflicts

Client-side apply computes its three-way diff on the machine running kubectl, from a locally cached last-applied-configuration annotation — fragile once more than one actor (a person, a CI pipeline, an operator, an HPA) touches the same object. Server-side apply (kubectl apply --server-side) moves that merge into the API server itself: the client sends only the fields it actually cares about, and the server tracks, per field, which field manager set it, recorded in metadata.managedFields.

kubectl apply --server-side -f k8s/base/deployment-catalog-api.yaml
kubectl get deployment catalog-api -n bookshelf -o jsonpath='{.metadata.managedFields}' | jq
managedFields:
  - manager: kubectl
    operation: Apply
    apiVersion: apps/v1
    fieldsType: FieldsV1
    fieldsV1:
      f:spec:
        f:replicas: {}
        f:template:
          f:spec:
            f:containers: {}
  - manager: horizontal-pod-autoscaler
    operation: Update
    apiVersion: apps/v1
    fieldsV1:
      f:spec:
        f:replicas: {}

When two managers try to own the same field with different values, the server returns a conflict instead of silently picking one — this is what stops kubectl apply from fighting the HPA over spec.replicas, or a Helm release from fighting a hand-run kubectl edit. Resolve a conflict one of three ways: drop the contested field from your manifest and let the other manager keep it; change the manifest and force the take-over with --force-conflicts (the new value wins, and the field moves to your manager); or, for legitimate shared fields, have every manager apply the same value, which is allowed without a conflict.

kubectl apply --server-side --force-conflicts -f k8s/base/deployment-catalog-api.yaml

Client-side apply (the plan’s and every earlier page’s default) remains the simpler choice for a single pipeline that is the only writer of an object; move to --server-side as soon as something else — an autoscaler, an operator, another team’s pipeline — writes to the same object too.

ApplySet pruning

Plain kubectl apply -f <dir> --prune -l <selector> deletes objects that match the selector but are no longer in the directory — but it identifies which objects belong to the set purely by that label selector, so a typo, or another tool’s object carrying the same label, can be deleted by mistake. ApplySet-based pruning replaces the selector with an explicit, server-tracked set: kubectl stamps a parent object with an applyset.kubernetes.io/id and every member object it applies gets a matching label, so --prune --applyset=<parent> deletes exactly, and only, what that specific apply run put there. The feature is still alpha in kubectl, so it must be enabled with the KUBECTL_APPLYSET=true environment variable:

KUBECTL_APPLYSET=true kubectl apply --server-side --prune --applyset=configmap/bookshelf-applyset \
  -n bookshelf -f k8s/base/

Because ApplySet pruning is still maturing feature by feature across recent minors, re-verify its exact status (alpha/beta and required feature gates) against the server-side apply reference before relying on it outside a throwaway cluster; the legacy label-selector --prune remains available but keeps the "read the current disclaimer before use" caveat it has always carried. Kustomize and GitOps and CI/CD show the alternative most teams reach for instead: let Argo CD or Flux own pruning declaratively, rather than passing --prune by hand.

edit, patch, label and annotate

kubectl edit deployment catalog-api -n bookshelf         # opens the live object in $EDITOR, applies on save

kubectl patch deployment catalog-api -n bookshelf \
  -p '{"spec":{"template":{"spec":{"containers":[{"name":"catalog-api","env":[{"name":"LOG_LEVEL","value":"debug"}]}]}}}}'
                                                          # strategic merge patch (default): understands lists that
                                                          # merge by key, e.g. containers by name, instead of by index

kubectl patch configmap catalog-api-config -n bookshelf --type merge \
  -p '{"data":{"LOG_LEVEL":"debug"}}'                    # RFC 7386 JSON merge patch: plain object merge, no
                                                          # Kubernetes-aware list semantics

kubectl patch deployment catalog-api -n bookshelf --type json \
  -p '[{"op":"replace","path":"/spec/replicas","value":5}]'
                                                          # RFC 6902 JSON patch: an explicit list of operations
                                                          # (add/remove/replace/test) against exact paths

kubectl label pods -n bookshelf -l app.kubernetes.io/name=reviews-api tier=backend --overwrite
kubectl annotate deployment catalog-api -n bookshelf kubernetes.io/change-cause="bump log level to debug"

edit is imperative (whatever you save wins, with no reconciliation); patch is the scriptable equivalent, and its --type choice matters most for arrays — a strategic merge patch on a container list finds the entry by name and merges into it, where a JSON merge patch would replace the whole list. label/annotate are shorthand patches for exactly those two maps, and both refuse to overwrite an existing key without --overwrite.

Discovery, authorization and extending kubectl

kubectl api-resources                                    # every resource type the server serves, with its API
                                                           # group, whether it is namespaced, and its short names
kubectl api-resources --namespaced=true --verbs=list,get
kubectl api-versions                                     # every served <group>/<version>, e.g. apps/v1, batch/v1
kubectl auth can-i create deployments -n bookshelf
kubectl auth can-i delete pods -n bookshelf --as=system:serviceaccount:bookshelf:notifier
kubectl auth whoami                                      # the identity kubectl itself is currently authenticated as
yes
no
ATTRIBUTE   VALUE
Username    kubernetes-admin
Groups      [system:masters system:authenticated]

auth can-i answers one RBAC question at a time and, with --as, answers it as another identity — indispensable when preparing a ServiceAccount’s Role before ever running a Pod with it (see Authentication, Authorization and RBAC). auth whoami answers a different question — not "can I", but "who does the server think I am right now" — useful after switching kubeconfig contexts or assuming a cloud identity, to confirm which user or group a subsequent can-i check is actually being evaluated against.

Plugins and krew

A kubectl plugin is any executable on $PATH named kubectl-<name>; running kubectl <name> finds and execs it, passing through global flags such as --namespace and --context automatically. krew is the plugin manager for this ecosystem — itself installed as a plugin (kubectl krew) — that indexes, installs and upgrades community plugins from a public manifest index:

kubectl krew install neat tree who-can
kubectl neat get deployment catalog-api -n bookshelf -o yaml   # strips managedFields, status and other server noise
kubectl tree deployment catalog-api -n bookshelf               # ReplicaSet + Pod ownership tree, one command instead
                                                                 # of following owner references by hand
kubectl who-can delete pods -n bookshelf                        # every RoleBinding/ClusterRoleBinding granting a verb
kubectl krew upgrade

kubectl for Docker users

Docker CLI kubectl equivalent

docker run

kubectl run (bare Pod only — see the correction below) or, for anything durable, kubectl create deployment / kubectl apply -f

docker ps

kubectl get pods

docker ps -a

kubectl get pods (completed/failed Pods are already included; see the -a correction below)

docker inspect <container>

kubectl get pod <name> -o yaml / kubectl describe pod <name>

docker logs -f <container>

kubectl logs -f <pod>

docker exec -it <container> sh

kubectl exec -it <pod> — sh

docker stop/docker rm

kubectl delete pod <name> (a controller such as a Deployment recreates it — delete the Deployment/ReplicaSet instead to actually remove the workload)

docker cp

kubectl cp

docker stats

kubectl top pods

docker network create / docker run --network

a Service plus, for isolation, a NetworkPolicy — there is no per-command network flag

docker volume create / docker run -v

a Volume or a PersistentVolumeClaim referenced from spec.volumes

docker-compose up

kubectl apply -f a directory of manifests, or helm install

Book 2 corrections

Book 2’s terminal sessions predate several kubectl changes that would otherwise mislead a reader following along on 1.37:

  • kubectl run --restart=OnFailure creating a Job. In the book’s era, kubectl run had generators that could produce a Job, a Deployment or a bare Pod depending on --restart and other flags. The generators were removed in 1.18: kubectl run now always creates a single, bare Pod, whatever --restart is set to (only Always, OnFailure and Never remain, and they set the Pod’s own restartPolicy, not a controller choice). Use kubectl create job --image=… notifier-once — <command> for the one-shot behaviour the book was reaching for, or kubectl create deployment for a durable, replicated workload — Jobs and CronJobs rebuilds the book’s exact Job patterns this way. Along with it, prefer the recommended app.kubernetes.io/name / app.kubernetes.io/part-of labels over the run: <name> label kubectl run still applies by default, since a Service or NetworkPolicy selecting run: … reads as legacy the moment a reader sees it next to any other object in this section. Separately, kubectl run --filename/-f (reading a Pod override document from a file) is deprecated in 1.37; pass --overrides with inline JSON, or an actual manifest applied with kubectl apply -f, instead.

  • kubectl get … -a (--show-all). Already covered above: the flag is gone, and today’s plain get already shows completed and failed Pods.

  • kubectl exec pod cmd without --. Already covered above: always write kubectl exec pod — cmd, and add -c <container> in a multi-container Pod (the sampler’s Redis StatefulSet, rebuilt on Stateful Applications in Practice, needs exactly this to reach the redis container rather than its sidecar).

Job objects also carry two auto-generated labels worth knowing before you write a selector against one: job-name (the owning Job’s name) and controller-uid (a value unique to that particular Job object, distinguishing two Jobs that happen to share a name over time). kubectl create job sets manualSelector: false by default, meaning the Job controller manages these labels itself; hand-writing spec.selector and setting manualSelector: true is possible but easy to get wrong, and is not needed for anything in Bookshelf.