The Kubernetes Fundamentals domain in my KCNA study notes assumes container runtime knowledge going in - Docker, in practice, since it’s still the most common way people get there. The Kubernetes Fundamentals series is the companion to this post, one level up the stack.


Prerequisites

  • WSL2 with Ubuntu on Windows, or native Linux/macOS
  • sudo access on the machine you’re installing on
  • A Docker Hub account if you want to push images

Installing Docker

On WSL2/Ubuntu, install Docker Engine directly rather than Docker Desktop - run this from your home directory in the WSL2 shell:

sudo apt update
sudo apt install -y ca-certificates curl gnupg

# Add Docker's GPG key to a keyring
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# Add the repo - $(. /etc/os-release && echo $VERSION_CODENAME) resolves to your release codename
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Without this next step, every docker command needs sudo. Add your user to the docker group, then start a new shell session for it to take effect:

sudo usermod -aG docker $USER

On native macOS or Windows without WSL2, Docker Desktop is the simpler path - Mac install or Windows install.

Verify the install:

docker version
docker run hello-world

Core commands

Images

docker pull nginx:latest      # pull only - doesn't start a container
docker images                 # list local images
docker image history nginx    # inspect the layers that make up an image
docker rmi nginx              # remove an image

Containers

docker run nginx                  # attached - blocks this terminal
docker run -d nginx               # detached - returns immediately
docker run -d --rm nginx          # detached, auto-removed when it exits
docker run -it ubuntu bash        # interactive shell inside a new container
docker exec -it <container> bash  # shell into an already-running container
docker ps                         # running containers
docker ps -a                      # all containers, including stopped ones
docker logs <container>           # a container's stdout/stderr
docker stop <container>           # stop a running container
docker rm <container>             # remove a stopped container

Ports, volumes, and environment variables

docker run -d -p 8080:80 nginx                                       # map host port 8080 to container port 80
docker run -d -p 8080:80 -v $(pwd)/my-site:/usr/share/nginx/html nginx  # bind-mount a local folder into the container
docker run -e MYSQL_ROOT_PASSWORD=changeme mysql                     # set an environment variable

Try the volume mount end to end:

mkdir my-site && echo "hello from my site" > my-site/index.html
docker run -d --rm -p 8080:80 -v $(pwd)/my-site:/usr/share/nginx/html nginx
curl http://localhost:8080

Building images

CMD vs ENTRYPOINT

CMD sets a default command that’s replaced entirely if you pass a command at docker run:

FROM alpine
CMD ["sleep", "5"]

docker run myimage sleeps for 5 seconds. docker run myimage echo hi runs echo hi instead - CMD is ignored completely, not appended to.

ENTRYPOINT is fixed - anything passed at docker run is appended to it as an argument, rather than replacing it:

FROM alpine
ENTRYPOINT ["sleep"]
CMD ["5"]

docker run myimage runs sleep 5. docker run myimage 10 runs sleep 10 - CMD here is just ENTRYPOINT’s default argument, overridable without touching the fixed command itself.

Multi-stage builds

Building cmatrix (a terminal screensaver - harmless example, real teaching point, adapted from James Spurin’s Dive Into… courses) from source needs a full compiler toolchain: alpine-sdk, autoconf, ncurses-dev. None of that needs to ship in the image that actually runs the binary. A multi-stage build keeps the build toolchain in one stage and copies out only the compiled result:

# Stage 1 - build
FROM alpine AS builder

WORKDIR /cmatrix

RUN apk add --no-cache git autoconf automake alpine-sdk ncurses-dev ncurses-static && \
    git clone https://github.com/spurin/cmatrix.git . && \
    autoreconf -i && \
    mkdir -p /usr/lib/kbd/consolefonts /usr/share/consolefonts && \
    ./configure LDFLAGS="-static" && \
    make

# Stage 2 - runtime
FROM alpine

LABEL org.opencontainers.image.description="Container image for cmatrix"

RUN apk add --no-cache ncurses-terminfo-base && \
    adduser -D -H -s /usr/sbin/nologin appuser

COPY --from=builder /cmatrix/cmatrix /usr/local/bin/cmatrix

USER appuser
ENTRYPOINT ["cmatrix"]
CMD ["-b"]

From the same directory as the Dockerfile above:

docker build -t your-dockerhub-username/cmatrix .
docker run --rm -it your-dockerhub-username/cmatrix

The final image only carries alpine plus the compiled binary - none of the build-time packages from stage 1 make it into the image that ships.

Multi-architecture builds

docker build produces an image for your local architecture only. buildx builds and pushes more than one architecture from the same Dockerfile in one command. From the same directory as the Dockerfile above:

docker login
docker buildx create --name multiarch-builder --use
docker buildx build --platform linux/amd64,linux/arm64 -t your-dockerhub-username/cmatrix:latest --push .

Docker Compose

Compose runs more than one container from a single file. Current Compose doesn’t need a version: key, and containers on the same Compose file share a default network automatically - they reach each other by service name, with no manual links: configuration:

services:
  web:
    image: nginx
    ports:
      - "8080:80"
    depends_on:
      - cache
  cache:
    image: redis:alpine
docker compose up -d
docker compose down

web can reach cache at the hostname cache - Compose’s built-in DNS resolves service names within the file automatically.


Registries

RegistryNotes
Docker HubThe default public registry - docker pull/docker push use it unless you specify otherwise
Private registriesAmazon ECR, Azure Container Registry, Google Artifact Registry are the managed options on each major cloud
Self-hostedRun your own with docker run -d -p 5000:5000 --name registry registry:2
docker login <registry-url>
docker tag myimage:latest <registry-url>/myimage:latest
docker push <registry-url>/myimage:latest

Cleanup

docker system prune     # remove stopped containers, unused networks, dangling images, build cache
docker system prune -a  # also remove unused images, not just dangling ones

Resources


Notes

  1. This covers Docker specifically, not Podman or containerd directly - KCNA’s Container Orchestration domain covers the CRI abstraction that lets Kubernetes work with any of them interchangeably.