Container images and runtimes

Listen to this lesson

Episode 34 · 63:27

This episode is a study companion for CompTIA Linux+ XK0-006 and is not produced by or endorsed by CompTIA.

Objective 2.6 · Services and User Management · 20% of the exam

Why this matters

Objective 2.6 is the largest single objective on XK0-006 — 46 sub-topics — which tells you how much weight CompTIA puts on containers. That reflects reality: most new Linux workloads are containerised, and a Linux administrator who cannot read a Dockerfile or debug a container that will not start is missing a large part of the job.

A container is not a small virtual machine. It is a process on the host kernel, isolated by namespaces and limited by cgroups. Holding that in mind explains nearly everything else — why containers start in milliseconds, why they cannot run a different kernel, and why isolation is weaker than a VM's.

The lesson

Runtimes

Several pieces, at different layers, and knowing which is which stops a lot of confusion.

runC is the low-level runtime. It does the actual work of creating a container from a bundle — setting up namespaces and cgroups and starting the process. It implements the OCI runtime specification and you will rarely invoke it directly.

containerd sits above runC and manages images, storage and container lifecycle. It is what Docker uses underneath, and what Kubernetes talks to directly.

Docker is the full platform: CLI, a daemon (dockerd), image building, networking, and Docker Hub. It popularised containers and remains the default vocabulary.

Podman is Red Hat's alternative and the one the exam leans toward. Two differences matter:

  • Daemonless. There is no long-running root daemon; podman starts containers as child processes of your shell. Nothing to crash, nothing to attack.
  • Rootless by default. Containers run as your user via user namespaces, so a container escape lands you as an unprivileged user rather than root.
alias docker=podman        # the CLIs are deliberately compatible

Podman implements the same commands, so almost everything below works with either. That compatibility is intentional, and it is why alias docker=podman is a real migration strategy rather than a joke.

Images, layers and tags

An image is a read-only template. A container is a running instance of one — image is to container as program is to process.

Images are built from layers, each a filesystem diff from the one below. Layers are content-addressed and shared: ten images built FROM ubuntu:22.04 store that base once. Pulling an image you partly have downloads only the missing layers.

A running container adds one thin writable layer on top. Everything it writes goes there, and it is discarded when the container is removed — which is the whole reason volumes exist.

Tags name a version: nginx:1.25, nginx:latest. A tag is a mutable pointer, not an identity — latest today and latest next month may be entirely different images.

Pin tags in production. FROM node:latest builds differently every time and eventually breaks without anything in your code having changed. Pin to a specific version, or to a digest (nginx@sha256:...) for a truly immutable reference.

Pulling and inspecting

podman pull nginx:1.25
podman pull docker.io/library/nginx:1.25     # fully qualified
podman images                                # local images
podman image inspect nginx:1.25
podman history nginx:1.25                    # the layers, and what made each
podman search nginx

Podman does not assume Docker Hub; /etc/containers/registries.conf lists which registries an unqualified name is searched in. That is a security property — an unqualified name resolving to an unexpected registry is a supply-chain risk — and it is why Podman sometimes prompts you to choose.

podman history shows which Dockerfile instruction created each layer and how large it is. It is the first tool for "why is this image 1.8 GB".

Building an image

A Dockerfile (or Containerfile) describes the build:

FROM node:20-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .

ENV NODE_ENV=production
EXPOSE 3000
USER node
ENTRYPOINT ["node"]
CMD ["server.js"]
podman build -t myapp:1.0 .
podman build -t myapp:1.0 -f Containerfile.prod .
podman build --no-cache -t myapp:1.0 .

The instructions worth understanding rather than memorising:

FROM sets the base image and must come first. Choosing it is a security decision: alpine variants are a fraction of the size and carry far fewer packages, so far fewer CVEs. -slim and distroless images go further.

USER sets the user for everything after it, and for the running container. Without it a container runs as root, and while that is root inside a namespace rather than on the host, it is unnecessary privilege — and under Docker's root daemon it is a genuine escalation route. USER is one line and it is the highest-value line in most Dockerfiles.

ENTRYPOINT versus CMD is the pair people get wrong:

  • ENTRYPOINT is the executable — what the container fundamentally is.
  • CMD is the default arguments, and is replaced by anything on the command line.

With the Dockerfile above, podman run myapp runs node server.js, while podman run myapp worker.js runs node worker.js. If you set only CMD ["node","server.js"], then podman run myapp bash replaces the whole thing and gives you a shell — which is convenient for debugging and wrong for a container whose job is fixed.

Layer ordering decides build speed. Each instruction is a cached layer, and a change invalidates that layer and everything after it. Copying package*.json and installing dependencies before copying the source means editing your code does not re-run npm ci. Reversing those two lines turns a five-second rebuild into a five-minute one.

Running containers

podman run nginx                          # foreground
podman run -d nginx                       # detached
podman run -d --name web -p 8080:80 nginx # named, with a port mapping
podman run -it ubuntu bash                # interactive shell
podman run --rm alpine echo hi            # remove when it exits
podman run -d -e DB_HOST=db -e DB_PORT=5432 myapp
podman run -d --env-file .env myapp
podman run -d --memory=512m --cpus=1.5 myapp
podman run -d --restart=unless-stopped myapp

