Kubectl commands: A comprehensive guide

A practical guide covering Kubectl shortcuts, autocomplete, context switching, selectors, output formats, troubleshooting, and advanced tips to boost your Kubernetes productivity.

11 minutes reading time

Written by

Jubril Oyetunji
Jubril Oyetunji

Technical Writer at Civo

This tutorial is for anyone looking to get more productive with kubectl, the Kubernetes command-line management tool. If you deal with Kubernetes clusters and their components, you will be doing it through kubectl. While this tutorial assumes you have kubectl already installed, I hope you find something useful here. I have tried to cover tricks for benefiting beginner kubectl usage, as well as more advanced tips.

Setting up kubectl autocomplete

The first thing I will always do when using kubectl is set autocomplete, which allows you to use tab auto-completion when composing commands. This is extremely useful; for example, if you are unsure of the exact name of a resource in a namespace, you can compose your command with -n namespace first, and then tab will present the resources in that particular namespace, which saves running a kubectl get to retrieve all resources first.

If bash:

source <(kubectl completion bash)
echo "source <(kubectl completion bash)" >> ~/.bashrc

On Bash, kubectl completion depends on the bash-completion package being installed first. If completion does nothing after sourcing, install bash-completion from your package manager (for example apt install bash-completion or brew install bash-completion), then reopen your shell. Zsh has the completion machinery built in, so it needs no extra package.

For Zsh:

source <(kubectl completion zsh)
echo "[[ \$commands[kubectl] ]] && source <(kubectl completion zsh)" >> ~/.zshrc

Once enabled, hit Tab to complete resource names inside a namespace instead of looking them up by hand.

kubectl configuration and contexts

A context bundles three things together: a cluster, a user (the credentials), and a default namespace. Switching context is how you jump from staging to production without retyping connection details every time.

Point kubectl at a specific kubeconfig file, either through the environment or per command:

KUBECONFIG=~/.kube/config
kubectl get nodes --kubeconfig ~/custom_config

List the contexts you have, see which one is active, and switch between them:

kubectl config get-contexts
kubectl config current-context
kubectl config use-context production-context
kubectl config view

kubectl config get-contexts gives you the full picture at a glance:

CURRENT NAME CLUSTER AUTHINFO NAMESPACE
* staging-context staging staging-user default
production-context production production-user default

The asterisk marks the active context. After use-context, every subsequent command talks to that cluster with that user and that default namespace.

Working with namespaces

Namespaces partition a cluster into separate areas. Most commands default to the default namespace, so you will reach for these flags constantly.

Target one namespace, or span all of them at once:

kubectl get pods -n namespace-name
kubectl get pods -A

If you are living in one namespace for a while, stop typing -n every time and pin it to your current context:

kubectl config set-context --current --namespace=namespace-name

That setting sticks per context, so staging and production can each remember their own default namespace.

Troubleshooting Kubernetes pods

Pending pods

When a pod is stuck in Pending, it has not been scheduled to a node yet. The reason is almost always in the events at the bottom of describe:

kubectl describe pod pod-name

Look for messages about insufficient CPU or memory, unsatisfied node selectors, or taints the pod cannot tolerate.

Failed pods and container logs

When a pod is running but misbehaving, the logs are your first stop. Pull logs from a pod, a specific container, or every container at once:

kubectl logs pod-name -n namespace
kubectl logs pod-name -n namespace -c container1
kubectl logs pod-name -n namespace --all-containers

Follow a live stream with -f to watch output as it happens:

kubectl logs pod-name -n namespace -c container1 -f

Limit to the recent past with a relative window:

kubectl logs pod-name -n namespace -c container1 --since=5m

And if the container has restarted, read the logs from the instance that died, not the fresh one:

kubectl logs pod-name -n namespace --previous

Time-bounded logs: pinning an incident window

The --since=5m flag is great for "what just happened," but incident reviews need absolute time. When you are reconstructing "10:00 to 10:15 last night," relative windows are useless. We will use --since-time with an RFC3339 timestamp, and add --timestamps so every line carries its own clock.

Start logs from an exact instant:

kubectl logs civo-pod -n civo-namespace --since-time="2026-06-05T10:00:00Z"

Print a timestamp on every line so you can see exactly when each event landed:

kubectl logs civo-pod -n civo-namespace --since-time="2026-06-05T10:00:00Z" --timestamps

