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.
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.
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
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
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 (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 (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 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
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
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 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
- 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
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
- 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.
Container security is not automatic—it requires deliberate configuration. Apply these best practices to harden your Docker deployments and protect against container escapes.