Seeking Wisdom · AI/ML Engineering · Investing · Books · Projects · About ·

Kubernetes by Example · Part 2

Pods and Containers

It ran perfectly in Docker. You put it in a Pod and it died. Here is what a Pod actually is, and why that difference matters more than it looks.

assumes Part 1~18 minutesthe real tuner arrives

The symptom

You've written the actual tuner now. It works — you ran it locally with docker run and watched it print throughput numbers, happy as anything.

So you put it in the cluster. And:

NAME                    READY   STATUS             RESTARTS   AGE
tuner-6b8f4d9c7-mn4kp   0/1     CrashLoopBackOff   4          2m

Four restarts. You reach for the logs, obviously:

$ kubectl logs deploy/tuner
$

Nothing. Not an error — nothing. An empty response and your prompt back.

At this point most people conclude that Kubernetes has eaten their logs. It hasn't. The logs are there, they're just attached to something that no longer exists, and finding them takes one extra flag that nobody tells you about on your first day.

To understand why, you need to know what you actually put your container inside.

The thing your container lives in

In Part 1 we treated the Pod as a box with a container in it, which was fine because we only needed to watch it get created. Now we need to open the box.

A Pod is best thought of as a very small machine. It has one network address. It has a lifecycle — it starts, it runs, it ends, and it does not come back. It holds one or more containers that have agreed to be co-located, sharing some things and pointedly not sharing others.

Which things get shared, and which don't, explains most of the surprising behaviour you'll meet this week.

one Pod — the smallest thing Kubernetes will place tuner-7d4f8b9c6-x2kmp init: resolve-station runs first runs alone must finish, and exit 0 own filesystem tuner the long-running one if it exits, it is restarted own filesystem a sidecar, later starts at the same time same IP, same localhost own filesystem a volume — the only way they share files mounted into whichever containers ask for it one network identity for the whole Pod IP 10.244.1.17one localhost · one set of ports · shared by every container above nodechosen once, never changed — a Pod does not move house finishes completely, then… these two start together both bind :8080 → one of them dies init writes it · tuner reads it restarts here. Pod stays put. gone: new name, new IP, nothing carried over

Shared, and not shared

Shared automatically: the network — one IP, one localhost, one set of ports. And the lifecycle: they are placed together and they are deleted together.

Shared only if you ask: files, via a volume mounted into more than one container. Process visibility, via shareProcessNamespace.

Never shared: the root filesystem. Each container gets its own, from its own image.

The commands for looking inside

Part 1 was about get and apply — commands for the list. These three are for the world: what a container said, what it's doing now, and what the cluster looks like from the inside.

Getting the logs, including the ones that look missing

Click any piece of the command.

Getting inside a running container

Click any piece of the command.

A throwaway Pod, for testing from inside the cluster

Click any piece of the command.

Which one to reach for

A rough decision tree, since the hard part is usually knowing where to start rather than how to type it.

The Pod is…Start withBecause
Pendingkubectl describe podIt hasn't been placed. There are no logs yet because there is no container yet. The reason is in the events at the bottom.
ImagePullBackOffkubectl describe podWrong name, wrong tag, or no credentials. Again, events — the message is usually explicit.
Init:0/1kubectl logs -c <init>Stuck in an init container. Name it explicitly or you'll be told the main one hasn't started.
CrashLoopBackOffkubectl logs --previousIt started and then died. The evidence belongs to the dead instance.
Running but wrongkubectl exec -it … -- shGo in and look. Check env, check mounts, check what it can reach.
Running, no shellkubectl debugAttaches a container with tools, sharing the target's namespaces.

Build it

Time to retire the placeholder. The real tuner reads a stream and reports how much audio it's getting — and it now has an init container that works out which stream, writing the answer to a volume the tuner reads back.

It runs entirely offline. By default the init container synthesises a local stream, so nothing depends on the internet or on somebody else's radio station staying up.

Apply it

kubectl apply -f manifests/tuner.yaml
kubectl get pods -l app=tuner -w

Watch the STATUS column rather than waiting for it to settle. You should catch Init:0/1 go past — the init container doing its work — before Running appears. That transition is Part 2's whole thesis on screen for about half a second.

Read both containers

