Skip to content
Home
Docker Security: Best Practices for Container Security

Docker Security: Best Practices for Container Security

7 min read

Docker containers share the host kernel, which means a container breakout could compromise the entire system. While containers provide better isolation than running processes directly on the host, they are not as secure as virtual machines. Understanding this threat model is the foundation of Docker security.

This guide covers Docker security best practices — from image selection to runtime security.

The Docker Security Threat Model

┌──────────────────────────────┐
│        Host OS               │
│  ┌────────────────────────┐  │
│  │  Docker Daemon         │  │
│  │  ┌──┐ ┌──┐ ┌──┐       │  │
│  │  │C1│ │C2│ │C3│       │  │
│  │  └──┘ └──┘ └──┘       │  │
│  │  Shared Kernel         │  │
│  └────────────────────────┘  │
└──────────────────────────────┘
  • Shared kernel — containers use the host kernel
  • Root inside container != root on host (if properly configured)
  • Process isolation via namespaces
  • Resource limits via cgroups

Image Security

Choose Minimal Base Images

# ❌ Large image with many attack surfaces
FROM ubuntu:latest

# ✅ Minimal image
FROM alpine:3.19

# ✅ Distroless — no package manager, no shell
FROM gcr.io/distroless/base

# ✅ Scratch — truly empty
FROM scratch

Pin Specific Tags

# ❌ Vulnerable to tag updates
FROM node:latest
FROM python:3

# ✅ Immutable and reproducible
FROM node:20.11.0-alpine@sha256:abc123def456
FROM python:3.12-slim@sha256:789ghi...

Scan Images for Vulnerabilities

# Using Docker Scout (built into Docker Desktop)
docker scout quickview myimage:tag
docker scout cves myimage:tag

# Using Trivy
trivy image myimage:tag

# Using Snyk
snyk container test myimage:tag

# Using Grype
grype myimage:tag

# Scan in CI/CD
trivy image --severity HIGH,CRITICAL --exit-code 1 myimage:tag

Multi-Stage Builds

# Build stage — includes all tools
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage — minimal runtime
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
CMD ["node", "dist/server.js"]

Running Containers Securely

Least Privilege: Non-Root Users

# Dockerfile — create and use a non-root user
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
CMD ["node", "server.js"]
# Override user at runtime
docker run -u 1000:1000 myimage

Read-Only Filesystem

docker run --read-only --tmpfs /tmp myimage

Makes the container’s filesystem read-only. Use --tmpfs for writable directories the application needs.

Drop Capabilities

# Start with no capabilities, add only what's needed
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myimage

# Alternatively, the secure computing mode
docker run --security-opt seccomp=/path/to/seccomp-profile.json myimage

Resource Limits

# CPU and memory limits — prevent DoS
docker run \
  --memory="512m" \
  --memory-swap="1g" \
  --cpus="0.5" \
  --pids-limit=100 \
  myimage

Secrets Management

Never Hardcode Secrets

# ❌ Bad — secret in image
ENV DB_PASSWORD=supersecret

# ❌ Bad — secret in build args (visible in history)
ARG API_KEY

Use Docker Secrets (Swarm)

# Create a secret
echo "supersecret" | docker secret create db_password -

# Use in a service
docker service create \
  --name app \
  --secret db_password \
  --secret api_key \
  myapp

# Secrets are mounted as files in /run/secrets/

Use BuildKit for Build Secrets

# docker build --secret id=npmrc,src=$HOME/.npmrc .
# syntax=docker/dockerfile:1
FROM node:20-alpine

# Mount secret during build without leaving layers
RUN --mount=type=secret,id=npmrc \
    cp /run/secrets/npmrc .npmrc && \
    npm ci --production

Use Environment with –env-file

# Store in a file with restricted permissions
echo "DB_PASSWORD=supersecret" > .env
chmod 600 .env

# Pass to container
docker run --env-file .env myapp

# Override specific variables
docker run -e DB_PASSWORD=otherpass myapp

Runtime Security

AppArmor and SELinux

# AppArmor
docker run --security-opt apparmor=my-profile myimage

# SELinux
docker run --security-opt label=level:TopSecret myimage

seccomp

# Default seccomp profile blocks ~44 system calls
docker run --security-opt seccomp=default.json myimage

# Custom seccomp profile
docker run --security-opt seccomp=custom-profile.json myimage

Rootless Docker

Run the Docker daemon without root privileges:

# Install rootless Docker
dockerd-rootless-setuptool.sh install

# Run containers without root
docker run -d --name web nginx

Dockerfile Security Checklist

FROM alpine:3.19                      # ✅ Minimal base image
LABEL maintainer="team@example.com"    # ✅ Metadata

RUN addgroup -S app && \
    adduser -S app -G app              # ✅ Non-root user

WORKDIR /app
COPY --chown=app:app . .               # ✅ Proper ownership

