By the end you'll know
- How an image gets from your laptop to a server, through a registry
- How to build an image for a different kind of processor, and fix
exec format error - How to run your own image on a Kubernetes cluster on your laptop
- How each Docker idea in this track has a Kubernetes equivalent
The last step
Everything so far has run on your laptop. Real apps run on servers, often many of them, managed by Kubernetes: a system that runs containers across a group of machines and keeps them running. Kubernetes doesn't build images. It runs the images you give it, so the question is how your image gets there, and what changes when it does.
The answer has two parts. Your image travels through a registry (Lesson 3), and the docker run flags you've learned become entries in Kubernetes files. By the end of this lesson, the app you built in Lessons 4 to 7 will be running on Kubernetes.
docker run). A chain writes the recipe down once, ships ingredients from a central warehouse to every location (that's the registry), and has managers at each restaurant who keep every station staffed, replace a cook who leaves, and open more stations when it's busy (that's Kubernetes). The dish is the same. Who keeps it coming is what changes.Try it: push your image to a registry
This part is optional: it needs a free Docker Hub (opens in a new tab) account. If you'd rather skip it, go straight to running on Kubernetes below, which works without one.
Log in, then set a variable with your Docker ID (your Docker Hub username), so the next commands use it:
docker login
DOCKER_ID=yourname
Replace yourname with your own Docker ID. To push an image, its name must include where it's going (Lesson 3): your Docker ID as the owner. docker tag adds a second name to an image you already have, without copying it:
docker tag hello-app:3.0 "$DOCKER_ID/hello-app:3.0"
docker push "$DOCKER_ID/hello-app:3.0"
Docker uploads each layer. Push again and it skips them all ("Layer already exists"): the same layer sharing you saw in Lesson 3. Your image is now on Docker Hub, and any machine in the world can docker pull it.
Two things to know before you push anything real:
- Images on free Docker Hub accounts are public by default. Anyone can download them and look inside. That's why Lesson 7 kept secrets out of the image: once pushed, assume everyone can see every layer.
- For private images, clusters need permission to pull. In Kubernetes that's a Secret holding your registry login, referred to as an
imagePullSecret. Public images need nothing.
Building for the server's processor
Lesson 3 warned about this: an image you build on an M-series Mac is for arm64 processors, and most cloud servers use amd64. Push it, run it on such a server, and it fails to start with exec format error.
The fix is to tell Docker which processor to build for, with --platform:
docker build --platform linux/amd64 -t hello-app:3.0-amd64 .
On an M-series Mac this takes longer, because your laptop has to imitate an amd64 processor while it builds. Check the result with the command from Lesson 3: docker image inspect hello-app:3.0-amd64 --format '{{.Architecture}}' says amd64. You can also build for both at once, and push them under one tag, as the official images do. That uses buildx, Docker's extended builder, which pushes as it builds:
docker buildx build --platform linux/amd64,linux/arm64 -t "$DOCKER_ID/hello-app:3.0" --push .
docker buildx imagetools inspect "$DOCKER_ID/hello-app:3.0"
(This one needs the Docker Hub login from above, because it pushes as it builds.) The second command, from Lesson 3, now lists both platforms behind your tag. If the build stops with an error saying multi-platform builds aren't supported for the docker driver, create a builder that supports them, then run the build again:
docker buildx create --name multi --use
Try it: run your image on Kubernetes
You'll use kind, which runs a small Kubernetes cluster inside Docker on your laptop, and kubectl, the command-line tool for talking to any Kubernetes cluster. On macOS with Homebrew:
brew install kind kubectl
On other systems, Your first cluster in the Kubernetes track walks through installing both. Then create a cluster:
kind create cluster --name learn
Your hello-app:3.0 image from Lesson 7 is on your laptop, but the cluster can't see your laptop's images: it would try to download hello-app:3.0 from Docker Hub, and fail. kind load copies it straight in, the shortcut in the diagram:
kind load docker-image hello-app:3.0 --name learn
Now Redis. These two commands are the Kubernetes version of the cache service in Lesson 6's compose.yaml: run it, and give it the name cache on the cluster's network:
kubectl create deployment cache --image=redis:7.2.7-alpine
kubectl expose deployment cache --port=6379
And your app. Save this as web.yaml. Read it slowly: almost every line is something you already know from a docker run flag or a compose.yaml entry, and the comments say which.
apiVersion: apps/v1
kind: Deployment # keeps the app running (like compose up, but it never stops checking)
metadata:
name: web
spec:
replicas: 1 # how many copies to run
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
securityContext:
runAsNonRoot: true # refuse to run as root (Lesson 7)
runAsUser: 1000 # the image's USER is a name, so Kubernetes needs its number to check
containers:
- name: web
image: hello-app:3.0 # the image you loaded
ports:
- containerPort: 8000
env: # like -e (Lesson 5)
- name: GREETING
value: Hello
- name: REDIS_HOST
value: cache # Redis's name, exactly as in Compose (Lesson 6)
resources: # like --memory and --cpus (Lesson 5)
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 128Mi
readinessProbe: # Kubernetes' own health checks; it ignores HEALTHCHECK (Lesson 7)
httpGet:
path: /healthz
port: 8000
livenessProbe:
httpGet:
path: /healthz
port: 8000
---
apiVersion: v1
kind: Service # a stable name for the app's pods, like a Compose service name
metadata:
name: web
spec:
selector:
app: web
ports:
- port: 8000
targetPort: 8000
Apply it, and watch the pod start:
kubectl apply -f web.yaml
kubectl get pods
When web shows 1/1 and Running, reach it from your laptop. kubectl port-forward is the Kubernetes cousin of -p: it forwards your laptop's port 8000 to the Service, for as long as it runs:
kubectl port-forward service/web 8000:8000
In a second terminal:
curl http://localhost:8000
Hello from web-...! This page has been visited 1 times. Your image, built with your Dockerfile, running on Kubernetes, talking to Redis by name.
See what Kubernetes adds
So far it looks like Compose with more typing. Here's the difference. Ask for three copies:
kubectl scale deployment web --replicas=3
kubectl get pods
Three web pods, from one command. Now send six requests from inside the cluster, through the Service:
kubectl run tmp --rm -it --image=busybox:1.36 --restart=Never -- sh -c 'for i in 1 2 3 4 5 6; do wget -qO- http://web:8000; done'
The pod names change between requests: the Service is spreading them across all three copies. And the visit count keeps climbing in order, because every copy shares the same Redis. Now delete all three web pods at once, and look again straight away:
kubectl delete pod -l app=web --wait=false
kubectl get pods -l app=web
Three new pods are already starting, with new names. You never told Kubernetes to restart anything: you told it "three copies", and it keeps that true. That's the idea the whole Kubernetes track is built on.
Every Docker idea, in Kubernetes
| In Docker | In Kubernetes | Learn it in |
|---|---|---|
docker run one container | A Pod, usually kept running by a Deployment | Pods, Deployments |
compose.yaml | Kubernetes YAML files, applied with kubectl apply | Reading YAML |
-p 8000:8000 | A Service, plus kubectl port-forward for testing | Services |
| Service names on a Compose network | Service names, through the cluster's DNS | Services |
-e, --env-file | env, ConfigMaps and Secrets | Configuration and Secrets |
| A named volume | A PersistentVolumeClaim | Storage |
--memory, --cpus | resources: requests and limits | Requests and limits |
HEALTHCHECK | Readiness and liveness probes | Health checks |
USER | securityContext, such as runAsNonRoot | This lesson's web.yaml, and the break-it below |
| Publishing a web app to the world | A Gateway and its routes | The Gateway API |
In my real setup
CivicGrid's API takes exactly this journey, with no laptop involved. When I merge a change, GitHub Actions, my CI pipeline, runs the tests and checks first: a failing test never produces an image. Then it builds the image and pushes it to GitHub Container Registry, tagged with the change's ID, like sha-664d196 (Lesson 3).
It builds for amd64 only, with a comment in the file saying why: my cluster's node is amd64, and also building for arm64 doubled the build time. That's the trade-off in this lesson, made on purpose: build for the machines you actually run on.
To ship a version, I change one line in my GitOps repository, the image tag, and Argo CD tells the cluster to pull it. The pipeline also pushes a latest tag, and nothing I run uses it, for the reason in Lesson 3.
Break it on purpose
Ask for an image the cluster doesn't have. Change image: hello-app:3.0 to image: hello-app:3.1 in web.yaml, and apply:
kubectl apply -f web.yaml
kubectl get pods
The new pods show ErrImagePull, then ImagePullBackOff. You never loaded hello-app:3.1, so the cluster tried Docker Hub, which has no such image. Your old pods keep serving the whole time, because the rollout can't finish. kubectl describe pod on a new pod says exactly which image failed. When a pod can't pull its image, check three things: the name and tag, whether the image is in a registry the cluster can reach (or loaded, on kind), and, for private images, its pull permission. Change it back to 3.0 and apply.
Leave out the user's number. In web.yaml, delete the line runAsUser: 1000, keep runAsNonRoot: true, and apply:
kubectl apply -f web.yaml
kubectl get pods
The new pods are stuck at CreateContainerConfigError. Ask why:
kubectl describe pod -l app=web | grep -i "non-root"
The message says the container has runAsNonRoot and the image has a non-numeric user (app), so Kubernetes cannot verify the user is non-root. In Lesson 7 you created the user by name, and a name could secretly be root. Kubernetes only trusts a number, so it refuses to start the container rather than guess. Two fixes: give the number in the YAML with runAsUser: 1000, as you had it, or write USER 1000 instead of USER app in the Dockerfile. Put the line back and apply.
Clean up
Press Ctrl+C in the terminal running the port forward, then:
kind delete cluster --name learn
You're ready for Kubernetes
That's the end of Docker basics. You can package an app into an image, keep it small and safe, configure it, connect it to a database, and get it onto Kubernetes. The Kubernetes basics track picks up from exactly here, and explains everything in web.yaml one piece at a time.
Check yourself
Does Kubernetes build your image from your Dockerfile?
What does docker tag hello-app:3.0 yourname/hello-app:3.0 do?
You build on an M-series Mac, and the image fails on an amd64 server with exec format error. How do you fix it?
Your Deployment uses an image that's only on your laptop, and the pod shows ErrImagePull on kind. Why?
Which Kubernetes object plays the role of -p 8000:8000 and of a service name in Compose?
You delete all three of a Deployment's pods. What happens?
Key terms
- Kubernetes: a system that runs containers across many machines and keeps them running.
- kind: "Kubernetes in Docker", a small Kubernetes cluster that runs on your laptop.
- kubectl: the command-line tool for working with a Kubernetes cluster.
- docker push / docker tag: upload an image to a registry / give an image another name.
- --platform: which operating system and processor to build an image for.
- buildx: Docker's extended builder, which can build for several platforms at once.
- kind load: copies an image from your laptop into a kind cluster.
- imagePullSecret: a Kubernetes Secret with registry login details, for pulling private images.
Go deeper
- Multi-platform builds (opens in a new tab) in the official Docker docs.
- kind's quick start (opens in a new tab), including loading images.
- The Kubernetes basics track on this site, which starts where this one ends.