Kubernetes · Kubernetes Foundations · Foundation · 20 min read · FREE LESSON

Containers and Images: The Foundation Under Every Pod

How containers actually work, how to build small, fast, secure images, how registries and tags behave, and a team standard for container builds you can roll out.

A story to begin with

What a container really is

A container is an ordinary process on a Linux machine, given a restricted view of the system:

  • Namespaces give it its own view of process IDs, network, mounts and hostname.
  • cgroups limit how much CPU and memory it can use.
  • A layered filesystem gives it its own files, built from an image.

It is not a tiny virtual machine. Containers share the host's kernel, which is why they start in milliseconds and use little memory, and also why a kernel-level problem affects all of them.

flowchart TB
  subgraph Host[One Linux host, one kernel]
    subgraph C1[Container A]
      A1[App A + its libraries]
    end
    subgraph C2[Container B]
      B1[App B + its libraries]
    end
    RT[Container runtime<br/>namespaces + cgroups]
  end
  RT --> C1
  RT --> C2
  style RT fill:#6366f1,color:#fff

Images are stacks of layers

An image is a read-only stack of layers plus metadata. Each Dockerfile instruction creates a layer; unchanged layers are cached and shared.

flowchart BT
  L1[Base layer: minimal OS files] --> L2[Layer: runtime, e.g. Python]
  L2 --> L3[Layer: dependencies installed]
  L3 --> L4[Layer: your application code]
  L4 --> I[Image: my-app:1.4.2]
  style L4 fill:#34d399,color:#062

Why this matters: put things that change rarely near the top of the Dockerfile (base image, dependencies) and things that change often (your code) near the bottom, so rebuilds reuse cached layers and finish in seconds.

From a naive Dockerfile to a good one

Version 1: works, but heavy and risky

FROM ubuntu:latest
RUN apt-get update && apt-get install -y python3 python3-pip build-essential
COPY . /app
WORKDIR /app
RUN pip3 install -r requirements.txt
CMD ["python3", "app.py"]

Problems: latest can change under you, the whole OS and compilers ship to production, the app runs as root, and every code change reinstalls dependencies.

Version 2: better practice

# ---- build stage: has compilers and tools, never shipped ----
FROM python:3.12-slim AS build
WORKDIR /src
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# ---- runtime stage: small and locked down ----
FROM python:3.12-slim
RUN useradd --create-home --uid 10001 appuser
WORKDIR /app
COPY --from=build /install /usr/local
COPY --chown=appuser app/ .
USER 10001
EXPOSE 8080
HEALTHCHECK CMD python -c "import urllib.request;urllib.request.urlopen('http://localhost:8080/healthz')"
CMD ["python", "app.py"]

What changed and why:

  • Pinned, slim base image: smaller, predictable.
  • Multi-stage build: build tools never reach production.
  • Dependencies copied before code: cache-friendly.
  • Non-root user: a compromised process has far less power.
  • Only the necessary files: add a .dockerignore to exclude .git, tests and local files.

Tags, digests and registries

  • A tag (my-app:1.4.2) is a movable label. Anyone with push rights can point latest or even 1.4.2 at a different image.
  • A digest (my-app@sha256:...) identifies exact image content and never changes.
  • A registry (Docker Hub, ECR, GCR, ACR, Harbor) stores images.

Rules that prevent surprises:

  1. Never deploy :latest. Use immutable version tags (a version number or Git SHA).
  2. For stricter control, deploy by digest.
  3. Use a private registry with access control; mirror public base images to avoid rate limits and supply-chain surprises.
  4. Scan images in CI and again on a schedule, because new vulnerabilities appear after you ship.
  5. Sign images and verify signatures at deployment time (cosign, admission policy) for sensitive environments.

Running a container locally

docker build -t my-app:1.4.2 .
docker run --rm -p 8080:8080 my-app:1.4.2
docker images                      # see sizes
docker history my-app:1.4.2        # see which layer is big

Habit to build: check the size and the layer history of every image you ship.

Common container mistakes

Mistake Consequence Fix
Running as root Larger blast radius if compromised USER non-root, drop capabilities
Secrets baked into the image Anyone who can pull the image can read them Inject at runtime via Secrets or a vault
One container, many processes Hard to monitor and restart cleanly One main process per container
Writing logs to files in the container Lost when the container restarts Log to stdout and stderr
Huge images Slow pulls, slow scaling, more vulnerabilities Slim base, multi-stage builds
Ignoring signals Slow, unclean shutdowns Handle SIGTERM; use an init such as tini if needed

How to introduce a container build standard in your organization

A short, shared standard prevents months of inconsistent images.

flowchart LR
  A[Pick approved base images] --> B[Create a Dockerfile template<br/>per language]
  B --> C[CI pipeline:<br/>build, scan, sign, push]
  C --> D[Policy: only signed,<br/>scanned images deploy]
  D --> E[Review monthly:<br/>image sizes, findings, age]
  style C fill:#6366f1,color:#fff
  1. Approve a small set of base images (for example one per language) that the platform team patches regularly.
  2. Provide Dockerfile templates with multi-stage builds, a non-root user and a health check, so teams copy a good example instead of inventing one.
  3. Standardise the CI job: build, run unit tests, scan, tag with the Git SHA, sign, push to the private registry.
  4. Set acceptance rules: no latest, no root, no high-severity vulnerabilities without an exception, a maximum size target.
  5. Enforce at the cluster edge with an admission policy so only images from your registry, with valid signatures, are admitted.
  6. Track the metrics: average image size, number of images running as root, age of base images, scan findings over time.
  7. Rebuild on a schedule (weekly) so base-image security patches reach production even when application code is unchanged.

Pitfalls

  • Standards documented but not enforced, so they decay.
  • Banning everything at once; phase rules in with audit mode first.
  • No path for exceptions, which pushes teams to work around the rules.

Interview questions

Q1. What is the difference between a container and a virtual machine? A VM includes its own guest operating system on virtualised hardware. A container is an isolated process that shares the host kernel, using namespaces and cgroups. Containers are lighter and start faster, but isolate less strongly than VMs.

Q2. Why are multi-stage builds useful? They compile and package in an environment with build tools, then copy only the final artefacts into a small runtime image. The result is smaller, faster to pull and has a smaller attack surface.

Q3. Why is :latest dangerous in production? It is a movable tag, so the image behind it can change between deployments, making releases unpredictable and rollbacks unreliable. Use immutable version tags or digests.

Q4. How do image layers affect build speed? Unchanged layers are cached. Placing rarely changing steps (installing dependencies) before frequently changing ones (copying code) lets rebuilds reuse most layers.

Q5. How do you keep secrets out of images? Never copy them in or bake them into build arguments. Supply them at runtime through Kubernetes Secrets, a secret manager or mounted files, and scan images for accidental secrets.

Q6. How would you reduce an image from 1 GB to under 100 MB? Use a slim or distroless base, multi-stage builds, remove build dependencies and caches, copy only needed files with .dockerignore, and avoid installing full toolchains in the runtime stage.

Next: Your First Cluster and kubectl puts these images to work.