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:
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:
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
| Habit | Threat it blunts | Cost |
|---|---|---|
| Non-root user | Compromised app starts with high privileges | Two Dockerfile lines |
| Minimal base image | Vulnerabilities in software you never needed | One suffix |
| Secrets at runtime only | Keys permanently recorded in image layers | Discipline you already have from .env |
| Image scanning | Shipping known CVEs unknowingly | One 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.