Docker Security Best Practices

Container security is critical for production deployments. This guide covers image scanning, user namespaces, seccomp profiles, AppArmor/SELinux, rootless Docker, and comprehensive container hardening techniques.

Image Security User Namespaces seccomp AppArmor
Container Security Principles

Containers share the host kernel, making security different from virtual machines. While containers provide good isolation, they are not a security boundary. Following security best practices is essential to prevent container escapes and protect your infrastructure.

The key principles of container security include: running as non-root, using minimal base images, applying least privilege, scanning images for vulnerabilities, implementing runtime security with seccomp and AppArmor, and keeping the Docker daemon secure.

Docker security is a shared responsibility. Docker provides isolation features, but you must configure them properly and keep your images and runtime up to date.
Image Security: Scan, Trust, and Harden

Security starts with the images you run. Vulnerable base images are the most common source of container security issues. Always use official or trusted images, pin to specific versions (never `:latest`), and scan images for known vulnerabilities.

Image scanning tools: Docker Scout (built-in), Trivy (open source), Clair, Snyk, and Grype. Integrate scanning into your CI/CD pipeline to block vulnerable images from reaching production.

# Docker Scout (built into Docker) docker scout quickview nginx:latest docker scout cves nginx:latest docker scout recommendations nginx:latest # Trivy scanning trivy image python:3.11-slim trivy image --severity CRITICAL --ignore-unfixed nginx:latest # Integrate scanning in CI (GitHub Actions) - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: image-ref: ${{ github.repository }}:latest format: 'sarif' output: 'trivy-results.sarif' # Scan Dockerfile for best practices docker scout recommendations --only-severity critical Dockerfile
Always use specific image tags, never `:latest`. Example: `node:18.17.0-alpine3.18` provides reproducibility and prevents unexpected updates.
Run as Non-Root User (Principle of Least Privilege)

By default, containers run as root. This is a major security risk—if an attacker compromises your container, they have root access. Always create and switch to a non-root user in your Dockerfile.

# Dockerfile with non-root user FROM node:18-alpine # Create non-root user (Alpine syntax) RUN addgroup -g 1001 -S nodejs && \ adduser -S nodejs -u 1001 WORKDIR /app # Change ownership of app files COPY --chown=nodejs:nodejs . . # Switch to non-root user USER nodejs CMD ["node", "server.js"] # Or at runtime with --user flag docker run --user 1000:1000 myapp # Check current user inside container docker exec myapp whoami
Running as non-root is one of the most effective security measures. Many official images now have non-root variants (e.g., `nginx:alpine` runs as `nginx` user).
Minimal Base Images: Smaller Attack Surface

Smaller images have fewer packages, fewer vulnerabilities, and a smaller attack surface. Choose minimal base images over full OS images.

  • Alpine Linux (5MB) - Excellent for most applications, but uses musl libc (may have compatibility issues).
  • Distroless (2-20MB) - Contains only your application and runtime dependencies. No shell, no package manager. Very secure but harder to debug.
  • Slim variants (40-100MB) - Debian-based images with only essential packages. Good balance of size and compatibility.
# Alpine (smallest) FROM node:18-alpine # Distroless (most secure, no shell) FROM gcr.io/distroless/nodejs18-debian11 # Slim (good balance) FROM python:3.11-slim # Remove package manager after installation RUN apt-get update && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/* \ && apt-get clean
seccomp: System Call Filtering

seccomp (secure computing mode) restricts the system calls a container can make. The default Docker seccomp profile blocks about 44 dangerous syscalls. You can apply custom profiles for stricter security or to enable specific functionality.

Custom seccomp profiles are JSON files that define allowed and denied syscalls. They're useful for highly sensitive workloads where you want to minimize the attack surface.

# Check default seccomp profile docker run --rm alpine grep Seccomp /proc/self/status # Run with custom seccomp profile docker run --security-opt seccomp=/path/to/profile.json myapp # Disable seccomp (NOT recommended for production) docker run --security-opt seccomp=unconfined myapp # Example custom profile (allow only essential syscalls) { "defaultAction": "SCMP_ACT_ERRNO", "architectures": ["SCMP_ARCH_X86_64"], "syscalls": [ {"names": ["read", "write", "exit", "exit_group", "openat"], "action": "SCMP_ACT_ALLOW"} ] }
AppArmor and SELinux: Mandatory Access Control

AppArmor (Ubuntu, Debian) and SELinux (CentOS, RHEL, Fedora) provide mandatory access control. They restrict container capabilities at a granular level, including file access, network, and capabilities.

Docker automatically applies a default AppArmor profile. For tighter security, create custom profiles for sensitive workloads. SELinux is enforced by default on Red Hat-based systems.

# Check AppArmor status sudo aa-status docker run --rm alpine cat /proc/self/attr/current # Run with custom AppArmor profile docker run --security-opt apparmor=custom-profile myapp # Disable AppArmor (not recommended) docker run --security-opt apparmor=unconfined myapp # Check SELinux status getenforce sestatus # Run with SELinux context docker run --security-opt label=type:my_container_t myapp
Linux Capabilities: Drop All, Add Only What's Needed

Linux capabilities break down root privileges into small, granular units. By default, Docker drops many capabilities but keeps a set for normal operation. For maximum security, drop all capabilities and add only those your application needs.

# Check default capabilities docker run --rm alpine capsh --print # Drop ALL capabilities (most secure) docker run --cap-drop=ALL myapp # Add only necessary capabilities docker run --cap-drop=ALL --cap-add=NET_ADMIN myapp # Add capabilities for specific use cases docker run --cap-drop=ALL --cap-add=NET_RAW --cap-add=NET_ADMIN ping 8.8.8.8 # Common capability sets: # NET_ADMIN - network configuration # SYS_TIME - change system time # DAC_OVERRIDE - bypass file permissions # SETUID - change user IDs
Most applications need only a few capabilities. Audit your application and drop all unnecessary capabilities.
Resource Limits: Prevent DoS Attacks

