Back
2/4

Security & Best Practices

+20 XP on completion

#Security & Best Practices

After this lesson you'll know:

  • why running as root in a container is dangerous
  • how to run containers with minimal privileges
  • what secrets are and how to use them safely
  • how resource limits prevent DoS from within a container
  • how to scan images for vulnerabilities with Docker Scout
  • what distroless images are and why they matter

#Don't run as root

⚠️ Container root is dangerous

Containers run as root by default. An attacker who breaks into the container has root privileges on the host.

# Better: Create your own user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# Everything now runs as appuser

#Reduce capabilities

💡 Principle of least privilege

Linux capabilities are permissions for specific system calls. Docker gives containers many by default. You don't need most — and every open capability is an attack vector.

# Only allow the capability you actually need: binding to ports under 1024
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE nginx
💡 How to test

Even with --cap-drop ALL most containers run fine. If something breaks (e.g. ping), add specific capabilities instead of allowing everything.

#No secrets in the image!

🚫 Secrets in images are a security risk

Once baked into the image, anyone with registry access can extract the secret — docker history shows it in plain text.

# WRONG — API key is baked into the image forever
ENV API_KEY=sk-12345

# RIGHT — pass at runtime
docker run -e API_KEY=sk-12345 my-app
# Or with Docker Secrets (recommended for Swarm)
docker run --secret id=api-key my-app

#Read-Only Root Filesystem

💡 Read-only = more secure

A read-only filesystem prevents attackers from persisting malware. Temporary data goes to RAM (--tmpfs).

docker run --read-only --tmpfs /tmp nginx
# The container can't write to the filesystem (except /tmp)

#Resource Limits as a Security Measure

⚠️ Containers without limits = a risk to the whole host

A bug or attack that eats 100% CPU — without limits, every other container suffers. This is a denial of service from within.

# Limit memory (most important DoS protection)
docker run --memory=256m nginx

# Limit CPU
docker run --cpus=1.5 nginx

# Auto-restart on crashes
docker run --restart=unless-stopped nginx

# All together — secure setup
docker run -d \
  --name web \
  --memory=512m \
  --cpus=1 \
  --restart=unless-stopped \
  nginx

#Supply Chain Security — Docker Scout & Distroless Images

Even if you set USER, minimize capabilities, and protect secrets — the image you are using might be insecure from the start.

⚠️ Images from Docker Hub are not automatically safe

An nginx:latest from Docker Hub can contain hundreds of known vulnerabilities (CVEs). docker pull just downloads them all.

# SCAN: Check your image for vulnerabilities (Docker Scout)
docker scout quickview nginx
# → Shows: Critical, High, Medium, Low CVEs
# → nginx:latest often has 50+ known vulnerabilities

# DETAILS about the vulnerabilities
docker scout cves nginx

# RECOMMENDATIONS — Docker Scout tells you what to do
docker scout recommendations nginx

Docker Scout is built into Docker Desktop and available via CLI since Docker Engine 25+. You only need a Docker Hub account.

What to do with many CVEs? Not every CVE is critical. Focus on CRITICAL and HIGH and check if the vulnerability is actually exploitable. A CVE in an Alpine tool you do not use is less concerning.

#Distroless Images — the lean alternative

💡 Only the app, nothing else

Distroless images contain only your app and its runtime dependencies — no shell, no package manager, no tools. This drastically reduces the attack surface.

# Instead of node:20-alpine (~120 MB, full OS stack)
FROM node:20-alpine

# Better: gcr.io/distroless/nodejs20-debian12 (~40 MB, runtime only)
FROM gcr.io/distroless/nodejs20-debian12
COPY --from=builder /app/dist /app/dist
WORKDIR /app
CMD ["/app/dist/server.js"]

Downsides? No shell (docker exec -it container sh will not work). Debugging is harder — but the container is practically immune to the most common attack classes.

Middle ground: Multi-stage with a lean base image like alpine and setting USER. For most projects, this is the best compromise.

💡 How to choose

- Alpine base (with USER) → Good default for most projects

- Distroless → Highest security, minimal image, no debug shell access

- Scratch → Only for statically linked binaries (Go, Rust)


#✋ Try it out

  • Add USER to your Dockerfile and rebuild. Start the container and check with whoami
  • Start a container with --cap-drop ALL and try ping google.com — why doesn't it work?
  • Start a container with --read-only --tmpfs /tmp and try writing a file to / vs /tmp
  • docker run --memory=64m alpine sh -c "dd if=/dev/zero of=/dev/null bs=1M" — start with 64 MB limit. Check with docker inspect --format='{{.State.OOMKilled}}' if the container was killed
  • docker scout quickview nginx — scan the official nginx image for vulnerabilities

#📌 Summary

  • Always set USER in Dockerfile — root in a container is a security risk
  • Minimize capabilities with --cap-drop ALL
  • Secrets don't belong in images, use environment variables or --secret
  • Resource limits (--memory, --cpus) prevent DoS from individual containers
  • Docker Scout scans images for known vulnerabilities — run it regularly
  • Distroless images minimize the attack surface down to the absolute minimum
← → to navigate