Combine with --tail to cap how much you pull back, which is useful on chatty pods:

kubectl logs civo-pod -n civo-namespace --since-time="2026-06-05T10:00:00Z" --timestamps --tail=200

Grab the previous container instance with timestamps, handy right after a crash-loop restart:

kubectl logs civo-pod -n civo-namespace --previous --timestamps

Now the honest limitation: there is no --until. kubectl can set a start time but not an end time. So to isolate a window like 10:00 to 10:15, we set --since-time to the start, add --timestamps, and filter the tail of the window with standard text tools:

# logs between 10:00:00 and 10:15:00 (RFC3339 timestamps sort lexicographically)
kubectl logs civo-pod -n civo-namespace --since-time="2026-06-05T10:00:00Z" --timestamps \
| awk '$1 <= "2026-06-05T10:15:00Z"'

Or grep a single suspicious minute out of that stream:

kubectl logs civo-pod -n civo-namespace --since-time="2026-06-05T10:00:00Z" --timestamps \
| grep "2026-06-05T10:12"

Two things to remember: only one of --since and --since-time may be used at once, and RFC3339 timestamps sort correctly as plain strings, which is exactly why the awk comparison above works. kubectl is blunt about both rules if you cross them:

kubectl logs ... --until=1m -> error: unknown flag: --until
kubectl logs ... --since --since-time -> error: at most one of `sinceTime` or `sinceSeconds` may be specified

Confirming there is no --until flag and that --since and --since-time are mutually exclusive.

Watching for changes

Instead of running get in a loop, ask kubectl to stream updates with -w:

kubectl get pods -w

When you need to block a script until a resource reaches a state, use wait:

kubectl wait --for=condition=Ready pod/civo-pod --timeout=30s
kubectl wait --for=condition=Ready=false pod/civo-pod
kubectl wait --for=delete pod/civo-pod

wait is the difference between a flaky sleep-and-hope script and one that proceeds the instant the cluster is actually ready.

Using labels and selectors

Labels are how you slice a cluster into the set of resources you care about. Selectors are how you query by those labels.

Match a single label:

kubectl get pods --selector=app=civo-app

Negate a label to exclude a set, for example every node that is not a control-plane node:

kubectl get node --selector='!node-role.kubernetes.io/master'

Field selectors filter on built-in fields rather than labels, and you can combine them:

kubectl get pods --field-selector status.phase=Running
kubectl get pods --field-selector=status.phase!=Running,spec.restartPolicy=Always

Sorting query results

Sort output to make patterns jump out, by name or by any field in the resource:

kubectl get services --sort-by=.metadata.name
kubectl get pv --sort-by=.spec.capacity.storage

The --sort-by value is a JSONPath into the resource, so you can sort by almost anything the API exposes.

Monitoring resource usage

kubectl top shows live CPU and memory consumption for pods and nodes:

kubectl top pods
kubectl top nodes
kubectl top pod civo-pod --containers
kubectl top nodes --sort-by memory

One prerequisite catches people out: kubectl top needs metrics-server running in the cluster. Without it, you get error: Metrics API not available and no clue why. Check whether it is installed:

kubectl get deployment metrics-server -n kube-system

If that returns nothing, install metrics-server first. On a fresh cluster, even after installing it, kubectl top can briefly return error: metrics not available yet until metrics-server has collected its first sample, so give it a minute.

Working with output formats

YAML output

Dump the full resource definition to a file, useful for inspection or as a starting manifest:

kubectl get pod civo-pod -o yaml > civo-pod.yaml

JSON with jq

JSON output plus jq lets you ask precise questions. Find every pod that has restarted at least ten times across all namespaces:

kubectl get pods -A -o json | jq -r '.items[] | select(.status.containerStatuses[].restartCount >= 10) | .metadata.namespace + "/" + .metadata.name + " = " + (.status.containerStatuses[].restartCount | tostring)'

You can even generate other commands. This one finds paused KubeVirt VMs and prints the virtctl command to restart each:

kubectl get virtualmachineinstances.kubevirt.io -A -o json | jq '.items[] | select(.status.conditions[] | select(.type=="Paused" and .status =="True")) | {"namespace": .metadata.namespace, "name": .metadata.name}' -rc | jq '"virtctl restart " + .name + " -n " + .namespace' -r
virtctl restart virtual-machine-instance -n machine-namespace

Custom columns

