Lesson 02 · Serving

Deploy your first site

Put a real website on the cluster and reach it from a browser — tracing the whole request path: Pod → Deployment → Service → Ingress. This is the skeleton every site you ever host will use.

This is the mission's core win. "Deploy & serve my sites" is these four objects. Learn the chain once and you can host anything; later lessons just swap the content and add TLS and more sites.

The mental model: the request path

One HTTP request from your browser travels right to left through four objects. Read it as a sentence: an Ingress routes a hostname to a Service, which load-balances to Pods, which a Deployment keeps alive.

🌐 browser → Host: hello.local → Ingress Service Pod Pod
Deployment — controller that keeps N Pods running

Pod — the unit that runs

The smallest deployable thing: one (or a few) containers sharing an IP and storage. Pods are cattle, not pets — disposable, and their IP changes when they're recreated.

What breaks without the next layer Because Pod IPs are unstable, nothing should ever talk to a Pod by IP. That single fact is why Services exist.

Deployment — the controller that keeps them running

You don't create Pods directly. You declare a Deployment: "I want 2 replicas of this Pod template." Its controller continuously reconciles reality to that desire — kill a Pod and it makes a new one; change the image and it rolls the change out gradually.

Service — the stable front door

A Service gives a stable virtual IP + in-cluster DNS name. It finds its Pods by label selector (not by IP) and load-balances across them. Default type ClusterIP = reachable only inside the cluster.

Common misconception, corrected You do not need NodePort or LoadBalancer to serve the web. Keep the Service internal (ClusterIP) and let the Ingress be the single public door. Cleaner, and how real hosting works.

Ingress — the HTTP router at the edge

An Ingress is an L7 rule: "requests for host hello.local go to Service hello-site." It's executed by an ingress controller (the microk8s enable ingress addon — Traefik on 1.35+, exposed via the public IngressClass). One controller can route many hostnames → one site each.

Do it — on the machine that talks to your cluster

1

Turn on the addons you need

$ microk8s enable dns ingress
$ kubectl get ingressclass        # confirm a class exists; we'll use "public"
Why dns dns (CoreDNS) is what lets a Service be found by name inside the cluster. Without it, the name-based wiring above silently fails.
2

Write the whole site as one manifest

Save as hello-site.yaml. Three objects, one file, separated by ---. Read it top-to-bottom and predict what each does:

apiVersion: apps/v1
kind: Deployment
metadata: { name: hello-site }
spec:
  replicas: 2
  selector: { matchLabels: { app: hello-site } }
  template:
    metadata: { labels: { app: hello-site } }   # Pods get this label…
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports: [ { containerPort: 80 } ]
---
apiVersion: v1
kind: Service
metadata: { name: hello-site }
spec:
  selector: { app: hello-site }                 # …Service finds Pods by it
  ports: [ { port: 80, targetPort: 80 } ]
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: { name: hello-site }
spec:
  ingressClassName: public
  rules:
    - host: hello.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service: { name: hello-site, port: { number: 80 } }
The thread that ties it together The label app: hello-site appears three times: the Pod template sets it, the Deployment selector watches it, the Service selector targets it. Break that string in one place and traffic silently goes nowhere. Labels are the glue.
3

Apply it and watch reconciliation happen

$ kubectl apply -f hello-site.yaml
$ kubectl get deploy,pods,svc,ingress     # the whole chain at once
$ kubectl rollout status deploy/hello-site

Watch the Deployment bring up 2/2 Pods. You declared desire; the controller made it real — no create-pod step anywhere.

4

Serve it to your browser

The ingress controller listens on the node's port 80. Point the hostname at the VM's IP, then visit it.

$ multipass list                          # grab the VM IP, e.g. 192.168.64.5
# add to /etc/hosts on the machine with the browser:
$ echo "192.168.64.5  hello.local" | sudo tee -a /etc/hosts
$ curl http://hello.local/                 # or open it in a browser

The nginx welcome page = a website you hosted on Kubernetes. 🎉

Debug like you mean it

Your mission is to fix things with kubectl, not guess. When a site is down, walk the chain with these four verbs:

$ kubectl get pods                         # Running? CrashLoop? Pending?
$ kubectl describe pod <name>               # events at the bottom tell the story
$ kubectl logs <pod>                        # what the app itself printed
$ kubectl get events --sort-by=.lastTimestamp   # cluster-wide recent history
Triage heuristic Pod not Runningdescribe + logs (app/image problem). Pod Running but site unreachable → check the label/selector thread and the Ingress host (wiring problem). Decide which half you're in first.

Why must traffic reach Pods through a Service, never by Pod IP?

Pods are disposable; their IPs are unstable. The Service gives a stable name/IP and finds current Pods by label selector.

To serve a website publicly, which object is the public door?

Keep the Service ClusterIP (internal); the Ingress is the single public entry point that maps a hostname to that Service.

From Lesson 01: what makes your kubectl reach this cluster at all?

Interleaving check: none of today's objects matter if kubectl can't reach the API server — that's the kubeconfig from Lesson 01.

Primary source — read this next

Kubernetes — Ingress (official concept page). The authoritative model for the routing layer. Pair it with the microk8s ingress addon doc for the public/traefik class specifics.

Ask your teacher
Good questions now: "What's the difference between port and targetPort?", "What actually happens during a rolling update?", "How do I serve my own HTML instead of the nginx page?" (that's Lesson 03).