Docker · Lesson 9 of 11
Container Security Best Practices
Harden Docker containers: run as non-root, drop Linux capabilities, use read-only filesystems, scan images for CVEs and protect the Docker socket.
- Advanced
- 16 min read
- 4 objectives
Before this lessonLesson 8: Debugging and Inspecting Containers
What you will learn
- Run containers as a non-root user
- Reduce privileges with capabilities, read-only filesystems and limits
- Scan images for known vulnerabilities
- Avoid the most dangerous Docker configurations
Your Progress
0 of 11 lessons 0%
- Lessons0 / 11
- Completed0
- Est. time left~ 3 hours
Create a free account to keep your progress on every device.
Tip: pressing Next marks this lesson complete automatically.
Containers isolate processes, but they are not virtual machines. Every container shares the host's Linux kernel, and by default the main process runs as root. If an attacker finds a bug in your app, the defaults decide how much damage they can do next: read secrets, write to the filesystem, or in the worst configurations take over the whole host.
Security here is about layers of small, cheap defaults. None of them is hard; together they turn a compromised container from a disaster into a dead end.
Run as a non-root user
Check who your container runs as right now:
docker run --rm python:3.12-slim iduid=0(root) gid=0(root) groups=0(root)
Root inside a container is restricted, but it is still root on files you mount in, and it is the starting point for most container escape exploits. Create a dedicated user in the Dockerfile and switch to it after installing packages:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN groupadd --system app && useradd --system --gid app --no-create-home app
COPY --chown=app:app . .
USER app
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]docker build -t api:secure .
docker run --rm api:secure iduid=999(app) gid=999(app) groups=999(app)
Many images ship a ready user: node in Node images, nonroot in distroless. Non-root processes cannot bind ports below 1024 by default, so listen on 8000 or 8080 and map the port with -p 80:8080 if needed.
Drop capabilities and block privilege escalation
Linux splits root's powers into about 40 capabilities (change file ownership, bind low ports, manage the network and so on). Docker grants a default subset. A typical web app needs none of them:
docker run -d --name api \
--cap-drop ALL \
--security-opt no-new-privileges:true \
-p 8000:8000 api:secureno-new-privileges stops a process from gaining more rights through setuid binaries like sudo. If a specific feature breaks, add back just that capability with --cap-add, for example NET_BIND_SERVICE.
Read-only filesystem and resource limits
If attackers cannot write files, they cannot drop tools or modify your code. Make the root filesystem read-only and give the app a small in-memory /tmp. Resource limits stop one container (or one attack) from starving the host:
services:
api:
image: ghcr.io/stackcone/api:1.4.2
user: "999:999"
read_only: true
tmpfs:
- /tmp
cap_drop: [ALL]
security_opt:
- no-new-privileges:true
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
pids_limit: 200docker compose exec api touch /app/hackedtouch: cannot touch '/app/hacked': Read-only file system
Scan images for known vulnerabilities
Your base image and dependencies contain third-party code with published CVEs. Scanners compare every package in an image against vulnerability databases. Docker Scout is built into the CLI; Trivy and Grype are popular open-source alternatives:
docker scout quickview ghcr.io/stackcone/api:1.4.2 i New version 1.18.3 available (installed version is 1.18.1)
✓ Image stored for indexing
✓ Indexed 214 packages
Target │ ghcr.io/stackcone/api:1.4.2 │ 0C 2H 7M 31L
digest │ 9c1e4b7a2d3f │
Base image │ python:3.12-slim │ 0C 1H 5M 29L
Updated base image │ python:3.13-slim │ 0C 0H 2M 21L
│ │
What's next:
View vulnerabilities → docker scout cves ghcr.io/stackcone/api:1.4.2trivy image --severity HIGH,CRITICAL --exit-code 1 ghcr.io/stackcone/api:1.4.2ghcr.io/stackcone/api:1.4.2 (debian 12.11) Total: 1 (HIGH: 1, CRITICAL: 0) ┌──────────┬────────────────┬──────────┬────────┬───────────────────┬───────────────┐ │ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ ├──────────┼────────────────┼──────────┼────────┼───────────────────┼───────────────┤ │ libssl3 │ CVE-2025-XXXXX │ HIGH │ fixed │ 3.0.16-1~deb12u1 │ 3.0.17-1~deb12u1 │ └──────────┴────────────────┴──────────┴────────┴───────────────────┴───────────────┘
--exit-code 1 makes Trivy fail when it finds matching issues, so you can use it as a CI gate (next lesson). The fix is usually just rebuilding on a fresh base image. Smaller bases (slim, distroless) mean far fewer findings to triage in the first place.
Dangerous flags to avoid
--privileged: disables almost all isolation and gives the container access to host devices. Treat it as running directly on the host as root.-v /var/run/docker.sock:/var/run/docker.sock: anyone who can talk to the Docker socket can start a privileged container and own the host. Only mount it for trusted tools, and never into an internet-facing app.--network hostand--pid host: remove network or process isolation.- Bind-mounting sensitive host paths like
/,/etcor your home directory read-write.
Supply chain: trust what you run
Use official or verified-publisher base images, pin versions (see the images lesson), and rebuild regularly so security patches flow in. For stronger guarantees, sign your images (for example with Sigstore cosign) and generate an SBOM (software bill of materials) during the build with docker buildx build --sbom=true --provenance=true, so you can answer "are we affected?" in minutes when the next big CVE lands.
Recap
- Containers share the host kernel, so defaults matter; do not run your app as root.
- Drop all capabilities, enable
no-new-privileges, and add back only what breaks. - Use a read-only root filesystem with a tmpfs for scratch space, plus CPU, memory and PID limits.
- Scan images with Docker Scout or Trivy and rebuild on fresh base images to pick up fixes.
- Avoid
--privilegedand mounting the Docker socket; membership in the docker group equals root.
# Write your solution here
Finished reading? Mark this lesson complete to track your progress.
