- Scan images for vulnerabilities in CI: use Trivy, Grype, or Snyk before pushing to registry
- Never run containers as root — use USER directive and read-only file systems where possible
- Don't store secrets in images — use runtime secret injection via environment variables or mounted files
- Use minimal base images (distroless, alpine, scratch) to reduce attack surface
- Pin all base image versions and rebuild images regularly to pick up security patches
- Scan images in CI with Trivy, Grype, or Snyk; fail builds on HIGH/CRITICAL CVEs
- Never run as root: create non-root user, set file permissions, use --security-opt=no-new-privileges
- Never bake secrets into images — no COPY .env, no ARG for secrets; use runtime injection
- Use minimal base images: Google distroless, alpine, or scratch for Go/Rust static binaries
- Pin base image versions; rebuild weekly with --no-cache to pick up upstream security patches
- Use read-only root filesystem: docker run --read-only --tmpfs /tmp for containers that need write access
- Sign images with cosign/Notary; verify signatures before deploying to production
- Drop all Linux capabilities and add back only what's needed: --cap-drop=ALL --cap-add=NET_BIND_SERVICE
- Use Docker Content Trust (DOCKER_CONTENT_TRUST=1) to enforce signed image pulls
- Limit container resources: --memory, --cpus, --pids-limit to prevent resource exhaustion attacks
- Use secrets management: Docker secrets, Kubernetes secrets, or cloud-native secret stores at runtime
- Enable audit logging for container operations; monitor for suspicious exec/attach commands