Docker and Container Security: A Practical Guide to Securing Your Containers

Docker changed how applications are deployed — but it also introduced a new attack surface that many organizations completely ignore. Containers are not VMs. They share the host kernel. A misconfigured container can lead to a complete host takeover in seconds. This guide covers container security from the ground up.

Common Container Security Mistakes

Mistake 1: Running as Root

# BAD: Default Docker behavior runs as root
docker run ubuntu id
# Output: uid=0(root) gid=0(root) groups=0(root)

# GOOD: Run as non-root user
# In Dockerfile:
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser

# Or specify at runtime:
docker run --user 1000:1000 ubuntu id

# Check which containers are running as root
docker ps -q | xargs -I{} docker inspect {} --format '{{.Id}} {{.Config.User}}' | grep -v ":"

Mistake 2: Mounting Docker Socket

# EXTREMELY DANGEROUS: Mounting Docker socket gives container root on the host
# Never do this:
docker run -v /var/run/docker.sock:/var/run/docker.sock myimage

# Why it's dangerous: from inside the container with docker.sock:
docker run -v /:/host --privileged ubuntu chroot /host /bin/bash
# You now have root access to the entire HOST filesystem

# Check for exposed Docker sockets in running containers
docker ps -q | xargs -I{} docker inspect {} --format '{{.Id}} {{range .Mounts}}{{.Source}} {{end}}' | grep docker.sock

Mistake 3: Privileged Mode

# BAD: Privileged mode removes all security boundaries
docker run --privileged myimage

# Inside a privileged container, full host escape:
mkdir /tmp/host && mount /dev/sda1 /tmp/host
# You can now read/write the host filesystem

# Audit containers running in privileged mode:
docker ps -q | xargs -I{} docker inspect {} --format '{{.Id}} {{.HostConfig.Privileged}}' | grep "true"

Scanning Container Images for Vulnerabilities

# Trivy: Fast, free container security scanner
# Install
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin

# Scan a container image
trivy image nginx:latest
trivy image --severity HIGH,CRITICAL python:3.9

# Scan local Docker image before pushing
docker build -t myapp:latest .
trivy image myapp:latest

# Scan a filesystem
trivy fs /path/to/project/

# Generate SBOM
trivy image --format cyclonedx --output sbom.json nginx:latest

# Scan Kubernetes cluster
trivy k8s cluster --report summary

# Grype: Another excellent free scanner
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
grype nginx:latest
grype dir:/path/to/project/

Docker Bench Security

# Docker Bench: Automated CIS Docker Benchmark checker
# Checks your Docker daemon and container configurations
docker run --net host --pid host --userns host --cap-add audit_control     -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST     -v /etc:/etc:ro     -v /var/lib:/var/lib:ro     -v /var/run/docker.sock:/var/run/docker.sock:ro     --label docker_bench_security     docker/docker-bench-security

# Key checks it performs:
# - Docker daemon configuration
# - Running container settings
# - Docker security operations
# - Container images and build files

Secure Dockerfile Best Practices

# Secure Dockerfile example
FROM python:3.11-slim  # Use specific version, never :latest

# Install security updates
RUN apt-get update && apt-get upgrade -y &&     apt-get install -y --no-install-recommends ca-certificates &&     rm -rf /var/lib/apt/lists/*  # Clean up to reduce attack surface

# Create non-root user
RUN groupadd -r appuser -g 1001 &&     useradd -r -u 1001 -g appuser appuser

WORKDIR /app

# Copy only what is needed (use .dockerignore)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY --chown=appuser:appuser . .

# Drop all capabilities
# Use only what you need in docker run --cap-add
RUN setcap -r /app/binary || true

# Switch to non-root
USER appuser

# Mark filesystem as read-only
VOLUME ["/tmp"]
# Run with: docker run --read-only --tmpfs /tmp myapp

# Health check
HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:8080/health || exit 1

EXPOSE 8080
CMD ["python", "app.py"]
# .dockerignore file (prevent secrets from being included)
.git
.env
*.key
*.pem
secrets/
credentials/
.aws/
*.log
node_modules/
__pycache__/
*.pyc

Docker Daemon Security Configuration

# /etc/docker/daemon.json
{
  "icc": false,           // Disable inter-container communication by default
  "no-new-privileges": true,  // Prevent privilege escalation
  "userns-remap": "default",  // Enable user namespace remapping
  "live-restore": true,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "storage-driver": "overlay2",
  "selinux-enabled": true,  // Enable SELinux (RHEL/CentOS)
  "seccomp-profile": "/etc/docker/seccomp-default.json"
}

# Restart Docker after config change
sudo systemctl restart docker

# Run containers with security constraints
docker run   --read-only   --tmpfs /tmp   --cap-drop ALL   --cap-add NET_BIND_SERVICE   --security-opt no-new-privileges=true   --security-opt seccomp=default   --memory 512m   --cpu-shares 512   --pids-limit 100   myapp:latest

Kubernetes Security Basics

# Pod Security Standards (replaces deprecated PodSecurityPolicy)
# Apply restricted policy to namespace
kubectl label namespace myapp pod-security.kubernetes.io/enforce=restricted

# Secure Pod specification
apiVersion: v1
kind: Pod
metadata:
  name: secure-pod
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1001
    fsGroup: 2000
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: myapp
    image: myapp:1.0.0
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop:
        - ALL
    resources:
      limits:
        memory: "128Mi"
        cpu: "500m"

# Scan Kubernetes for misconfigurations with kube-bench
docker run --rm -v $(which kubectl):/usr/local/mount-from-host/bin/kubectl aquasec/kube-bench:latest

# Check for exposed secrets in cluster
kubectl get secrets --all-namespaces -o json | grep -i password

Container security requires a shift-left mentality — scan images during CI/CD before they ever reach production. A vulnerability found during development costs minutes to fix. The same vulnerability found after a breach costs millions. Tools like Trivy integrate seamlessly into GitHub Actions, GitLab CI, and Jenkins pipelines, making automated container security scanning a zero-friction addition to any deployment workflow.