Skip to the content.
Lesson 3 50 min Docker Containers Security

Containerisation

Package applications into portable, reproducible containers using Docker — from image fundamentals to multi-stage builds, layer caching, and security hardening.

This lesson is private to enrolled students. Please keep the link to yourself — thanks.

What You Will Learn

  • Understand container internals: namespaces, cgroups, and the OCI image spec
  • Write production-quality Dockerfiles with multi-stage builds
  • Optimise image size and layer caching for fast CI pipelines
  • Scan images for vulnerabilities and harden container security
  • Push and manage images in a container registry

1. How Containers Work

Containers are not VMs. They are isolated processes sharing the host kernel, implemented using two Linux primitives:

Namespaces — isolation

Namespace What it isolates
pid Process IDs — container sees PID 1
net Network interfaces and routing
mnt Filesystem mount points
uts Hostname and domain name
ipc IPC resources (shared memory, queues)
user UID/GID mappings (rootless mode)

cgroups — resource limits

# Limit container to 512 MB RAM and 1 CPU
docker run --memory 512m --cpus 1 my-app:v1
📌
Key insight When Kubernetes sets resources.limits in a Pod spec, it translates directly to cgroup limits on the host. Understanding this connection helps you debug OOMKilled pods.

2. Writing Production Dockerfiles

The naive approach (don’t do this)

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y nodejs npm
COPY . /app
WORKDIR /app
RUN npm install
CMD ["node", "server.js"]

Problems: 800+ MB image, npm devDependencies included, runs as root.

Multi-stage build (do this instead)

# ── Stage 1: build ───────────────────────────────────────────
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production   # only prod deps, no devDeps

COPY . .
RUN npm run build

# ── Stage 2: runtime ─────────────────────────────────────────
FROM node:20-alpine AS runtime
WORKDIR /app

# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Copy only built artefacts from stage 1
COPY --from=build --chown=appuser:appgroup /app/dist ./dist
COPY --from=build --chown=appuser:appgroup /app/node_modules ./node_modules

USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]

Result: ~120 MB image, non-root user, no build tools in production.


3. Layer Caching — Speed Up Your CI

Docker caches each layer. Layers only rebuild when their content changes or a layer above them changes.

# BAD — code changes bust the npm install cache
COPY . .
RUN npm ci

# GOOD — package files rarely change; code changes don't bust install cache
COPY package*.json ./
RUN npm ci
COPY . .
💡
Cache ordering rule Always copy dependency manifests (package.json, go.mod, requirements.txt) before copying application source. Dependency installs are expensive; source copies are cheap.

4. Security Hardening

Image scanning

# Scan with Trivy (open source, used in most CI pipelines)
trivy image my-app:v1.2.0

# Fail CI if HIGH or CRITICAL vulns found
trivy image --exit-code 1 --severity HIGH,CRITICAL my-app:v1.2.0

Minimal base images

Base image Size Use when
alpine ~5 MB Shells, CLIs, minimal runtimes
distroless ~2 MB Production Go/Java/Python apps
scratch 0 MB Statically compiled Go binaries
ubuntu:22.04 ~70 MB When you need package manager

Non-root is non-negotiable

# Always run as a non-root user
RUN useradd -r -u 1001 appuser
USER 1001

Kubernetes admission controllers (like OPA Gatekeeper) will reject pods running as UID 0 in most production environments.


5. Hands-on Exercise

  1. Take an existing application Dockerfile and rewrite it as a multi-stage build
  2. Measure the image size before and after: docker images | grep my-app
  3. Run trivy image on the original and new images — compare the vulnerability count
  4. Add a .dockerignore file to exclude node_modules, .git, and test files
  5. Push your optimised image to GitHub Container Registry (GHCR)

Summary

Concept Key takeaway
Namespaces Process isolation without a separate kernel
cgroups CPU and memory limits enforced by the host kernel
Multi-stage builds Build artefacts stay out of production images
Layer caching Order COPY statements from least to most frequently changing
Non-root Always define a non-root USER — most prod clusters require it

Discussion & Questions

Ask questions, share what you built, or leave feedback about this lesson. GitHub account required.