Always set resource limits to prevent containers from consuming all host resources (CPU, memory, disk). This protects against denial-of-service, whether accidental or malicious.

# Run with resource limits docker run \ --memory=512m \ --memory-swap=1g \ --cpus=0.5 \ --ulimit nofile=1024:2048 \ --pids-limit=100 \ myapp # In docker-compose.yml services: app: image: myapp deploy: resources: limits: cpus: '0.5' memory: 512M reservations: cpus: '0.25' memory: 256M # Check container stats docker stats myapp
Read-Only Root Filesystem

Making the root filesystem read-only prevents attackers from modifying binaries or writing to the container's writable layer. Any writes must go to a volume or tmpfs.

# Run with read-only root docker run --read-only -v /tmp:/tmp myapp # Docker Compose services: app: image: myapp read_only: true tmpfs: - /tmp - /run volumes: - app-data:/data # Check if filesystem is read-only docker exec myapp touch /test.txt # Permission denied
Rootless Docker: Run Docker Without Root Privileges

Rootless Docker runs the Docker daemon and containers without root privileges on the host. This significantly reduces the impact of a container escape. While it has some limitations, it's recommended for security-sensitive environments.

# Install rootless Docker curl -fsSL https://get.docker.com/rootless | sh # Start rootless Docker service systemctl --user start docker # Check if running rootless docker info | grep -i rootless # Limitations of rootless mode # - No cgroup support (resource limits unavailable) # - No overlay networks # - Some networking limitations # - Performance overhead for certain operations
Network Security: Isolation and Encryption
  • Use custom bridge networks (not default) for isolation.
  • Never expose database ports (5432, 3306) to the host - keep internal.
  • Use network policies in Swarm/Kubernetes to restrict traffic.
  • Enable encryption on overlay networks for multi-host communication.
  • Use TLS for Docker daemon socket (default is Unix socket only).
  • Don't expose Docker socket to containers unless absolutely necessary.
# Create isolated network for sensitive services docker network create -d bridge --internal secure-net # Run container on internal network (no external access) docker run --network secure-net --name db postgres # Run another container on same network docker run --network secure-net --name app myapp # Encrypted overlay network in Swarm docker network create -d overlay --opt encrypted secure-overlay
Secrets Management: Never in Environment Variables or Images

Never hardcode secrets in images, Dockerfiles, or environment variables (they're visible in logs and inspect). Use Docker secrets (Swarm) or external secret managers (Hashicorp Vault, AWS Secrets Manager).

# Docker Swarm secrets echo "mysecretpassword" | docker secret create db_password - docker service create --secret db_password --name db postgres # Docker Compose with secrets (Swarm) secrets: db_password: external: true services: db: secrets: - db_password environment: POSTGRES_PASSWORD_FILE: /run/secrets/db_password # External secret manager (Vault example) docker run -e VAULT_TOKEN=$VAULT_TOKEN vault kv get secret/database
Docker Security Checklist
  • Run containers as non-root user (USER directive in Dockerfile).
  • Use minimal base images (Alpine, Distroless, slim variants).
  • Scan images for vulnerabilities (Trivy, Docker Scout).
  • Set resource limits (memory, CPU, pids).
  • Drop all capabilities, add only what's needed.
  • Use read-only root filesystem.
  • Enable seccomp and AppArmor/SELinux profiles.
  • Never expose Docker socket to containers.
  • Use secrets for sensitive data (not env vars).
  • Keep Docker and images updated.
  • Use custom bridge networks (not default).
  • Enable Docker daemon TLS for remote access.
  • Audit container logs and monitor for anomalies.
Frequently Asked Questions
Is Docker secure by default?
Docker provides reasonable defaults but not production-hardened security. You must configure additional protections: non-root users, read-only filesystems, resource limits, and seccomp/AppArmor profiles.
What is a container escape?
A container escape is when an attacker breaks out of the container isolation to access the host system. This can happen through kernel vulnerabilities, misconfigured capabilities, or exposed Docker sockets.
Should I use rootless Docker in production?
Rootless Docker is more secure but has limitations (no cgroup, overlay network limitations). For most production workloads, properly configured rootful Docker with security best practices is sufficient. Rootless is great for development and high-security environments.
How often should I update base images?
Weekly for production. Use automated builds with Dependabot to monitor base image vulnerabilities. Rebuild your images to pick up security patches.
What are the most dangerous capabilities?
CAP_SYS_ADMIN (mounting filesystems), CAP_SYS_PTRACE (debugging processes), CAP_NET_ADMIN (network configuration), CAP_DAC_OVERRIDE (bypass file permissions). Drop these unless absolutely needed.
Should I run Docker daemon as root?
The Docker daemon requires root privileges to manage containers. Rootless Docker runs the daemon without root, but has limitations. For rootful Docker, ensure the daemon is properly secured and access is restricted.
How do I audit container security?
Use tools like Docker Bench Security (docker-bench-security), Falco (runtime security), and Trivy (vulnerability scanning). Regularly scan images and monitor runtime behavior.
Is Alpine Linux safe for production?
Yes, Alpine is widely used in production. However, it uses musl libc instead of glibc, which can cause compatibility issues with some software. Always test your application with Alpine before deploying.
Previous: Swarm vs Kubernetes Next: Kubernetes Basics

Container security is not automatic—it requires deliberate configuration. Apply these best practices to harden your Docker deployments and protect against container escapes.