-p 8080:80 maps host 8080 to container 80 — host first. Getting it backwards is a rite of passage.

-e sets environment variables, which is how containers are configured; --env-file reads them from a file. Note that environment variables are visible in podman inspect and in the host's process list, so they are the wrong place for secrets — use a secrets mechanism or a mounted file.

Managing running containers

podman ps                       # running
podman ps -a                    # including stopped
podman stop web                 # SIGTERM, then SIGKILL after a grace period
podman start web
podman restart web
podman kill web                 # SIGKILL immediately
podman rm web                   # delete a stopped container
podman rm -f web                # stop and delete

stop sends SIGTERM and waits — the same graceful-shutdown reasoning as the signals lesson — so an application that handles it can finish in-flight work. kill does not wait.

Logs, inspection and getting inside

podman logs web
podman logs -f web              # follow
podman logs --tail 50 web
podman logs --since 10m web

podman inspect web              # everything: mounts, network, env, state
podman inspect -f '{{.State.Status}}' web
podman inspect -f '{{.NetworkSettings.IPAddress}}' web

podman exec -it web bash        # a shell INSIDE a running container
podman exec web ls /app
podman top web                  # processes inside it
podman stats                    # live CPU, memory, I/O per container
podman port web
podman diff web                 # what has changed vs the image

logs then exec is the debugging sequence. A container writes to stdout and stderr rather than to log files, by design — the runtime collects them, so podman logs is where application output goes. A container that exits immediately has usually printed why, and podman logs shows it even after the container has stopped.

podman exec -it web bash fails on minimal images because there is no bash — try sh. On distroless images there is no shell at all, which is deliberate and means debugging by other means.

podman diff shows what has been written into the container's writable layer since it started, which quickly reveals an application writing data somewhere that will be lost.

Volumes, briefly

podman run -d -v mydata:/var/lib/mysql mariadb        # named volume
podman run -d -v /srv/config:/etc/app:ro myapp        # bind mount, read-only
podman volume ls

Anything written outside a volume lives in the writable layer and disappears when the container is removed. A database in a container without a volume loses everything on the first podman rm, and this is the most common serious container mistake. Volumes and networks are the next lesson.

Pruning: cleaning up

Pruning is the term for reclaiming space. Images, stopped containers, unused volumes and build caches accumulate quickly and will fill a disk.

podman container prune          # remove stopped containers
podman image prune              # remove dangling images
podman image prune -a           # remove ALL unused images
podman volume prune             # remove unused volumes
podman system prune -a          # everything unused
podman system df                # what is actually using the space

prune is not reversible and volume prune deletes data. A volume is "unused" the moment no container references it, which includes the gap after you remove a container and before you recreate it. Run podman system df first, and be specific rather than reaching for system prune -a on a machine you care about.

On the exam

  • A container is a process on the host kernel with namespaces and cgroups — not a VM. It cannot run a different kernel.
  • Podman is daemonless and rootless by default; Docker uses a root daemon. The CLIs are compatible.
  • runC is the low-level runtime, containerd manages images and lifecycle above it.
  • ENTRYPOINT is the executable, CMD the default arguments that a command line replaces.
  • USER in a Dockerfile stops the container running as root.
  • Layers are cached in order — put dependency installation before copying source.
  • -p host:container, host first.
  • Tags are mutable; pin versions or digests in production.
  • podman logs shows why a container exited; exec -it needs a shell to exist in the image.
  • Data outside a volume is lost when the container is removed.

Practise what you just read

1. Which statement best describes what a container is?

Select one

  1. A packaged binary that runs without any host dependencies
  2. A process on the host kernel, isolated by namespaces and limited by cgroups
  3. A lightweight virtual machine with its own kernel
  4. A chroot environment with a private copy of the operating system
Show answer

B. Holding this in mind explains nearly everything else: containers start in milliseconds because starting one is starting a process; they cannot run a different kernel from the host, so a Windows container will not run on Linux; and isolation is weaker than a VM's, because a kernel vulnerability is shared by every container on the machine.

2. What are the two defining differences between Podman and Docker?

Select one

  1. Podman uses a different image format and its own registry
  2. Podman is daemonless and rootless by default
  3. Podman runs only on RHEL-family systems and uses runC alone
  4. Podman requires systemd and cannot run interactive containers
Show answer

B. Podman starts containers as child processes of your shell rather than through a long-running root daemon, and runs them as your unprivileged user via user namespaces. Both reduce the attack surface considerably. The image format and registries are the same OCI standards, and the CLIs are deliberately compatible -- alias docker=podman is a real migration strategy.

3. In "podman run -d -p 8080:80 nginx", which port belongs to the host?

Select one

  1. 80, since the container's port is given first
  2. 8080, since the host port is given first
  3. Both, since the mapping is bidirectional
  4. Neither; the numbers are the internal and external container ports
Show answer

B. The form is -p host:container, host first. Reversing it is a rite of passage and produces a container that appears to start correctly and is unreachable. Note also that -p 8080:80 binds every interface -- a database or admin interface should use -p 127.0.0.1:8080:80 to stay local.

8 more questions on this objective are part of the full course.

Practise the full question bank in the exam simulator

Hands-on labs

All hands-on labs