devops-roadmap
stage 07 / 16
Intermediate4 lessons · ~4 min read · 2–3 weeks to practise

Containers & Docker

Package once, run the same everywhere

Containers ended 'it works on my machine'. Learn images and Dockerfiles, what a container actually is under the hood, multi-service local stacks, and how to harden images.

0/4 read
On this page · 4 lessons
07.01

Docker fundamentals

Images vs containers, the Dockerfile, layer caching, volumes and ports, and the everyday commands.

Containers ended "it works on my machine." A container packages your app with its exact dependencies so it runs identically on a laptop, in CI, and in production. Unlike a virtual machine, it shares the host's kernel, so it starts in milliseconds and is tiny.

Two concepts to keep straight:

  • An image is the immutable blueprint (your app + libraries + runtime).
  • A container is a running instance of an image.

You build an image from a Dockerfile, a recipe read top to bottom:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]

Each instruction creates a cached layer, so ordering matters: copy package.json and install before copying source, and Docker reuses the dependency layer whenever only your code changes — builds go from minutes to seconds.

Everyday commands: docker build -t app ., docker run -p 8080:3000 app, docker ps, docker logs, docker exec -it <id> sh to poke inside a running container.

07.02

How containers actually work

Namespaces, cgroups and the union filesystem — why a container is just a Linux process, and why that matters.

A container feels like a lightweight VM, but it isn't one — and knowing the difference explains almost every weird container behaviour you'll hit.

A VM virtualises hardware and runs a whole guest kernel. A container is just a normal Linux process on the host, isolated by three kernel features:

  • Namespaces — what the process can see. Separate namespaces for PID (its own process tree, so your app thinks it's PID 1), network (its own interfaces and ports), mount (its own filesystem view), user, and more.
  • cgroups (control groups) — what the process can use. CPU, memory, and I/O limits. This is exactly what Kubernetes resource requests/limits configure.
  • Union filesystem (OverlayFS) — layered, copy-on-write images. Layers are shared between containers, which is why images are small and start instantly.

The consequences matter in practice: containers share the host kernel (so a Linux container can't run on a Windows kernel without a VM, and a kernel exploit is a host compromise — this is why container security matters). And since your app runs as PID 1, it must handle SIGTERM to shut down gracefully, or Kubernetes will hard-kill it.

07.03

Compose & multi-service stacks

Run an app plus a database plus a cache with one command; push to registries with meaningful tags.

Real apps aren't one container — they're an app plus a database plus a cache. Docker Compose describes the whole set in one docker-compose.yml and starts it with a single command.

services:
  web:
    build: .
    ports: ["8080:3000"]
    depends_on: [db]
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: secret

docker compose up brings the entire stack to life on a shared network where services reach each other by name (db:5432). This is how you run realistic local environments.

To share images you push them to a registry — Docker Hub, GitHub Container Registry, or a cloud one (ECR/GCR). Tag meaningfully (app:1.4.2, not just latest) so deployments are reproducible.

Two production must-dos:

  • Multi-stage builds: compile in a fat "builder" image, then copy only the finished artifact into a tiny runtime image — smaller and more secure.
  • Scan images (Trivy) and run as a non-root user; never bake secrets into layers.
07.04

Container & image security

Minimal/distroless bases, non-root users, no secrets in layers, and scanning that fails the build.

A container is not a security boundary by default — it shares the host kernel. Hardening images is a core DevOps responsibility.

Build a minimal image. Every package you don't ship is a vulnerability you don't have. Use multi-stage builds to leave compilers and dev dependencies behind, and prefer a small base — Alpine, or distroless images which contain your app and nothing else (no shell, no package manager, so an attacker who gets in has no tools).

Never run as root. By default containers run as root, and root in the container can be root on the host if it escapes. Always:

RUN adduser -D appuser
USER appuser

Never bake secrets into images. A secret in any layer is permanent and visible with docker history, even if a later layer deletes it. Inject secrets at runtime.

Scan everything, continuously. Trivy (or Grype/Snyk) finds known CVEs in your base image and dependencies. Wire it into CI and fail the build on critical findings. Note that a clean scan today can be vulnerable tomorrow — rescan running images, don't just scan at build.

Then: pin base image versions (not latest), drop Linux capabilities, mount the root filesystem read-only, and sign images so you can verify what you deploy.

practise — build these
  • $Containerise an app with a multi-stage Dockerfile, non-root user, and a final image under 100MB.
  • $Run app + database + cache with Docker Compose, then scan the image with Trivy and fix every critical CVE.