Security & Best Practices
#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
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
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
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!
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
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
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.
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
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.
- 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 ALLand tryping google.com— why doesn't it work? - Start a container with
--read-only --tmpfs /tmpand 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 withdocker inspect --format='{{.State.OOMKilled}}'if the container was killeddocker 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