kubectl and Your First Pod
Build the mental model of kubectl and bring up your first Pod. From one imperative cycle of kubectl run to the declarative YAML manifest, the everyday commands get / describe / logs / exec, the Pod lifecycle, and common failure patterns like ImagePullBackOff · CrashLoopBackOff.
In Chapter 2 Local Environment we put a cluster on a laptop. In this chapter we put our first Pod on that cluster, go inside it, read logs, and delete it cleanly — one whole cycle. Along the way we learn kubectl’s basic commands and a Pod’s role together.
By the end of this chapter kubectl’s everyday command set is second nature, and you’ll be in a state of having created the unit called a Pod once with your own hands. From Chapter 4 Deployment and ReplicaSet on, we move to the story of not creating these Pods by hand but leaving them to a controller.
What a Pod is #
In the resource table of Chapter 1 What Kubernetes Is there was a one-line definition — a Pod is K8s’s smallest execution unit, bundling 1 container or a few that work together. Let’s unpack it a bit.
A reader who has come as far as Docker is already familiar with the unit called a container. K8s adds one more layer on top — a Pod is the unit that bundles one or several containers and schedules and runs them together. Containers in the same Pod share the following.
- Network namespace — they share the same IP and the same port space. Inside one Pod they call each other over
localhost. - Some volumes — several containers can co-mount a volume defined on the Pod.
- Lifetime — they come up together and die together (on the same node).
This is the point most easily confused with a docker container. In docker it was “1 container = 1 execution unit,” but in K8s it’s “1 Pod = 1 execution unit,” and that Pod may have 1 container or 2. As a formula in your head, Pod ≈ “a group of containers sharing one IP” is a fine fit.
That said, 99%+ of real-world work is 1 Pod, 1 container. Putting two containers in the same Pod is limited to cases where it’s natural for that container to ride along with the main one, like a sidecar (log collector, proxy, etc.). At first, understanding “Pod = one layer K8s wraps around a single container to run it” is enough.
One more thing — in operations you almost never write kind: Pod as a manifest yourself. Usually Chapter 4 Deployment creates them for you. We handle Pods directly in this chapter to lay the foundation for understanding K8s. The unit a person writes as a manifest is almost always a controller like Deployment · StatefulSet · Job, and a Pod is the product that controller creates.
kubectl commands in one table #
Let’s organize the everyday commands worth making second nature into one table. This is the shape you’ll meet repeatedly throughout the book.
| Command | What it does |
|---|---|
kubectl get <res> | lists resources. e.g., kubectl get pods, kubectl get nodes |
kubectl get <res> <name> | shows a summary of one specific resource |
kubectl get <res> -o wide | shows extra info like IP · node beyond the default columns |
kubectl get <res> <name> -o yaml | prints that resource’s full definition as YAML |
kubectl describe <res> <name> | prints that resource’s details together with recent Events |
kubectl logs <pod> | shows the Pod container’s stdout / stderr |
kubectl logs -f <pod> | follow — works like tail -f |
kubectl exec -it <pod> -- <cmd> | runs a command inside a running Pod container |
kubectl apply -f file.yaml | reflects a manifest into the cluster (declarative) |
kubectl delete -f file.yaml | deletes the resource created from the same manifest |
kubectl delete <res> <name> | deletes a resource by name |
kubectl run <name> --image=<img> | creates one Pod imperatively, on the spot |
In this table the <res> part takes names like pods, pod, po, deployments, deploy, services, svc. There’s a short form for each commonly used resource, so the amount of typing drops quickly.
Imperative vs declarative #
There are two grains to handling a cluster with kubectl.
- Imperative — like
kubectl run,kubectl create deployment, you directly command “make this now.” It’s fast and light-handed, but to reproduce that command you have to have the command itself written down somewhere. - Declarative — you write “it should be this shape” in a YAML manifest and call
kubectl apply -f file.yaml. The manifest file is itself a record of the cluster state, so it’s easy to put in Git, and re-runningapplyon the same file yields the same state.
Real-world work is almost entirely declarative. The decisive point is that cluster state is recorded as code. This book too brings up a Pod imperatively only for the first time, then moves straight to manifests. This model is covered more seriously once more with ArgoCD / Flux in Chapter 20 GitOps.
First Pod, imperatively — kubectl run #
First let’s bring up one Pod the shortest way. We use nginx:1.27, which you can pull anywhere.
kubectl run hello --image=nginx:1.27 --port=80pod/hello created
--port=80is an informational flag. Making external traffic come in on this port is covered in Chapter 5 Service. At this stage, treat it as just a note that “the nginx inside the Pod listens on 80.”
Check the result that was created.
kubectl get podsNAME READY STATUS RESTARTS AGE
hello 1/1 Running 0 12sREADY 1/1 means 1 of the 1 container in the Pod is ready, and STATUS Running is the shape we expected. Pinning the column names in one line — NAME / READY / STATUS / RESTARTS / AGE. You’ll see them often to the end of the book.
When you need a bit more detail, append -o wide.
kubectl get pods -o wideNAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
hello 1/1 Running 0 25s 10.244.0.5 kind-control-plane <none> <none>IP is the cluster-internal IP assigned to the Pod, and NODE shows which worker node it’s up on. Since it’s a single-node kind cluster they all gather on the one kind-control-plane, but in a multi-node cluster this shows where each Pod scattered. NOMINATED NODE and READINESS GATES are usually empty, filled in when you use advanced scheduling / conditions.
describe — what happened inside #
The list alone doesn’t tell you how the Pod got there. describe fills that gap.
kubectl describe pod helloName: hello
Namespace: default
Priority: 0
Node: kind-control-plane/172.18.0.2
Start Time: ...
Labels: run=hello
...
Containers:
hello:
Image: nginx:1.27
Port: 80/TCP
State: Running
Started: ...
Ready: True
Restart Count: 0
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 30s default-scheduler Successfully assigned default/hello to kind-control-plane
Normal Pulling 29s kubelet Pulling image "nginx:1.27"
Normal Pulled 25s kubelet Successfully pulled image "nginx:1.27"
Normal Created 25s kubelet Created container hello
Normal Started 25s kubelet Started container helloUp top is the Pod’s definition (node, labels, containers, state), and at the bottom Events are written in time order. This Events section is the first starting point of K8s debugging — Scheduled → Pulling → Pulled → Created → Started. The flow we saw only as a diagram in Chapter 1 is written out directly. The scheduler decides the node, that node’s kubelet pulls the image, then creates and starts the container, in order.
When something goes wrong, the answer is almost always in these Events too. If you wrote the image name wrong, Failed to pull image shows up here; if a container dies right after starting, Back-off restarting failed container comes up. The finished version of this diagnostic flow is covered in Chapter 27 kubectl debugging patterns.
logs — what the Pod is saying #
You view a Pod container’s stdout / stderr with kubectl logs.
kubectl logs hello/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
...
/docker-entrypoint.sh: Configuration complete; ready for start up
... [notice] start worker processesIt’s a command that maps 1:1 to docker’s docker logs. The common variants are the same too.
kubectl logs -f hello # follow
kubectl logs --tail=100 hello # last 100 lines
kubectl logs --since=10m hello # the last 10 minutes
kubectl logs --previous hello # logs of the container that died earlierThe last one, --previous, is an option you only meet in K8s, so it’s worth getting used to. If a container died once and started again, the logs from just before it died are the output of the previous container instance, not the current container’s stdout. You use it often when tracing a crash cause.
exec — going inside a Pod #
It’s the command that maps to docker’s docker exec.
kubectl exec -it hello -- bash-it is interactive + TTY, and the -- after it is a separator meaning from here on, the command runs inside the Pod. If you drop the --, kubectl interprets the following options as its own, which is a common mistake.
Go in, check nginx’s config once, and come out.
root@hello:/# curl -s localhost:80 | head -5
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
root@hello:/# exitIf you only want to run a one-off command, you can do it without -it.
kubectl exec hello -- nginx -v
# nginx version: nginx/1.27.xFirst cleanup #
That’s one imperative cycle. Tear it down cleanly.
kubectl delete pod hellopod "hello" deletedConfirm with kubectl get pods that it’s empty, and you’re back at the starting point.
Again, declaratively — the YAML manifest #
This time let’s write the same Pod as a manifest. Almost every K8s manifest starts with four top-level fields.
| Field | Meaning |
|---|---|
apiVersion | the version of the K8s API that defines this resource. Pod is v1 of the stable group, so v1 |
kind | the kind of resource. Here it’s Pod |
metadata | identifying info like name · labels · namespace |
spec | the body of “how this resource should be.” Its shape differs per resource kind |
The shape of these four fields is the same whether Pod or Deployment or Service. Learn it and it becomes the spine of every manifest.
Write a hello-pod.yaml file as follows.
apiVersion: v1
kind: Pod
metadata:
name: hello
labels:
app: hello
spec:
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80Call apply in the same directory.
kubectl apply spits out an error different from what you intended, leaving you to trace the cause back from the cluster side. Pasting the manifest into utilrepo’s YAML validator before you apply it points out syntax errors by line and column number. utilrepo is a lightweight collection of browser-based web utilities; no secret information leaves your machine, and it catches common traps like multi-document manifests joined by --- and mixed tabs and spaces. You can use it the same way in every manifest exercise in this book.kubectl apply -f hello-pod.yamlpod/hello createdConfirm the result is the same with kubectl get pods — the shape that shows is exactly like kubectl run. The difference is outside the cluster.
Advantages of a manifest #
- It goes into Git. Cluster state is recorded as code, so you can review “the shape of this environment” as a PR and keep a history.
- Re-running
applyyields the same state. Imperative depends on your head or terminal history, but a manifest is the file itself as the record. applyis idempotent. If it already exists, it reflects only the diff; if not, it creates it. This property fits CI / CD well.- It extends naturally into a controller. Changing only the
kindof the same manifest toDeploymentmoves you straight to Chapter 4.
Fields K8s fills in #
Only what we wrote went into the manifest, but the object that actually entered the cluster has many more fields attached. Let’s take a look.
kubectl get pod hello -o yaml | head -40apiVersion: v1
kind: Pod
metadata:
creationTimestamp: "..."
labels:
app: hello
name: hello
namespace: default
resourceVersion: "..."
uid: ...-...-...-...-...
spec:
containers:
- image: nginx:1.27
imagePullPolicy: IfNotPresent
name: web
ports:
- containerPort: 80
protocol: TCP
...
nodeName: kind-control-plane
...
status:
conditions:
- ...
phase: Running
podIP: 10.244.0.5
...The parts we wrote (name · labels · image · port) are still there, and K8s filled in fields like the following.
metadata.uid,metadata.resourceVersion,metadata.creationTimestamp— this object’s identityspec.nodeName— the node the scheduler decided- defaults like
spec.containers[].imagePullPolicy: IfNotPresent - the whole
statusblock — Phase, Pod IP, conditions, and so on, the actual state
The desired state vs actual state we saw in Chapter 1 sitting together inside one object is exactly this shape. spec is desired, status is actual. The controller continuously compares the two.
The Pod lifecycle, once #
A Pod has the concept of a Phase. What the STATUS column of kubectl get pod output mostly reflects is this Phase. The exact values are five.
| Phase | Meaning |
|---|---|
Pending | the object is created but its containers haven’t started yet. It may be downloading the image or waiting for node assignment |
Running | assigned to a node, and at least one container is running |
Succeeded | all containers terminated successfully (exit 0). They won’t restart |
Failed | all containers terminated, and at least one of them ended abnormally |
Unknown | the apiserver lost communication with the node and can’t tell the state |
A Pod that must stay up, like a web server, spends its life in Running. A Pod that should run once and finish, like a batch job, goes into Succeeded or Failed.
Let’s trace this flow once more in the Events section of kubectl describe — Scheduled → Pulling → Pulled → Created → Started. It’s exactly the order of the output we saw above. Where it stops among these five words becomes the first fork in debugging.
Two common failures #
The two failures you meet most often at first are these.
ImagePullBackOff / ErrImagePull — a state where the image couldn’t be pulled. It might be a name typo, a missing private-registry credential, or that tag really not existing. The Events of kubectl describe pod <name> have a Failed to pull image "..." message with the reason written below it. Start from there.
CrashLoopBackOff — a state where the container dies right after starting, K8s restarts it, it dies again, and so on, repeatedly. The first clue is kubectl logs <pod>, and if you need the logs of the container that just died, use kubectl logs --previous <pod>. It also shows as the RESTARTS column climbing fast.
You meet these two patterns repeatedly throughout the book. Just keeping the order — the first clue is describe’s Events, the second clue is logs — solves 9 out of 10. The finished version of the diagnostic tree is organized in Chapter 27 kubectl debugging patterns.
Why one Pod isn’t enough #
If you’ve followed this far, one thing naturally starts to nag — what happens to this Pod when it dies? Let’s force-delete it once as an experiment.
kubectl delete pod hellopod "hello" deletedkubectl get podsNo resources found in default namespace.It just disappears. It doesn’t come back up automatically. From K8s’s point of view, we never wrote down anywhere that “this Pod must exist.” Since a person deleted an object that was created once with apply, K8s took it as that person’s intent.
For the reconcile loop we saw in Chapter 1 to work, there must be a higher-level declaration that “N of these must be up.” What holds that is the star of the next chapter, the Deployment. A Deployment creates a ReplicaSet, and the ReplicaSet takes on the responsibility of “keep N with this Pod template.” So when you do the same experiment with a Deployment, a new Pod comes back up automatically right after you delete one.
A word the K8s official docs often use about Pods is mortal — meaning something that dies someday. If a node dies, a container dies with OOM, or someone deletes it by accident, the Pod just disappears, and there’s no one separately to bring it back. Auto-restart · rescheduling isn’t free from K8s — it’s provided through a controller.
So the one line from the start comes back — in operations you almost never use kind: Pod directly. Except special cases like a temporary debug Pod, the one-off Pod a Job creates, or the Pod a DaemonSet brings up per node, the manifests we write are almost always controllers. This chapter’s Pod is the foundation for seeing what those controllers create inside.
Cleanup and teardown #
Wipe today’s experiment traces clean. The surest way to delete resources made from a manifest is with the same manifest.
kubectl delete -f hello-pod.yamlpod "hello" deletedThere’s also the path of deleting directly by name.
kubectl delete pod helloFinally, just confirm the default namespace is empty.
kubectl get podsNo resources found in default namespace.The system Pods of kube-system we saw in Chapter 2 are still up. Those are Pods the cluster holds for its own operation, a different class from the workloads we created. The story of splitting the two with namespaces so they don’t mix is covered in Chapter 7 Namespace and labels.
Exercises #
- After bringing up a Pod imperatively with
kubectl run hello --image=nginx:1.27as in the body, compare thekubectl get pod hello -o yamloutput with thehello-pod.yamlyou wrote. Pick 5 fields K8s filled in (e.g.,metadata.uid,spec.nodeName,spec.containers[].imagePullPolicy, thestatusblock), organize them into a table, and mark each asdesiredoractual. - Deliberately write the image name wrong (e.g.,
nginx:9.99-no-such-tag). Record in time order how the created Pod’skubectl describe podEvents and theSTATUSinkubectl get podschange. Compare at which stageImagePullBackOffandErrImagePullappear with §“Two common failures.” - Delete the Pod of
hello-pod.yamlmade from the manifest withkubectl delete pod helloand confirmkubectl get podsis empty. Then re-runkubectl apply -f hello-pod.yamlwith the same file and confirm the same Pod comes back. Organize in a paragraph, in your own words, the meaning of “a manifest is idempotent” from §“Advantages of a manifest.”
In one line: a Pod is K8s’s smallest execution unit, and the everyday commands are essentially these six — get / describe / logs / exec / apply / delete. Imperative (
kubectl run) is fast but hard to reproduce; declarative (YAML + apply) records cluster state as code. A Pod is mortal — it just disappears when it dies — and that’s why a controller like the next chapter’s Deployment is needed.
Next chapter #
The one thing we saw at the end of this chapter — that deleting a Pod just makes it disappear — becomes the starting point of the next chapter. We need an abstraction by which K8s continuously maintains “N of this Pod must be up” without a person watching it one by one.
In Chapter 4 Deployment and ReplicaSet we cover how a ReplicaSet guarantees the number of Pods, what a Deployment does on top of it (rolling update, rollback), and how, after writing the same nginx Pod as a Deployment manifest and bringing it up with replicas: 3, it auto-heals when you delete one. This chapter’s single Pod starts behaving like a real workload there.