Containers in a nutshell
The thing Kubernetes actually runs.
Kubernetes runs containers. So the first thing you need is a clear picture of what one of those actually is. Start with the object the technology borrowed its name from. A shipping container is a big steel box. You pack goods inside, seal the doors, and every crane, ship and truck moves that box around without knowing or caring what is in it. A software container copies that trick. It is a sealed box holding your app plus everything the app needs in order to run.
What is inside the box
An app on its own is rarely enough to run. It wants a particular version of a programming language, a handful of code libraries, and a few configuration settings. Miss one of those on a new machine and the app falls over. A container packs the lot together: your code, the runtime (the engine that actually executes your code), the libraries, and the settings. Those supporting pieces have a name. They are the app's dependencies, the other software it leans on. All of it gets sealed into one unit. Because the container carries its own dependencies with it, it behaves the same on your laptop, on a teammate's machine, and on a server in the cloud. That is the end of "but it works on my machine".
You may be wondering how that differs from a virtual machine, which also bundles software into one package. A virtual machine is a separate house. It brings a whole copy of an operating system along with it, foundations and plumbing and all. A container is an apartment in a building. Its own locked front door, its own rooms, but the foundation underneath is shared with every other apartment in the block. That shared foundation is the host machine's operating system kernel (the core part of the operating system that talks to the hardware). Sharing it keeps containers small and quick. They start in seconds, and one machine can run many of them at once.
Image versus container
Two words get swapped by mistake constantly, so pin them down now. An image is the template. You build it once and it freezes: the app and all its dependencies packed together, read-only. A container is a running copy of that image. Recipe and cake. One recipe, baked as often as you like, gives you a kitchen full of cakes. One image, started as often as you like, gives you a cluster full of containers. The commands below use Docker, a popular tool for building and running containers on a single computer. Start by looking at an image sitting on disk.
$ docker pull nginx:1.271.27: Pulling from library/nginxa2318d6c47ec: Pull complete5b5f5c8f3d0a: Pull complete8f1e9c2d4b7a: Pull completeDigest: sha256:9c3f1e0d7a2b4c6e8f0a1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6Status: Downloaded newer image for nginx:1.27docker.io/library/nginx:1.27$ docker images nginxREPOSITORY TAG IMAGE ID CREATED SIZEnginx 1.27 e0e5f7a1c9d2 4 weeks ago 192MB
That image is the frozen template. Nothing is running yet. Now start a container from it. The command below runs it in the background and hands the container a name, so it is easy to find again.
$ docker run -d --name web nginx:1.279f3c2a1b7e4d5c6f8a0b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a$ docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES9f3c2a1b7e4d nginx:1.27 "/docker-entrypoint.…" 8 seconds ago Up 6 seconds 80/tcp web
One image, one running container. Start ten more from the same image and all ten come out identical. That is container work done by hand: one tool, one machine, you typing. Kubernetes does the same job across a whole fleet of machines (each machine in the fleet is called a node), and it does the typing for you. You do not run that command a thousand times. You hand Kubernetes a short text file describing what you want, and it fetches the images and starts the containers wherever there is room.
How Kubernetes runs a container
One detail before the file. Kubernetes will not run a bare container for you. It wraps your container in a Pod, the smallest thing Kubernetes knows how to run. For now, read "Pod" as "a thin wrapper around one container"; a later lesson opens it up properly. You describe the Pod in a YAML file. YAML (Yet Another Markup Language) is a plain-text format for writing settings as name-and-value pairs, where indentation shows what belongs to what. Here is a complete one you can save and use.
apiVersion: v1kind: Podmetadata:name: webspec:containers:- name: webimage: nginx:1.27ports:- containerPort: 80
Four things to pick out. The apiVersion and kind lines tell Kubernetes what sort of object this is, here a Pod. The metadata block hands it a name, web, so you can refer to it later. The spec block is the wish itself: run one container called web, built from the nginx:1.27 image, and expect it to listen on port 80. That is the entire file. Send it to the cluster (the group of machines Kubernetes manages on your behalf) using kubectl, the command-line tool that talks to a cluster, then check whether it came up.
$ kubectl apply -f pod.yamlpod/web created$ kubectl get podsNAME READY STATUS RESTARTS AGEweb 1/1 Running 0 9s
READY 1/1 means the single container in the Pod is up and healthy. STATUS Running means Kubernetes found the image, pulled it down onto a machine, and started it. You wrote down what you wanted, and the cluster went and made it true. Everything bigger in this course, Deployments, Services, scaling, is built on that one move: describe a container, let Kubernetes run it.
When the Pod refuses to start
That was the happy path. Real clusters make you read failures too, and the most common first stumble is a mistyped image name. Type the tag as nginx:1.72 when you meant nginx:1.27, and the Pod never comes up.
$ kubectl apply -f pod.yamlpod/web created$ kubectl get podsNAME READY STATUS RESTARTS AGEweb 0/1 ImagePullBackOff 0 30s
READY 0/1 means the container never started, and STATUS tells you why. Kubernetes tried to pull the image, could not, and is now waiting longer and longer between retries. For the detail, ask the Pod to describe itself. The Events section at the bottom is where you look whenever something is stuck.
$ kubectl describe pod webName: webNamespace: defaultStatus: Pending...Events:Type Reason Age From Message---- ------ ---- ---- -------Normal Scheduled 46s default-scheduler Successfully assigned default/web to worker-1Normal Pulling 17s (x3 over 45s) kubelet Pulling image "nginx:1.72"Warning Failed 16s (x3 over 44s) kubelet Failed to pull image "nginx:1.72": not foundWarning Failed 16s (x3 over 44s) kubelet Error: ErrImagePullNormal BackOff 3s (x4 over 43s) kubelet Back-off pulling image "nginx:1.72"
Read it from the top. The scheduler placed the Pod on worker-1 without trouble. Then the kubelet (the agent running on every node, the thing that pulls images and starts containers) went looking for nginx:1.72, failed, and kept failing. No such tag exists. Correct it to nginx:1.27 and apply the file again. kubectl describe is your first move any time a Pod will not start.
Smaller images start faster
Here is a problem that hides from you, because on your laptop it never shows up. You build an image carrying a full operating system plus a compiler and build tools, and it comes out at 1.2 GB (gigabytes). Your laptop already has that image downloaded, so the container starts instantly. In a cluster, a node that has never seen the image has to fetch all 1.2 GB before anything runs, which can take minutes. It lands at the worst possible moment: scaling up under a traffic spike, or when a node dies and gets replaced.
$ docker images myappREPOSITORY TAG IMAGE ID CREATED SIZEmyapp full a1b2c3d4e5f6 2 hours ago 1.24GBmyapp slim f6e5d4c3b2a1 2 hours ago 82MB
The fix is a slimmer starting point. Many projects publish -slim or -alpine variants of their base image, and distroless images (images that ship your app and its runtime and nothing else, no package manager, no shell) go further still. Smaller image, faster pull, quicker scaling. The honest trade-off: those images drop the shell and the usual debugging tools, so when a container misbehaves you have far less to poke at from the inside. While you are learning, a full image is fine. In production, small wins.
A container image is a frozen filesystem plus one default process, and that is the whole of it. Kubernetes did not invent containers and does not change what they are. It schedules them, restarts them, and wires them together. So the image habits you already have carry over untouched: pin digests, run as a non-root user, keep the thing small. The cluster has no taste. Hand it a dangerous image and it will run that image happily, on every node you own.
One consequence of that catches people out. When a Pod dies and Kubernetes starts a replacement, you get a brand new container built from the same image, not your old process brought back to life. Whatever the previous container wrote into /tmp went with it. That is why "it worked until the Pod restarted" nearly always turns out to be state living on the container filesystem, or a volume nobody configured.
Try this
Create a Pod, then read back the image and container status Kubernetes recorded for it. Then do the same with an image that cannot be pulled, and compare what you get.
$ kubectl run web --image=nginx:1.27 --restart=Neverpod/web created$ kubectl get pod web -o jsonpath='{.status.containerStatuses[0].imageID}{"\n"}{.status.phase}{"\n"}'docker.io/library/nginx@sha256:abcdef0123456789...Running$ kubectl delete pod webpod "web" deleted$ kubectl run bad --image=nginx:does-not-exist-xyz --restart=Neverpod/bad created$ kubectl get pod badNAME READY STATUS RESTARTS AGEbad 0/1 ImagePullBackOff 0 12s$ kubectl delete pod badpod "bad" deleted
Takeaway
Containers are running instances of images, and Pods are how Kubernetes places and restarts those instances. Pin your image versions, check the digest the cluster actually pulled, expect an empty filesystem every time a Pod comes back, and read ImagePullBackOff as a wrong name or a missing registry entry rather than a mysterious cluster fault.