Most beginner Docker setups have serious security problems. Containers running as root. Passwords in Dockerfiles. Outdated base images with known vulnerabilities. No resource limits.

This tutorial covers the most important Docker security practices. You do not need to implement all of them at once. Start with the first three — they will fix the most critical issues.

1. Never Run Containers as Root

By default, processes inside Docker containers run as root (UID 0). If an attacker exploits a vulnerability in your app, they have root access inside the container — and potentially a path to the host.

Fix: create a non-root user in your Dockerfile:

FROM python:3.13-slim

WORKDIR /app

# Install dependencies as root first
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Create a non-root user
RUN groupadd --gid 1001 appgroup && \
    useradd --uid 1001 --gid appgroup --no-create-home appuser

# Copy code with correct ownership
COPY --chown=appuser:appgroup . .

# Switch to non-root user
USER appuser

CMD ["gunicorn", "main:app", "--bind", "0.0.0.0:8080"]

In Docker Compose, you can also set the user:

services:
  api:
    image: myapp:1.0
    user: "1001:1001"

Rule: Every production Dockerfile should have a USER instruction that is not root.

2. Use Read-Only Root Filesystems

If your app does not write to disk, make the container’s root filesystem read-only. Even if an attacker gets code execution, they cannot write malware or modify the application:

docker run --read-only myapp:1.0

In Compose:

services:
  api:
    image: myapp:1.0
    read_only: true
    # If the app needs to write temp files, mount a tmpfs
    tmpfs:
      - /tmp
      - /var/run

Most stateless apps (APIs, web servers) can run read-only. Test your app first — some apps need write access to /tmp or other directories.

3. Use Minimal Base Images

Fewer packages = smaller attack surface. Every package in your image is a potential vulnerability.

