Support

Lesson 4 of 6 ·11 min read

Container Security

Isolation is real but thinner than it looks. Four habits establish a safer baseline.

Fast answer: A container is not a virtual machine. Containers share the host's Linux kernel, so "it is in Docker" is isolation, not immunity. Four habits establish a safer baseline: do not run as root, use minimal base images, never bake secrets into images, and scan images for known vulnerabilities.

The kernel you share

Back in Lesson 1, sharing the host kernel was the trick that made containers small and fast. Here it returns as the security caveat: a VM gives each guest its own kernel, while containers depend on the host kernel enforcing their isolation. Container escapes are uncommon and the isolation is genuinely useful; it is just thinner than the word "container" makes it sound. That is why the following habits are standard rather than paranoid.

Habit 1: do not run as root

By default, the process inside a container runs as root. If an attacker gets code execution inside your app, they start with the container's highest privileges, right next to that shared kernel. Two Dockerfile lines fix the default:

Dockerfile
RUN useradd -m appuser
USER appuser

Everything after USER appuser runs with ordinary privileges. Your Python API does not need root to listen on a port and talk to a database, so do not hand it root.

Habit 2: minimal base images

Every package in the image is a package that can carry a vulnerability. The fix costs one suffix:

FROM python:3.12         full OS userland, build tools, ~1 GB
FROM python:3.12-slim    what Python needs, a fraction of the size

Less software, fewer CVEs, faster pulls. This is the same instinct as the multi-stage build from Lesson 3: production should contain what production needs and nothing else.

Habit 3: secrets never go in images

The tempting shortcut:

Dockerfile
ENV API_KEY="sk-my-secret-key"    # never do this

An image is a stack of recorded layers, and that key is now part of the record: visible in the image history, present in the registry, shipped to everyone who pulls the image. Rotating it later does not un-ship it.

The rule is the boundary from Lesson 1, now with security stakes: the image is the same everywhere; secrets arrive at runtime.

image  +  runtime config  +  secrets  →  running container

In development that means -e flags or an env file that never enters the build. In production it means the platform's secret store, which adds what environment variables alone cannot: access control, an audit trail, and rotation. The same reasoning as the .env guide, one layer up the stack.

Habit 4: scan what you ship

Your image contains a base OS layer and a pile of dependencies, each aging at its own pace. Scanners (docker scout, Trivy, or your registry's built-in scanning) check the image against known-vulnerability databases and tell you which layer to bump. The pipeline from Lesson 3 has a natural slot for this, between build and push, so a critical CVE fails the build instead of reaching production.

Behind scanning sits the supply-chain question: where did this image come from? FROM python:3.12-slim means trusting whoever published it. Prefer official images, pin versions, and treat a random someuser/python-optimized image with the suspicion you would give a random executable.

The checklist

HabitThreat it bluntsCost
Non-root userCompromised app starts with high privilegesTwo Dockerfile lines
Minimal base imageVulnerabilities in software you never neededOne suffix
Secrets at runtime onlyKeys permanently recorded in image layersDiscipline you already have from .env
Image scanningShipping known CVEs unknowinglyOne pipeline step

None of this requires a security team. It is four defaults, set once, carried by every image you build afterwards.

Try it: review an unsafe Dockerfile

Imagine a Dockerfile that starts with FROM ubuntu, installs Python and build tools, sets ENV API_KEY=..., and never declares USER. Name four changes before opening the answer below.

Review: use a trusted minimal Python base, remove build-only tools with a multi-stage build, inject the key at runtime and rotate the exposed value, and create then switch to an unprivileged user. Scan the finished image in CI.

If the four habits feel unrelated

They all reduce what an attacker receives after finding a flaw: fewer privileges, fewer installed tools, no permanent credentials, and fewer known vulnerable packages. They do not replace host patching, network controls, authentication, logging, or backups; they improve the container's starting position.

Check your understanding

Answer before opening each explanation.

Why does sharing a kernel explain so much container-security advice?

Hint: Ask what ultimately enforces the isolation.

Answer: Containers rely on one host kernel to keep their processes separated. Reducing privileges and installed software limits the opportunities and impact if application or isolation controls fail.

Why is a key still compromised after deleting its ENV line and rebuilding?

Hint: Where have earlier image layers and pulls already travelled?

Answer: The old image and its recorded layer may remain in registries, caches, and other machines. Anyone who obtained the value can still use it. Remove it from future builds, but also revoke and rotate it immediately.

What does scanning in CI add over scanning only after deployment?

Hint: Prevention versus detection.

Answer: CI can stop a release containing a vulnerability above your chosen severity threshold before it reaches production. Post-deployment scanning still matters for newly disclosed CVEs, but it detects exposure after shipment.

What improves when you replace a hand-built Ubuntu base with python:3.12-slim?

Hint: Consider both provenance and unnecessary software.

Answer: You use a recognised official Python image and reduce the OS packages and tools shipped in the final image. Pinning a more specific supported version and scanning it are still required.