Docker Security Tips

Non-Root Users

# Dockerfile: create and switch to non-root user FROM node:20-alpine # Create app directory and user RUN addgroup -S appgroup && adduser -S appuser -G appgroup WORKDIR /app # Copy and install as root, then switch COPY package*.json ./ RUN npm ci --omit=dev COPY --chown=appuser:appgroup . . # Switch to non-root user USER appuser EXPOSE 3000 CMD ["node", "server.js"] # Run with --user flag (override image USER) docker run --user 1001:1001 myapp # Verify running as non-root docker exec my-container id docker exec my-container whoami

Read-Only Filesystem

# Run with read-only root filesystem docker run \ --read-only \ --tmpfs /tmp:rw,noexec,nosuid \ --tmpfs /run:rw,noexec,nosuid \ myapp # Allow specific directories to be writable docker run \ --read-only \ -v app-logs:/app/logs \ --tmpfs /tmp \ myapp # Dockerfile: avoid writing to filesystem at runtime # Write logs to stdout/stderr (Docker captures them) # Use env vars for config instead of writing config files

Linux Capabilities

# Drop ALL capabilities, add back only what's needed docker run \ --cap-drop=ALL \ --cap-add=NET_BIND_SERVICE \ myapp # Common capabilities and their purpose: # CHOWN - change file ownership # DAC_OVERRIDE - bypass file permission checks # NET_BIND_SERVICE - bind to ports < 1024 # NET_ADMIN - network config (iptables, etc.) # SYS_PTRACE - process tracing (debuggers) # SETUID/SETGID - change user/group IDs # Verify capabilities in container docker run --rm alpine sh -c "apk add libcap && capsh --print" # Check if container runs with elevated privileges docker inspect mycontainer --format='{{json .HostConfig.CapAdd{{"}}"}}'

Seccomp Profiles

# Use default seccomp profile (blocks ~44 syscalls) # Enabled by default in Docker Desktop and most runtimes # Run without seccomp (only for debugging) docker run --security-opt seccomp=unconfined myapp # Apply custom seccomp profile docker run \ --security-opt seccomp=/path/to/profile.json \ myapp # Minimal seccomp profile (deny-all + allowlist) # { # "defaultAction": "SCMP_ACT_ERRNO", # "syscalls": [ # { # "names": ["read", "write", "exit", "exit_group", # "open", "close", "stat", "fstat", # "mmap", "mprotect", "munmap", "brk", # "rt_sigaction", "rt_sigprocmask"], # "action": "SCMP_ACT_ALLOW" # } # ] # } # AppArmor profile (Linux) docker run --security-opt apparmor=docker-default myapp

Image Scanning

# Trivy (open source vulnerability scanner) trivy image nginx:latest trivy image --severity CRITICAL,HIGH myapp:v1.0 # Scan local image trivy image --input myapp.tar # Docker Scout (built into Docker Desktop) docker scout cves nginx:latest docker scout recommendations myapp:latest # Snyk snyk container test myapp:latest snyk container monitor myapp:latest # Grype grype myapp:latest # In CI (fail on critical vulnerabilities) trivy image --exit-code 1 --severity CRITICAL myapp:${TAG}

Secrets Management

# NEVER use ENV or ARG for secrets in Dockerfile # BAD: ARG DB_PASSWORD=secret (visible in image layers) # Docker Swarm secrets echo "mysecret" | docker secret create db_password - docker service create \ --name myapp \ --secret db_password \ myimage # Secret available at /run/secrets/db_password # Docker Compose secrets (v3.1+) services: app: image: myapp secrets: - db_password secrets: db_password: file: ./secrets/db_password.txt # BuildKit secret (not baked into image layer) # Dockerfile: # RUN --mount=type=secret,id=npmrc cat /run/secrets/npmrc # Build with secret: # docker build --secret id=npmrc,src=$HOME/.npmrc .