When you want a tidy table of exactly the fields you care about, use custom-columns:

kubectl get pods --output=custom-columns="NAME:.metadata.name,IMAGE:.spec.containers[*].image"
NAME IMAGE
kube-apiserver-01 k8s.gcr.io/kube-apiserver:v1.21.9

Inspecting events

Events are the cluster's running commentary on what it is doing and why things fail. The bottom of describe shows events for a single object:

kubectl describe pod civo-pod

To see events across the namespace in time order, sort them:

kubectl get events --sort-by=.metadata.creationTimestamp

A quick note on the sort key, because most blog posts use a different one. You will often see --sort-by=.lastTimestamp. On modern clusters --sort-by=.metadata.creationTimestamp is the more reliable choice, because on the events.k8s.io/v1 API lastTimestamp is a legacy compatibility field (the modern equivalents are eventTime and series.lastObservedTime). Sorting by .metadata.creationTimestamp sidesteps the whole question, which is why this guide uses it.

Debugging controllers: walk the Deployment to ReplicaSet to Pod chain

When a rollout is stuck, the pod is often the last place the useful information shows up. A Deployment creates a ReplicaSet, the ReplicaSet creates Pods, and each layer emits its own events. We will walk the chain from the top.

First, ask the Deployment directly whether it is making progress:

kubectl rollout status deploy/civo-app
kubectl rollout history deploy/civo-app

Read the controller-emitted events scoped to just this Deployment:

kubectl get events --field-selector involvedObject.kind=Deployment,involvedObject.name=civo-app

Drop to warnings only when the noise is high:

kubectl get events --field-selector type=Warning,involvedObject.kind=Deployment,involvedObject.name=civo-app

Sort events chronologically. --sort-by=.lastTimestamp is the classic form; --sort-by=.metadata.creationTimestamp is the more reliable key on modern clusters for the reason described just above:

kubectl get events --sort-by=.lastTimestamp
kubectl get events --sort-by=.metadata.creationTimestamp

Watch events live across all namespaces while you trigger the rollout in another terminal:

kubectl get events -A -w

Then describe each layer. The Deployment tells you which ReplicaSet it is scaling; the ReplicaSet tells you why it cannot create pods (quota, image pull, scheduling):

kubectl describe deployment/civo-app
kubectl describe replicaset -l app=civo-app

To see the chain concretely, deliberately break a rollout by pointing it at an image tag that does not exist. The Warning events surface the failure right away:

Warning Failed pod/civo-app-59488f449b-xxxxx Error: ImagePullBackOff
deployment/civo-app ScalingReplicaSet Scaled up replica set civo-app-59488f449b to 1

Warning events surfacing the ImagePullBackOff after a deliberately broken rollout.

The mental model: if rollout status hangs, describe the Deployment to find the new ReplicaSet, describe that ReplicaSet to see whether pods are even being created, and only then describe a Pod. The stuck reason is usually one layer above where people start looking.

Executing commands in pods

Run a one-off command inside a running container:

kubectl exec civo-pod -- ls /

Or open an interactive shell with -i -t:

kubectl exec civo-pod -c bash-container -i -t -- bash

The -c flag picks the container when a pod has more than one.

Copying data to and from containers

kubectl cp moves files in either direction between your machine and a container:

kubectl cp /tmp/file.txt civo-namespace/civo-pod:/root/
kubectl cp civo-namespace/civo-pod:/root/file.txt /tmp/

This is handy for pulling out a config dump or pushing in a test fixture without rebuilding an image.

The Oh My Zsh kubectl plugin

If you use Zsh with Oh My Zsh, its kubectl plugin gives you a set of short aliases for commonly used commands (for example k for kubectl, kgp for kubectl get pods). Enable it in your .zshrc plugin list and you shave a lot of typing off everyday work.

krew: the kubectl plugin manager

The Oh My Zsh plugin above gives you shell aliases. krew is a different thing: it is the official plugin manager from Kubernetes SIG CLI that installs new kubectl <verb> subcommands. This is the real extension ecosystem for kubectl.

Install krew with the official snippet:

(
set -x; cd "$(mktemp -d)" &&
OS="$(uname | tr '[:upper:]' '[:lower:]')" &&
ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')" &&
KREW="krew-${OS}_${ARCH}" &&
curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/latest/download/${KREW}.tar.gz" &&
tar zxvf "${KREW}.tar.gz" &&
./"${KREW}" install krew
)

