Time: 12:00 - 15:00 | Work at your own pace. Use AI tools (Claude, Claude Code) to help you learn!
You'll deploy a todo web app step by step — starting with the simplest possible thing, breaking it on purpose, and building up to a properly exposed, self-healing deployment.
The image is already built and ready: ghcr.io/alfredtm/k8s-todo-app:latest
On Windows (PowerShell):
winget install -e --id RedHat.OpenShift-Client
winget install -e --id Derailed.k9sRestart your terminal after installing so the commands are available.
If winget isn't available, you can download oc.exe directly from the cluster console: open the console URL in your browser, click the ? icon in the top-right corner, and select Command Line Tools.
On Mac:
brew install openshift-cli k9sVerify both are installed:
oc version
k9s version
Log in to the cluster with the command your instructor gave you:
oc login --token=<your-token> --server=<cluster-url>
Create your own namespace so you're not stepping on each other:
oc new-project <yourname>-todo
Project vs Namespace: In plain Kubernetes, isolated environments are called namespaces. OpenShift wraps this concept in a project, which is just a namespace with some extra metadata. oc new-project creates both at the same time — they refer to the same thing. Every kubectl/oc command accepts a -n <namespace> flag to target a specific namespace. We'll use it consistently so it's always clear where things are running.
Replace <yourname>-todo with your actual namespace name in every command below.
The simplest thing you can do in Kubernetes is run a single pod. No YAML needed:
oc run todo --image=ghcr.io/alfredtm/k8s-todo-app:latest --port=8080 -n <yourname>-todo
You'll see a security warning about missing securityContext settings — that's expected. The pod still runs. The Deployment you create in Task 4 fixes this properly by setting those fields explicitly.
Check that it started:
oc get pods -n <yourname>-todo
oc get pod todo -n <yourname>-todo
Wait until the STATUS column shows Running. You can also describe the pod to see more detail:
oc describe pod todo -n <yourname>-todo
Look at the Events section at the bottom — this is where Kubernetes logs what it did to start your pod (pulled the image, assigned it to a node, started the container).
The pod is running, but it's not reachable from the outside yet. Port-forward creates a temporary tunnel from your laptop directly to the pod:
oc port-forward pod/todo 8080:8080 -n <yourname>-todo
Open http://localhost:8080 in your browser. You have a running todo app. Add a few todos.
Press Ctrl+C to stop the port-forward when you're done, then move on.
oc delete pod todo -n <yourname>-todo
Watch what happens:
oc get pods -n <yourname>-todo
The pod is gone. There's nothing left. If you port-forwarded again, there would be nothing to connect to.
Your todos are also gone — they lived in that pod's memory.
This is the problem with bare pods. Kubernetes doesn't restart them when they die. If the node crashes, if you delete it by accident, if the container crashes — it's just gone. You need something smarter.
A Deployment tells Kubernetes "I want X copies of this pod running at all times." If a pod dies, Kubernetes starts a new one automatically.
Create a file called deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: todo-app
spec:
replicas: 2
selector:
matchLabels:
app: todo-app
template:
metadata:
labels:
app: todo-app
spec:
securityContext:
runAsNonRoot: true
containers:
- name: todo-app
image: ghcr.io/alfredtm/k8s-todo-app:latest
ports:
- containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]Apply it:
oc apply -f deployment.yaml -n <yourname>-todo
oc get pods -n <yourname>-todo -w
You'll see two pods start up. Now delete one of them:
oc delete pod <one-of-the-pod-names> -n <yourname>-todo
Watch what happens — Kubernetes immediately starts a replacement. The Deployment controller is constantly reconciling: making sure the actual state (running pods) matches the desired state (2 replicas).
Port-forward to verify the app still works:
oc port-forward deployment/todo-app 8080:8080 -n <yourname>-todo
Questions to think about:
- What happens if you set
replicas: 0? Try it:oc scale deployment todo-app --replicas=0 -n <yourname>-todo - What happens when you scale back up to 2?
Port-forwarding is only for local development. To make your app reachable on a real URL, you need two things:
- A Service — a stable internal address that load-balances across your pods
- An HTTPRoute — routes external traffic from the gateway into your Service
Create a file called expose.yaml:
apiVersion: v1
kind: Service
metadata:
name: todo-app
spec:
selector:
app: todo-app
ports:
- port: 80
targetPort: 8080
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: todo-app
spec:
parentRefs:
- name: aa
namespace: envoy-gateway-system
hostnames:
- "todo-<yourname>.devapp-jmxg8t.intility.dev"
rules:
- backendRefs:
- name: todo-app
port: 80Replace <yourname> with your actual name, then apply:
oc apply -f expose.yaml -n <yourname>-todo
Check that the HTTPRoute was accepted:
oc get httproute todo-app -n <yourname>-todo
The HOSTNAMES column should show your URL. Open it in your browser.
What you just built:
Internet → Gateway (aa) → HTTPRoute → Service → Pods
The Service selects pods using the app: todo-app label. Scale your deployment up and the Service automatically includes the new pods. Delete a pod and the Service stops sending traffic to it while Kubernetes restarts it.
The app stores todos in memory. They disappear when a pod restarts. To make them persist, deploy a real database.
The cluster has the CloudNativePG operator installed. Create database.yaml:
apiVersion: v1
kind: Secret
metadata:
name: postgres-credentials
type: kubernetes.io/basic-auth
stringData:
username: todos
password: todos
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgres
spec:
instances: 1
storage:
size: 1Gi
bootstrap:
initdb:
database: todos
owner: todos
secret:
name: postgres-credentialsApply and wait for postgres-1 to be Running:
oc apply -f database.yaml -n <yourname>-todo
oc get pods -n <yourname>-todo -w
Now update deployment.yaml to add the database URL. Add an env block inside the todo-app container, alongside the existing ports:
ports:
- containerPort: 8080
env:
- name: DATABASE_URL
value: "postgres://todos:todos@postgres-rw:5432/todos?sslmode=disable"Apply and restart:
oc apply -f deployment.yaml -n <yourname>-todo
oc rollout restart deployment/todo-app -n <yourname>-todo
The app header now shows "PostgreSQL (persistent)". Add some todos, delete a pod, scale to 0 and back — your todos survive everything.
You've deployed an app on Kubernetes. Now see how companies run it at a scale that's hard to imagine.
Go to kubernetes.io/case-studies and pick a company that interests you. Read their write-up and find answers to these questions:
- What problem were they trying to solve before Kubernetes?
- What scale are they running at — how many containers, pods, or deployments?
- What was the biggest benefit they got out of it?
- What surprised you?
Pod not starting?
oc describe pod <pod-name> -n <yourname>-todo # look at the Events section
oc logs <pod-name> -n <yourname>-todo
HTTPRoute not working?
oc get httproute todo-app -n <yourname>-todo # check HOSTNAMES and status
oc get svc todo-app -n <yourname>-todo # check the Service exists
oc get pods -n <yourname>-todo # check pods are Running
Need to start over?
oc delete project <yourname>-todo && oc new-project <yourname>-todo
This fully wipes and recreates your namespace — including things oc delete all misses, like the CNPG Cluster and HPA.
Need help?
- Ask your neighbor
- Ask Claude or Claude Code
oc explain <resource>— built-in docs, e.g.oc explain deployment.spec