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 id
Output
uid=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 id
Output
uid=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:secure

no-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: 200
docker compose exec api touch /app/hacked
Output
touch: 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
Output
    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.2
trivy image --severity HIGH,CRITICAL --exit-code 1 ghcr.io/stackcone/api:1.4.2
Output
ghcr.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 host and --pid host: remove network or process isolation.
  • Bind-mounting sensitive host paths like /, /etc or 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 --privileged and 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.

Up next · Lesson 10Build, Push and Deploy with CIAutomate Docker builds with GitHub Actions: build multi-platform images, tag by version and commit SHA, cache layers, scan, push to GHCR and deploy.