Order of security from most to least:

  1. scratch — empty image. No shell, no packages, no OS. Only for Go/Rust static binaries.
  2. gcr.io/distroless/* — Google’s minimal images. No shell, no package manager. No way to exec in if compromised.
  3. alpine:3.21 — tiny Linux (~5 MB). Minimal packages. Has a shell.
  4. debian:bookworm-slim — stripped Debian. Familiar, apt available.
  5. ubuntu:24.04 — full Ubuntu. Many packages pre-installed.

For production, prefer distroless or scratch. See Docker Tutorial #7: Multi-stage Builds for examples.

4. Scan Images for Vulnerabilities

Use Docker Scout (built into Docker Desktop) to scan your images for known CVEs:

# Scan an image for CVEs
docker scout cves myapp:1.0

# Quick summary
docker scout quickview myapp:1.0

# Compare two versions
docker scout compare myapp:1.0 myapp:2.0

Docker Scout is free for personal use with Docker Hub (up to 3 repos). It identifies packages with known CVEs and suggests fixes.

You can also use Trivy (open source, very thorough):

# Install Trivy
brew install aquasecurity/trivy/trivy

# Scan an image
trivy image myapp:1.0

# Scan with only CRITICAL and HIGH severity
trivy image --severity CRITICAL,HIGH myapp:1.0

Run image scanning in your CI/CD pipeline on every build.

5. Never Put Secrets in Dockerfiles or Images

This is a critical mistake that is very common:

# NEVER DO THIS
ENV DATABASE_PASSWORD=supersecret123
ENV API_KEY=sk-1234567890abcdef

# ALSO NEVER DO THIS
COPY .env /app/.env
COPY id_rsa /root/.ssh/id_rsa

Secrets in Dockerfiles or image layers are visible to anyone who can pull the image. Even if you remove them in a later layer, they remain in earlier layers in the image history.

What to do instead:

Pass secrets as environment variables at runtime:

docker run -e DATABASE_PASSWORD=secret myapp:1.0

Use an .env file at runtime (not copied into the image):

docker run --env-file .env myapp:1.0

In Docker Compose, use the env_file: directive with a .env file excluded from git.

For production, use Docker secrets (in Docker Swarm) or a secrets manager (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault).

6. Drop Linux Capabilities

Docker containers run with a subset of Linux capabilities by default. You can further restrict them:

# Drop all capabilities, then add only what's needed
docker run \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  myapp:1.0

NET_BIND_SERVICE allows binding to ports below 1024 (like port 80). Most apps don’t even need this — run on port 8080 and let nginx or a load balancer handle port 80.

In Compose:

services:
  api:
    image: myapp:1.0
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE

7. Prevent Privilege Escalation

Add this security option to prevent processes from gaining more privileges:

docker run --security-opt no-new-privileges myapp:1.0

In Compose:

services:
  api:
    image: myapp:1.0
    security_opt:
      - no-new-privileges:true

This prevents sudo and setuid binaries from elevating privileges inside the container.

8. Set Resource Limits

Without resource limits, a misbehaving container can consume all CPU and memory on the host, affecting other containers:

docker run \
  --memory 512m \
  --cpus 1.0 \
  myapp:1.0

In Compose (note: deploy.resources only applies when using docker stack deploy with Swarm, or docker compose --compatibility; it is ignored by docker compose up):

services:
  api:
    image: myapp:1.0
    deploy:
      resources:
        limits:
          memory: 512m
          cpus: "1.0"
        reservations:
          memory: 256m
          cpus: "0.5"

For local development without Swarm, use mem_limit and cpus directly under the service:

services:
  api:
    image: myapp:1.0
    mem_limit: 512m
    cpus: 1.0

9. Use Specific Image Tags

Never use latest in production:

# Bad: unpredictable
FROM python:latest

# Good: pinned version
FROM python:3.13.3-slim

latest changes whenever a new version is released. Your build may suddenly break because the base image changed. Pin to a specific version for reproducible builds.

For extra security, pin by digest (immutable):

FROM python:3.13.3-slim@sha256:abc123...

10. Rootless Docker

By default, the Docker daemon (dockerd) runs as root. This means that any user who can run Docker commands has effective root access to the host.

Rootless Docker runs the Docker daemon as a regular user with no root privileges:

# Install rootless Docker (Linux only)
dockerd-rootless-setuptool.sh install

# Run Docker commands as your regular user
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
docker run hello-world

Rootless Docker is the most secure way to run Docker Engine on Linux. It is not available on macOS/Windows (Docker Desktop already handles isolation differently there).

11. Docker Hardened Images

Docker Engine v29 introduces Docker Hardened Images (docker dhi) — a CLI plugin that provides pre-hardened versions of popular base images. These images:

  • Have CVEs patched more frequently than official images
  • Come with SLSA v1 provenance attestations
  • Use minimal attack surfaces
# Pull a hardened image
docker pull docker.io/dockerhardened/python:3.13

# Check available hardened images
docker dhi list

This is new in 2026 and particularly useful for teams with strict security requirements.

12. Keep Base Images Updated

Even with pinned versions, you need to update periodically. New CVEs are found every week.

Use Dependabot (GitHub) or Renovate to automatically open PRs when new base image versions are available:

.github/dependabot.yml:

version: 2
updates:
  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"

Renovate does the same thing and has broader platform support.

Security Checklist

Here is a quick checklist for every Docker image you build:

  • Non-root user (USER instruction in Dockerfile)
  • Minimal base image (slim, alpine, distroless, or scratch)
  • No secrets in Dockerfile or image
  • .dockerignore excludes .env and private keys
  • Specific image tag (not latest)
  • HEALTHCHECK defined
  • Read-only filesystem where possible
  • Resource limits set
  • Image scanned with Docker Scout or Trivy
  • --security-opt no-new-privileges in production

Common Mistakes

Running containers as root

This is the #1 Docker security mistake. If you do nothing else from this list, add a non-root USER to every production Dockerfile.

Using latest tag for base images

latest is unpredictable. You do not know what version you are getting. Pin to a specific version like python:3.13.3-slim.

Copying .env files or SSH keys into Docker images

These become part of the image and are visible to anyone who can pull the image. Never copy secrets into an image. Pass them as environment variables at runtime.

What’s Next?

Next: Docker Cheat Sheet 2026 — All Commands in One Page