██ What is Containerization?
• Lightweight virtualization method
◦ Application or Operating System level
• Isolate applications and their dependencies/runtimes
• Ensure portability across multiple systems
◦ Only container runtime is required
▍ https://medium.com/nerd-for-tech/docker-in-a-nutshell-but-it-works-on-my-machine-well-then-lets-ship-your-machine-81c6df2
Software Engineering II: Containers 2 / 19
██ Kinds of Containers ██ This lecture
• LXC/LXD We will focus on OCI Containers
◦ "System containers" that behave like virtual
machines Tools supporting OCI runtime or image specification:
◦ Contains full OS including init system (e.g.
systemd) • Runtimes: runc, crun, Kata
◦ Used by e.g. Proxmox and Incus ◦ Lowest level, ensure isolation, start and stop
• Kata Containers container processes
◦ Promise: Security of VM, speed of containers • Engines: Docker/Podman, containerd, CRI-O
◦ Uses one kernel per container ◦ User facing interface
◦ Runs inside a micro-VM ◦ Utilize runtime to manage containers
◦ Integrates with orchestration tools like Docker ◦ Provide CLI and API to do so
or Kubernetes • Orchestration: Kubernetes (k8s)
• Open Container Initiative (OCI) Containers ◦ Schedule containers across multiple machine
◦ Vendor-neutral container standard ◦ Scaling, self-healing, rolling updates
◦ Defines how image is packaged • Fun fact: Nix can build OCI images
◦ Defines container lifecycle (create, start, stop) • Also, systemd (kind of) can handle OCI (
https://fosdem.org/2026/schedule/event/ZKKQWC-native
_oci_container_support_in_systemd/)
Software Engineering II: Containers 3 / 19
• OCI image is essentially a tarball • A registry holds OCI images
• Consists of multiple "Layers" • Can be public or private
◦ Layers are stacked changes to the images • Keeps multiple releases of the same image
• Image contains metadata ◦ Can perform tasks like vulnerability scanning
◦ Entrypoint/Command, Env, Architecture, ...
• manifest.json describes how image is stacked
Software Engineering II: Containers 4 / 19
██ OCI Runtime ▓▓▓ Container Runtime Interface (CRI)
• Defines container lifecycle • Adds "typical" features not part of the OCI spec
◦ start/stop/create/... • Logical Volumes
• Containers can have entrypoint and command ◦ Addition to volume mounts
◦ Entrypoint is called with command as arguments • Pods (group of containers)
◦ Command can be overwritten by user ◦ Only in K8s and Podman, not Docker
◦ Entrypoint is supposed to be fixed ◦ Shared network namespace/volumes
• Isolated FS, Process and Network namespaces ◦ Sometimes shared process namespace
◦ cgroups (CPU/memory/IO) ▪ By default: Podman shared, K8s unshared
◦ seccomp filters ◦ Secrets
◦ volume mounts ▪ Not really secret, mostly base64 encoded
▪ Handle RBAC, not encryption
▪ There are additions that encrypt secrets ->
Software Engineering II: Containers 5 / 19
Docker vs Podman: Summary
Comparison of the two major container engines
Feature │ Docker │ Podman
─────────────┼────────────────────────┼─────────────────────────────────────────
Architecture │ Client/Server │ Daemonless
Permissions │ Rootful by default │ Rootless by default
Daemon │ Yes, always running │ Optional (Docker API support)
Runtime │ containerd / runc │ CRI-O / crun
Pods (k8s) │ Not natively │ Native support
IaC │ docker-compose │ compose, quadlet, k8s
Network │ ⚠️ Overwrites iptables │ Rootless: Userspace (also has drawbacks)
│ │ Rootful: ⚠️ Overwrites iptables
In general, Podman can be used as a Docker replacement: alias docker=podman
Of course there are differences when comparing rootless against rootful
This lecture uses Podman as default -> alias podman=docker if you have Docker installed
Software Engineering II: Containers 6 / 19
podman and docker are interchangeable (for the most part)
podman run: Subcommand to run containers
podman run [--rm] [-it] [--entrypoint <myentrypoint>] [--name mycontainer] myimage <command>
• --rm removes container after run
• -it = interactive with pseudo-TTY
• --name adds a descriptive name
• -v/--volumes: Attach volumes or volume mounts: -v ./my-local-folder:/mnt/folder-in-container:ro,Z
◦ Add :ro for read-only volumes, Z or z for SELinux context
◦ SELinux: container_file_t label
◦ z for shared volumes, Z if only container is allowed to write
• --publish, -p=[[ip:][hostPort]:]containerPort[/protocol]
◦ Publish a port, e.g. -p 8080:80 to map 80 in container to 8080 on host
• Network modes: Bridge, Host, none
• ⚠️ Double-check your firewall when running in rootful mode
Software Engineering II: Containers 8 / 19
• Implicit container image will be resolved to • Like git tags, handling depends on project
configured registry • Can be image:tag or image@sha256:<shasum>
• Available registries: • Ideally: Multiple SemVer releases, e.g. version
◦ Docker Hub (docker.io) 1.2.3, 1.2 and 1 refer to the same tag
◦ Quay (quay.io) ◦ Tags can also contain distros: python:3.14,
◦ GitHub Container Registry (ghcr.io) python:3.14-alpine, python:3.14-alpine3.23
◦ Kubernetes Container Registry (registry.k8s.io) ◦ Implicit tag is always latest. Tags like latest,
◦ GitLab/Forgejo: Usually same domain as git stable, dev, ... can exist but again depend on
• Registries can also be written down explicitly
◦ Recommended behavior, can still be overwritten by ▒▒▒▒ Commands
▪ Useful when using custom caching registry • Use podman image ls to show images
• Images can be retagged:
◦ podman image tag <oldtag> <newtag>
Software Engineering II: Containers 9 / 19
Tag usage always depends on requirements
◦ Use shasum or explicit tag, e.g. 2.1.0
◦ Use rolling images, e.g. stable or 2
• No silver bullet to solve this problem
• Explicit tags help reproduce older builds
◦ Imagine rebuilding a Container from git history one year ago
◦ Spoiler: Does not work very often...
• Workaround: Using explicit images & Dependabot/Renovate bot
◦ (Auto-)updates always depend on use case!
Software Engineering II: Containers 10 / 19
Typical Linux distros are available on e.g. Docker Hub
• Debian: trixie = 13 = 13.3
• Ubuntu: resolute = 26.04 = devel
• Fedora: 43 = latest, 44 = rawhide
• Archlinux: latest = base, base-devel
Software Engineering II: Containers 11 / 19
Building Container Images
Now we want to build our own image
# Indicates start of new image
• podman build <context> [-t <tag>] [-f <file>] FROM docker.io/debian:13
• Dockerfile and Containerfile are automatically
detected by Podman # Add a new step inside the layer, e.g. to install
• Build context (usually .) defines the base directory # a package, then clean up (reduce size)
◦ Files can only be copied from this directory (or RUN apt-get update && apt-get install nano \
subdirectories) && rm -rf /var/lib/apt/lists/*
◦ Add .dockerignore for sensible files!
▪ Slightly different syntax from .gitignore # Change the workdir (create directory)
▪ Symlinking might work WORKDIR /myworkdir
• Each directive (RUN, COPY) creates a new layer
◦ Cache is invalidated with first changed layer # Copy a file from the host
Command can be overwritten, e.g: COPY example.txt .
podman run --rm -it image /etc/os-release # Use command or shell notation
Software Engineering II: Containers 12 / 19
Your task is to create two simple container images, one running hello world both in C and Python:
• Create a file hello_world.py • Create hello_world.c and a second Containerfile
• Use a simple Python image to run the file • Start with a Debian image (docker.io/debian:slim)
◦ You don't need to build an image, just modify the • Install the build tools
volume mount and command • Compile the C file and run it
• Do you think this is a good solution?
◦ No! We want to reduce the overhead!
Software Engineering II: Containers 13 / 19
██ Base distros ██ More minimal distro
Typical Linux distros are available on e.g. Docker Hub • debian:slim (more minimal Debian)
• alpine (minimal distro with busybox and musl instead
• Debian: trixie = 13 = 13.3 of coreutils and glibc)
• Ubuntu: resolute = 26.04 = devel
• Fedora: 43 = latest, 44 = rawhide They still contain shell and other "useless" stuff
• Archlinux: latest = base, base-devel
• Developers use them because they are lazy ◦ Developed by Google
• Images contain a lot of overhead ◦ Only application & shared libraries
• Good for development / PoC ◦ Minimal OS files
• Bad for production (minimizing attack surface) ◦ No shell/package manager
◦ Meta image (built-in, no registry required)
• Scratch is not always the answer. Sometimes we need tools like shell, cat, ...
• Benchmarks indicate that images without glibc might perform worse!
Software Engineering II: Containers 14 / 19
Idea: Less stuff in image
• Each FROM directive starts a new image FROM docker.io/debian as builder
◦ Last FROM directive (or target) specifies the RUN apt-get update &&\
image to build apt-get install gcc libc6-dev
◦ Other images are stored as temporary artifacts WORKDIR /src
• Build dependencies are not shipped to prod image COPY hello_world.c .
◦ Ensure that all required libraries are present on RUN gcc -static -O2 -s hello_world.c -o hello_world
◦ Requires static compilation # Stage 2: prod
• Default: Build last target in image FROM scratch as prod
◦ Run podman build --target builder to build the COPY --from=builder /src/hello_world /hello
first layer explicitly ENTRYPOINT ["/hello"]
◦ Useful to have prod and dev image in same
Software Engineering II: Containers 15 / 19
Building Container Images - Advanced
• COPY can also be run with --chown user:group and --chmod 640
◦ Works different from cp command on directories # specify arg with default
◦ COPY ./src/ . copies the content of src to . ARG DEBIAN_VERSION=13
• ENV can be used to persist environment variables # use base image version
• EXPOSE can be used to highlight ports FROM debian:${DEBIAN_VERSION}
◦ e.g. EXPOSE 443/tcp for a web server # ensure reuse of ARG
◦ Only metadata, does not actually forward a port ARG DEBIAN_VERSION
• ARG can be used to add build-time argument # move ARG to ENV
• ARGs are reset per stage (after each FROM) ENV DEBIAN_VERSION=${DEBIAN_VERSION}
◦ Just declare them again, they keep their value # finally consume build arg
◦ Use --build-arg <key>=<value> to add arguments to the build CMD echo $DEBIAN_VERSION
Software Engineering II: Containers 16 / 19
Container image: Reducing Privileges
Now we know how to build smaller images - But how to restrict permissions?
• Switch to a different user/group
◦ USER <user>[:<group>]: useradd must be used to create new user
◦ USER UID[:<GID>]: User must not exist, only UID/GID are assigned
▪ Can also be accomplished at runtime: podman run --user <uid>[:<gid>]
But: I am running rootless Podman... Why do I have a root user at all?
• Rootless mode uses subuid mapping
◦ /etc/sub{u,g}id contains mapped user-id address space
◦ myuser:524288:65536: 2**16 subuids starting with 524288
◦ root in container is myuser on the host
◦ Other user in container becomes subuid, e.g. 1 in container is 524288 on the host
• But... I cannot access files created by 524288
◦ You need to access the subuid space ("unshare")
◦ podman unshare id -> uid=0(root) gid=0(root)
◦ podman unshare is like sudo for the subuid range
• Can Podman auto-resolve such issues for me? • As always, keep privileges to a minimum.
◦ Volume options idmap=auto or idmap=keep-id --userns=keep-id is kind of equal to running root
(rootful) inside the container.
◦ Or podman run --userns=keep-id (rootless)
Software Engineering II: Containers 17 / 19
██ OCI Image Authenticity
• We talked about integrity of images, but can we also verify trusted publishers of images?
• We can use image signing, e.g. using Cosign (https://github.com/sigstore/cosign)
◦ Verify image against public key
◦ Podman (and registries like goharbor) can reject pulling unverified images
I need to run rootless inside the container, but I want to utilize the ip command
• You will need to add the NET_ADMIN capability to your container
• Actually, ip is an edge case and drops all non-inherited capabilities
• Capabilities can be added to the container sandbox
◦ They can also be set during image build (using setcap as root)
Software Engineering II: Containers 18 / 19