View on GitHub

Containers & Kubernetes Tutorial

Session 01 demos – Containers & Docker: the mental model

Four short demos woven into Session 01. Demos 1–2 belong to Block A (after the “what is a container” slides), Demo 3 opens Block B, Demo 4 closes it and hands over to Lab 01.

Before the session

~5 minutes, with network:

docker pull hello-world
docker pull alpine:3.22
docker pull nginx:1.28-alpine
docker pull postgres:17-alpine

Demo 1: your first containers and the lifecycle (≈ 7 min)

What to show / say

A container is a process that starts, runs, exits and is then still there until you remove it — run is not start, and stop is not rm.

Steps

  1. The smallest possible container. It prints and exits — and it is still there.

    docker run hello-world
    docker ps
    docker ps -a
    
    CONTAINER ID   IMAGE         COMMAND    CREATED         STATUS                     NAMES
    3f9c1c2a77b1   hello-world   "/hello"   4 seconds ago   Exited (0) 3 seconds ago   nifty_mendel
    

    👉 point at docker ps being empty while docker ps -a shows the container in state Exited (0). “The process ended. The container object did not.”

  2. An interactive container — this is the “aha” moment of the session.

    docker run -it --name box alpine:3.22 sh
    

    Inside, in the left pane:

    hostname
    ps aux
    ls /
    cat /etc/os-release | head -2
    exit
    
    / # hostname
    7a1f0c9e42b8
    / # ps aux
    PID   USER     TIME  COMMAND
        1 root      0:00 sh
        7 root      0:00 ps aux
    

    👉 point at PID 1 is sh and the process list has two entries. Then run hostname and ps aux | wc -l in the right pane (the host) and compare: same kernel, same machine, completely different view. “Nothing was virtualised. The process just cannot see the rest.”

  3. A long-running, detached, published container.

    docker run -d --name web -p 8080:80 nginx:1.28-alpine
    curl -s localhost:8080 | head -4
    docker logs web
    

    👉 point at the log line produced by your curl — “the container’s stdout is the log; nobody configured a log file”.

  4. Get a shell inside the running container, then walk the lifecycle.

    docker exec -it web sh -c 'hostname; ls /usr/share/nginx/html'
    docker stop web && docker ps -a --filter name=web
    docker start web && curl -s -o /dev/null -w '%{http_code}\n' localhost:8080
    docker rm -f web
    

    👉 after stop, point at Exited (0); after start, the same container is back (same ID, same name) — run would have created a new one. rm is what actually deletes it.


Demo 2: what is a container, really? (≈ 8 min)

What to show / say

Namespaces + cgroups + a layered filesystem. No hypervisor, no guest kernel — and none of it is Docker-specific.

Steps

  1. Look at the isolation from the inside:

    docker run --rm -it alpine:3.22 sh
    
    ps aux
    cat /proc/self/cgroup
    ls /proc/1/ns
    exit
    
    / # cat /proc/self/cgroup
    0::/
    / # ls /proc/1/ns
    cgroup  ipc  mnt  net  pid  time  user  uts
    

    👉 0::/ — “the container thinks it is at the root of the cgroup tree. It is not.”

  2. Namespaces without Docker — this kills the “Docker is magic” idea:

    unshare --user --pid --fork --mount-proc ps aux
    
    PID   USER     TIME  COMMAND
        1 root      0:00 ps aux
    

    👉 “One kernel call, no Docker, no image — and I am PID 1. Docker’s job is to make this repeatable and to give the process a filesystem.”

  3. Now the same container seen from the host — this is the key slide made real:

    docker run -d --name web -p 8080:80 nginx:1.28-alpine
    pid=$(docker inspect --format '' web); echo "$pid"
    ps -o pid,ppid,cmd -p "$pid"
    sudo ls -l /proc/$pid/ns/ | awk '{print $9, $10, $11}'
    sudo ls -l /proc/1/ns/    | awk '{print $9, $10, $11}'
    cat /proc/$pid/cgroup
    
    nginx: master process nginx -g daemon off;
    net -> net:[4026532546]
    pid -> pid:[4026532548]
    mnt -> mnt:[4026532544]
    ...
    0::/system.slice/docker-9c1f….scope
    

    👉 point at the inode numbers: net, pid, mnt, uts, ipc differ from PID 1’s; user and time are usually the same inode (no user-namespace remapping by default). Then point at the host-side cgroup path — “that is where --memory and --cpus will write limits in Session 03.”

  4. The filesystem — union/overlay mounts:

    docker inspect --format '' web | python3 -m json.tool
    
    {
        "Data": {
            "LowerDir": "/var/lib/docker/overlay2/…/diff:/var/lib/docker/overlay2/…/diff",
            "MergedDir": "/var/lib/docker/overlay2/…/merged",
            "UpperDir": "/var/lib/docker/overlay2/…/diff",
            "WorkDir": "/var/lib/docker/overlay2/…/work"
        },
        "Name": "overlay2"
    }
    

    👉 LowerDir = the read-only image layers, UpperDir = the container’s writable layer, MergedDir = what the process sees as /. “Delete the container and the UpperDir goes with it. Remember that for Session 03.”

  5. Clean up before Demo 3:

    docker rm -f web
    

