█
LastWrite
  • > Curriculum
  • > Pricing
  • > For Educators
  • > About
  • > Contact
Log InGet Started

Questions, concerns, bug reports, or suggestions? We read every message, write to us at [email protected].

More ways to reach us →
LastWrite

Structured computer science lessons for aspiring developers and security professionals.

[email protected]

(201) 785-7951

Mon–Fri, 9 AM–5 PM EST

Learn

  • Curriculum
  • Pricing

Company

  • About
  • For Educators & Schools
  • Contact Us

Legal

  • Terms of Service
  • Privacy Policy
© 2026 LastWrite. All rights reserved.
Curriculum/DevOps and Infrastructure/Docker and Containerization/Container Security
40 minIntermediate

Container Security

After this lesson, you will be able to: Harden a container for production: run as a non-root user, use a read-only filesystem and dropped capabilities, keep secrets out of images, and scan for vulnerabilities.

A container is not a security boundary by default. The defaults (root user, writable filesystem, all Linux capabilities, secrets baked into layers) are convenient and dangerous. This lesson is the set of changes that turn a working image into one you are comfortable running in production, where a compromised container should not become a compromised host.

Prerequisites:Optimizing Docker Images

Why the defaults are dangerous

By default a container runs as root, and that root is (largely) the host's root. If an attacker breaks out of a process running as root in the container, they are far closer to owning the host. The image filesystem is writable, so a compromised process can drop tooling and persist. The container holds a broad set of Linux capabilities it almost never needs. And secrets copied into the image live forever in its layers, readable by anyone who pulls it. Each of these has a one-line fix.

Run as non-root, read-only, with dropped capabilities

The four changes that cover most of the risk.

tsx
# In the Dockerfile: create and switch to a non-root user
FROM node:20-slim
RUN useradd --system --uid 1001 appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
RUN npm ci --omit=dev
USER appuser # everything after this runs as appuser, not root
CMD ["node", "server.js"]
# At run time (or in Compose), lock it down further:
# docker run --read-only \ # filesystem is read-only
# --tmpfs /tmp \ # except an explicit writable temp
# --cap-drop ALL \ # drop all Linux capabilities
# --security-opt no-new-privileges \
# myimage:1.2.3

💡 Secrets never belong in an image

`ENV API_KEY=...` or `COPY .env .` bakes the secret into a layer that anyone with the image can read with `docker history` or by extracting the layer, even if a later layer deletes the file. Instead: inject secrets at run time as environment variables from your orchestrator (Compose `env_file`, Kubernetes Secrets, your cloud's secret store), or use Docker BuildKit's `--mount=type=secret` for build-time secrets that never land in a layer. Add a `.dockerignore` with `.env` and `.git` so they are never sent to the build context in the first place.

Scan images before you ship them

Catch known CVEs in your base image and dependencies as part of the build.

tsx
# Trivy: scan a built image for known vulnerabilities
trivy image myimage:1.2.3
# Fail CI on High/Critical findings:
trivy image --exit-code 1 --severity HIGH,CRITICAL myimage:1.2.3
# Docker Scout (built into Docker Desktop / CLI):
docker scout cves myimage:1.2.3
# Smaller base = smaller attack surface:
# node:20-slim or distroless instead of node:20
# alpine where the libc difference is acceptable
# Fewer packages in the image means fewer CVEs to patch.

Common mistakes only experienced engineers catch

Shipping images that run as root because it 'just works.' Add a USER line; most apps need no root at all. Baking secrets into the image with ENV or COPY, then assuming a later `RUN rm` removed them. Layers keep the original. Never scanning, so a known-critical CVE in the base image rides to production. Scan in CI and fail the build. Using a giant base image (the full `node:20`) when slim or distroless would cut the attack surface and image size dramatically. Forgetting `.dockerignore`, so `.env`, `.git`, and local junk get copied into the build context (and sometimes the image).

Quick Check

You set ENV STRIPE_KEY=sk_live_... in your Dockerfile and delete it in a later RUN. Is the key safe?

Pick the correct answer.

Sign in and purchase access to unlock this lesson.

Sign in to purchase
←Running Real Applications in Docker
Back to Docker and Containerization
Passion Project: Deploy a Containerized App→