Skip to content

Module 5 — Containers with Podman, and a Taste of Kubernetes

Goal: understand images vs containers, build and run them with Podman, wire multi-container apps together, and get an honest conceptual (plus small practical) introduction to Kubernetes.

Podman is used throughout: daemonless, rootless-friendly, Docker-CLI-compatible (alias docker=podman genuinely works), and it's what runs your CI runner in module 6.

5.1 What a container actually is

Not a VM. A container is a normal Linux process wearing blinkers:

  • namespaces — its own view of PIDs, network, filesystem mounts, hostname;
  • cgroups — CPU/memory limits;
  • an image — a layered, read-only filesystem snapshot it starts from.
VM Container
Boots full OS kernel just a process
Startup minutes milliseconds
Size GBs MBs
Isolation hardware-level kernel-level
Analogy separate exchange building line cards in one chassis

Why the industry moved: "works on my machine" dies — the image ships the app and its entire runtime environment, identical from laptop to production.

Lab 5.1 — First containers

podman --version
podman run hello-world? true            # (registry note below)
podman run docker.io/library/alpine echo "hello from a container"
podman run -it docker.io/library/alpine sh    # inside: ps aux; ls /; exit
podman ps -a                                   # dead containers remain
podman run -d --name web -p 8080:80 docker.io/library/nginx
curl localhost:8080
podman logs web
podman exec -it web sh                         # shell inside a RUNNING container
podman stop web && podman rm web
podman images

Prove the "just a process" claim: while nginx runs, on the host: ps aux | grep nginx — there it is, in the host's process table.

5.2 Building images

~/labs/module5/app/server.py — a tiny Python web app:

from http.server import HTTPServer, BaseHTTPRequestHandler
import os, socket

class H(BaseHTTPRequestHandler):
    def do_GET(self):
        body = f"Hello from {socket.gethostname()} v{os.getenv('APP_VERSION','1')}\n".encode()
        self.send_response(200)
        self.send_header("Content-Type", "text/plain"); self.end_headers()
        self.wfile.write(body)

HTTPServer(("", 8000), H).serve_forever()

Containerfile (same syntax as a Dockerfile):

FROM docker.io/library/python:3.12-alpine
WORKDIR /app
COPY server.py .
ENV APP_VERSION=1
EXPOSE 8000
CMD ["python", "server.py"]
podman build -t fieldapp:1 .
podman run -d --name fieldapp -p 8000:8000 fieldapp:1
curl localhost:8000
podman image tree fieldapp:1        # see the layers

Layer caching lesson: change APP_VERSION to 2, rebuild, and watch which layers rebuild vs cache. Order your Containerfile from least- to most-frequently-changed.

Tag and understand registries:

podman tag fieldapp:1 ghcr.io/<user>/fieldapp:1     # push comes in module 6

5.3 Networks, volumes, compose

Containers are ephemeral — anything not in a volume dies with the container. Container networks give containers DNS names for each other (service discovery in one line).

Lab 5.3 — Two containers, wired properly

podman network create appnet
podman volume create pgdata
podman run -d --name db --network appnet -v pgdata:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=labpass docker.io/library/postgres:16-alpine
podman run -it --rm --network appnet docker.io/library/postgres:16-alpine \
  psql -h db -U postgres    # note: reaches it by NAME "db"

Kill and recreate the db container — the data survives (the volume). This is tier-2/tier-3 separation from module 2, in miniature.

Then the declarative version with podman-composecompose.yaml:

services:
  app:
    build: .
    ports: ["8000:8000"]
    environment: { APP_VERSION: "3" }
    depends_on: [db]
  db:
    image: docker.io/library/postgres:16-alpine
    environment: { POSTGRES_PASSWORD: labpass }
    volumes: [pgdata:/var/lib/postgresql/data]
volumes:
  pgdata:
podman-compose up -d && podman-compose ps && podman-compose down

Spot the pattern: compose is to podman run what Terraform is to the console. Declarative wins again.

5.4 Kubernetes — the honest introduction

The problem k8s solves: you have many containers on many machines and need scheduling, self-healing, service discovery, rolling deploys and scaling — a container exchange management system.

Core objects (concepts only, one diagram):

  • Pod — smallest unit; one or more containers sharing network/storage.
  • Deployment — "I want 3 replicas of this pod, always" + rolling updates.
  • Service — stable virtual IP/DNS in front of ever-changing pods.
  • Ingress — layer-7 routing from outside (the ALB analogue).
  • ConfigMap/Secret — config injected into pods.
  • Control plane (desired state reconciler) vs worker nodes (kubelet). The reconciliation loop is Terraform's plan/apply running continuously.

Lab 5.4 — Tiny but real: k3s

On lab-box (k3s = lightweight single-node k8s, fine on a t3.medium):

curl -sfL https://get.k3s.io | sh -
sudo k3s kubectl get nodes
sudo k3s kubectl create deployment web --image=nginx --replicas=3
sudo k3s kubectl get pods -o wide
sudo k3s kubectl delete pod <one-of-them>     # watch it resurrect
sudo k3s kubectl expose deployment web --port=80 --type=NodePort
sudo k3s kubectl get svc web                  # note the NodePort
curl localhost:<nodeport>
sudo k3s kubectl scale deployment web --replicas=5
sudo k3s kubectl get pods -w                  # watch scaling live; Ctrl-C

The self-healing demo (delete a pod, it comes back) is the "aha" moment — nothing in modules 1–5 did that.

Honest framing for the student: most small/medium workloads don't need k8s; managed services (ECS, Cloud Run, plain EC2+ASG) are often the right answer. Know the concepts, recognise when the complexity pays for itself (many services, many teams, high deploy frequency).

Clean up when done (k3s is memory-hungry): sudo /usr/local/bin/k3s-uninstall.sh.

Quiz

1. Container vs VM in one sentence each. VM: virtual hardware running a full OS with its own kernel. Container: an isolated process sharing the host kernel, packaged with its filesystem.
2. You restart a container and the database is empty. What did you forget? A volume — container filesystems are ephemeral; state must live in volumes (or outside the container entirely).
3. What does a k8s Deployment give you that podman run doesn't? A supervisor holding desired state: replica count, self-healing on failure, and rolling updates.
4. Why does layer order in a Containerfile matter? Build cache: layers rebuild from the first changed layer down, so put stable steps (dependencies) early and volatile ones (your code) late for fast builds.