Demo 3: image names, tags and digests (≈ 5 min)

What to show / say

nginx is shorthand for docker.io/library/nginx:latest. A tag is a moving pointer; a digest is the content itself.

Steps

  1. Pull an explicitly named image and read its digest:

    docker pull nginx:1.28-alpine
    docker image inspect --format '' nginx:1.28-alpine
    
    nginx@sha256:d5f5f6f5b0f4b0a5f6ee9b5a0f9e64f9c1b3f9f1b1b8f1a2c3d4e5f60718293a
    

    👉 read the full name aloud, piece by piece: docker.io / library / nginx : 1.28-alpine @ sha256:… — registry / namespace / repository / tag / digest.

  2. Pull by digest and show that it lands on the same image:

    digest=$(docker image inspect --format '' nginx:1.28-alpine)
    docker pull "$digest"
    docker image ls --digests nginx
    
    REPOSITORY   TAG          DIGEST                       IMAGE ID       SIZE
    nginx        1.28-alpine  sha256:d5f5f6f5b0f4…         a1b2c3d4e5f6   52.3MB
    nginx        <none>       sha256:d5f5f6f5b0f4…         a1b2c3d4e5f6   52.3MB
    

    👉 same IMAGE ID, same digest, one entry has no tag. “The digest is the identity. The tag is a label someone can move tomorrow.”

  3. The latest trap, in ten seconds:

    docker pull nginx
    docker image ls nginx
    

    👉 “latest is not ‘the newest version’. It is just the tag you get when you don’t ask for one — and on many images it is months old, or points at a different variant. In this course every image is named with an explicit tag.”


Demo 4: CLI patterns you’ll use all week (≈ 6 min)

What to show / say

Six flags and three inspection commands cover 90 % of daily Docker use — and they are exactly what Lab 01 needs.

Steps

  1. --rm for throwaway commands:

    docker run --rm alpine:3.22 date -u
    docker ps -a | wc -l
    

    👉 “No leftovers. Use --rm for anything you run once.”

  2. Configuration is environment variables — including when it fails:

    docker run --rm postgres:17-alpine
    
    Error: Database is uninitialized and superuser password is not specified.
           You must specify POSTGRES_PASSWORD to a non-empty value for the
           superuser.
    

    👉 “The image told us its contract. Read the error, then pass -e.”

  3. Now with the env vars Lab 01 uses:

    docker run -d --name pinboard-db \
      -e POSTGRES_USER=pinboard \
      -e POSTGRES_PASSWORD=pinboard-secret \
      -e POSTGRES_DB=pinboard \
      postgres:17-alpine
    docker logs pinboard-db | tail -3
    docker inspect --format '' pinboard-db | head -4
    
    database system is ready to accept connections
    

    👉 “Same container you will start in the lab in twenty minutes.”

  4. Live resource usage:

    docker stats --no-stream
    
    CONTAINER ID   NAME          CPU %   MEM USAGE / LIMIT   MEM %   NET I/O     PIDS
    b2c3d4e5f6a7   pinboard-db   0.05%   31.2MiB / 15.6GiB   0.20%   1.1kB/0B    7
    

    👉 point at MEM USAGE / LIMIT: the limit is the whole host — “no limit is a limit you didn’t set. Session 03 fixes that.”

  5. Where the disk went:

    docker system df
    
    TYPE            TOTAL   ACTIVE   SIZE      RECLAIMABLE
    Images          5       2        412.7MB   287.1MB (69%)
    Containers      3       1        1.2kB     0B (0%)
    Local Volumes   1       1        41.2MB    0B (0%)
    Build Cache     0       0        0B        0B
    

    👉 “docker system prune (and prune -a, and -f) reclaims that. Know what -a deletes before you type it on a machine you care about.”


Reset

Lab 01 expects: no containers running, the images already pulled, no volumes.

docker rm -f pinboard-db web box 2>/dev/null
docker container prune -f
docker volume prune -f
docker image ls   # keep alpine:3.22, nginx:1.28-alpine, postgres:17-alpine, hello-world

Verify before handing over to the lab:

docker ps -a          # empty
docker volume ls      # empty