RUN apk add --no-cache curl=8.5.0 && \
    npm ci --only=production && \
    npm cache clean --force            # ✅ Pin versions, clean cache

USER app                               # ✅ Drop privileges
EXPOSE 3000                            # ✅ Document ports
HEALTHCHECK --interval=30s CMD curl -f http://localhost:3000/health || exit 1

CMD ["node", "server.js"]

Runtime Security Checklist

  • Containers run as non-root user
  • Read-only root filesystem (--read-only)
  • All capabilities dropped (--cap-drop ALL)
  • Resource limits set (CPU, memory, PIDs)
  • Secrets never in environment variables
  • Images scanned for vulnerabilities
  • No privileged containers
  • No SSH in containers
  • Docker socket not mounted into containers
  • Latest security patches applied

Container Runtime Hardening

Beyond image security, runtime hardening prevents exploits from compromising the host. Use --security-opt=no-new-privileges:true to prevent privilege escalation within the container. Set --ulimit nofile=1024:1024 to limit file descriptors and prevent fork bombs. Configure the container’s /etc/resolv.conf with trusted DNS servers to avoid DNS poisoning. Use --init flag to run an init process (tini or dumb-init) as PID 1, ensuring proper zombie reaping and signal forwarding. For sensitive workloads, enable user namespaces with --userns-remap=default to map container root to a non-privileged host user. Audit container events with docker events --filter 'type=container' --filter 'event=die' and ship logs to a centralized SIEM. Use Falco or Tracee for runtime anomaly detection — these tools monitor syscall patterns and alert on suspicious behavior like reverse shells or unexpected file access.

Compliance and Auditing

For regulated environments, maintain an audit trail of all container activities. Configure Docker daemon with --icc=false to disable inter-container communication on the default bridge. Use docker logs --details to capture container metadata. Export container configuration with docker inspect for compliance reporting. Docker Bench for Security (docker run --net host --pid host --cap-add audit_control -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST -v /var/lib:/var/lib:ro -v /var/run/docker.sock:/var/run/docker.sock docker/docker-bench-security) runs automated CIS benchmark checks against your Docker host and containers. Schedule this tool to run daily in CI or as a cron job.

FAQ

Is Docker secure enough for production?

Yes, when properly configured. Follow the principle of least privilege: run as non-root, drop capabilities, use read-only filesystems, and scan images. For multi-tenant environments, use VMs or Kata Containers for stronger isolation.

What is the most common Docker security mistake?

Running containers as root and not pinning image versions. Root containers give attackers full privileges if they break out. Unpinned versions silently introduce vulnerabilities when upstream tags are updated.

How do I scan images for vulnerabilities?

Use Docker Scout (built-in), Trivy (open source), or Snyk (commercial). Integrate scanning into your CI/CD pipeline with trivy image --severity HIGH,CRITICAL --exit-code 1 myimage to fail builds on critical vulnerabilities.

Should I mount the Docker socket into a container?

Never do this in production. Mounting /var/run/docker.sock gives the container root-level access to the Docker daemon, allowing it to run any container on the host — a complete security bypass.

What is the difference between --cap-drop and --security-opt?

--cap-drop removes Linux capabilities from the container (coarse-grained). --security-opt applies AppArmor, seccomp, or SELinux profiles (fine-grained). Use both for defense in depth.

Related: Check our Docker Networking guide.

Docker Security Fundamentals

Container security operates on multiple layers: kernel, host, container runtime, and application. The Linux kernel’s namespace and cgroup features provide isolation: PID namespaces isolate process visibility, mount namespaces isolate filesystem mounts, and network namespaces provide network stacks. Run containers with least privilege: drop all capabilities with --cap-drop=ALL and add only needed ones like --cap-add=NET_BIND_SERVICE. Use --security-opt=no-new-privileges:true to prevent privilege escalation. Seccomp profiles filter system calls: Docker’s default profile blocks ~44 risky syscalls. AppArmor and SELinux provide mandatory access control. Rootless mode runs the Docker daemon without root privileges. Read-only root filesystem (--read-only) prevents writes to the container filesystem. Enable Content Trust: export DOCKER_CONTENT_TRUST=1 ensures only signed images run.

Vulnerability Management

Image scanning in CI with docker scout identifies CVEs before deployment. Use minimal base images (distroless, Alpine, slim variants) to reduce the attack surface. Regularly update base images with docker pull and rebuild. Secret management with Docker secrets or external vaults avoids hardcoded credentials. Runtime security monitoring with Falco detects anomalous syscall patterns. Integrate scanning into your deployment pipeline with GitHub Actions or GitLab CI — fail the build if any CRITICAL or HIGH severity vulnerability is found. Schedule weekly scans even for stable images, as new CVEs are discovered daily.

#docker#security#container-security#best-practices#scanning