Then add krew to your PATH (put this in ~/.bashrc or ~/.zshrc):

export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH"

Open a new shell, refresh the plugin index, and confirm it works:

kubectl krew update
kubectl krew search
kubectl krew list

Install a few high-value plugins:

kubectl krew install ctx ns stern view-secret neat tree

What each one buys you:

  • ctx (kubectl ctx): switch between clusters and contexts without typing the long kubectl config use-context form.
  • ns (kubectl ns): switch your default namespace in one word.
  • stern (kubectl stern <query>): tail logs from many pods at once with color-coded, per-pod prefixes. This is the multi-pod answer to kubectl logs -f.
  • view-secret (kubectl view-secret <secret>): print a Secret's values already base64-decoded, instead of piping through base64 -d by hand.
  • neat (kubectl neat): strip the server-added clutter (managedFields, status, default annotations) out of -o yaml, leaving a clean manifest you can commit.
  • tree (kubectl tree <kind> <name>): show the ownership tree of a resource, for example a Deployment down through its ReplicaSet to its Pods.

Two of them in action:

kubectl stern civo-app --since 5m
kubectl tree deployment civo-app

kubectl tree makes the controller chain we walked earlier visible at a glance:

Deployment/civo-app
├─ReplicaSet/civo-app-59488f449b
└─ReplicaSet/civo-app-6b9557cdb8
├─Pod/civo-app-6b9557cdb8-9kn9l True Current
└─Pod/civo-app-6b9557cdb8-g79v7 True Current

The kubectl tree krew plugin showing the Deployment to ReplicaSet to Pod chain.

Getting a shell on a node

Sometimes the answer is not in a pod, it is on the node: a full disk, a kubelet problem, a sketchy iptables rule. There are two ways in, and which one is available depends on your provider.

Approach 1 (provider-agnostic): kubectl debug node

This works on any cluster where you have node-debug permissions, including managed clusters where you cannot SSH at all. It launches a throwaway pod in the node's host namespaces, with the node filesystem mounted at /host. Find the node name first with kubectl get nodes, then:

kubectl debug node/<node-name> -it --image=busybox

Once you are in the debug container, chroot into the host filesystem to get something close to a real node shell:

chroot /host

From there you can inspect the node almost as if you had SSHed in: check disk with df -h, look at processes (the container already shares the host PID, network, and IPC namespaces), and read kubelet logs. When you exit, the debug pod is cleaned up. Reaching the host this way returns exactly what you would expect:

node hostname: minikube
kernel: 6.12.54-linuxkit
overlay 59G 23G 33G 42% /

kubectl debug node with chroot /host reading the node hostname, kernel, and disk.

Approach 2 (real SSH): only where the provider exposes node IPs

Here is the honest provider note. On Civo's managed Kubernetes service the worker nodes do not get a public IP and are not reachable by SSH; the cluster has a single public IP and the nodes never appear as SSH-able instances. So on a managed Civo cluster, use Approach 1.

Real SSH applies when you run k3s yourself on standalone Civo Compute instances (for example the k3sup pattern). In that case, each instance is a normal VM with a public IP, and if you attached your uploaded SSH key at creation time, you can connect directly:

# self-managed k3s on Civo Compute instances (NOT managed Civo Kubernetes)
civo instance show -o custom -f public_ip <instance-name>
ssh civo@<node-public-ip>

Rule of thumb: managed cluster, use kubectl debug node; your own k3s on Civo instances, plain SSH with your key works.

Summary

If you use Kubernetes at all, familiarity with the variety of things you can achieve with Kubectl is a must. Aside from the commands themselves, and the plugins like the ZSH one mentioned above, a set of very useful command-line tools exist to further enhance your cluster management and debugging experience. You can read about some of them in this post on cluster administration from the command line.

Jubril Oyetunji
Jubril Oyetunji

Technical Writer at Civo

Jubril Oyetunji is a DevOps engineer and technical writer with a strong focus on cloud-native technologies and open-source tools. His work centers on creating practical tutorials that help developers better understand platforms such as Kubernetes, NGINX, Rust, and Go.

As a contract technical writer, Jubril authored an extensive library of technical guides covering cloud-native infrastructure and modern development workflows. Many of his tutorials achieved strong search rankings, helping developers around the world learn and adopt emerging technologies.

View author profile