# the init container, which has already finished
kubectl logs deploy/tuner -c resolve-station

# the one still going
kubectl logs -f deploy/tuner -c tuner --tail=20

Go in and look at what they share

kubectl exec -it deploy/tuner -c tuner -- sh

# inside:
cat /work/station.txt     # written by a container that no longer exists
ip addr | grep inet       # the Pod's IP, not the container's
env | grep STATION        # configuration, from the manifest
exit

That cat is the point of the exercise. You are reading a file written by a different container, which has already exited, and it's there because the Pod declared a volume and both containers mounted it. Remove the volume and the file vanishes — not because it was deleted, but because the two containers were never looking at the same place.

Break it

1 · Reproduce the empty logs

Point the tuner at a station that doesn't exist and watch the symptom from the top of the page appear on demand.

kubectl set env deploy/tuner STATION_SOURCE=http://nowhere.invalid/stream

kubectl get pods -l app=tuner -w     # wait for CrashLoopBackOff
kubectl logs deploy/tuner -c tuner              # empty, or nearly
kubectl logs deploy/tuner -c tuner --previous   # there it is

The first logs catches a container that has just been started and hasn't failed yet. The second reads the one that already died. Same Pod, same command, one flag apart — and the difference between twenty minutes of confusion and a ten-second fix.

While it's broken, look at how the cluster describes it:

kubectl describe pod -l app=tuner | grep -A6 'Last State'

You get Terminated, a Reason, and an Exit Code. Exit code 1 means your program decided to quit. 137 means it was killed — almost always out of memory, which is Part 10. 143 means it was asked to stop politely, which is Part 12. Those three numbers cover most of what you'll meet.

Put it back: kubectl set env deploy/tuner STATION_SOURCE=synthetic

2 · Make two containers fight over a port

This one is quick and the error message is wonderfully blunt.

kubectl apply -f manifests/port-clash.yaml
kubectl get pods -l demo=port-clash
kubectl logs -l demo=port-clash -c second

Two containers, both trying to listen on 8080. The first wins, the second reports address already in use and dies. If you still half-believed containers in a Pod are isolated from each other, this is the demonstration that settles it.

Clean up: kubectl delete -f manifests/port-clash.yaml

What follows from this

Because……this is what happens
A Pod has one IPContainers inside reach each other on localhost with no config — and cannot both use the same port.
Containers don't share filesAnything passed between them needs a volume, declared on the Pod and mounted in both.
Init containers must succeedA failing init container means the Pod never starts, which is the correct and much cheaper failure.
Restarts happen in placeThe logs you need usually belong to the previous instance. --previous, every time.
Pods never moveNode problems are Pod problems. Nothing migrates; something new gets created elsewhere.
Pods are disposableNever persist a Pod's name or IP. Both are temporary. Use labels and Services instead.

One thing to be careful about

Because containers in a Pod share so much, it's tempting to solve every problem by adding another container to the Pod. Resist it for now.

Two containers belong in the same Pod when they genuinely cannot work apart — they need the same filesystem, or the same network identity, or must be scheduled as one unit. If they'd work fine as separate Pods talking over the network, make them separate Pods. You get independent scaling, independent restarts and a smaller blast radius, all for free.

Cheat sheet

Part 2 · pods and containers

• The Pod is the unit of scheduling, not the container. One IP, one lifecycle, one node, forever.

• Shared free: network and lifecycle. Shared on request: files, via a volume. Never: the root filesystem.

• Init containers run first, one at a time, and must exit 0. Ordinary containers then start all together — there is no ordering between them.

• Two containers in one Pod cannot bind the same port. Same network namespace.

• A crash restarts the container in place, with growing backoff. That is what CrashLoopBackOff means.

• kubectl logs --previous is how you read the instance that died. Learn it before you need it.

• -c picks the container, init containers included.

• Exit 1 means it quit, 137 means it was killed (usually memory), 143 means it was asked to stop.

• kubectl run probe --rm -it --image=busybox --restart=Never -- sh for a look from inside the cluster.

• Pod names and IPs are temporary facts. Never write them down.

Previous · Part 1The object model SeriesAll 14 parts Next · Part 3 · soonLabels, selectors, namespaces