Docker Optimization
Audit Dockerfiles and docker-compose files for common mistakes and security vulnerabilities. Based on patterns from hundreds of real-world Docker deployments, CWE-mapped security rules, and industry best practices.
The Job
- •Find Dockerfile(s) and docker-compose files in the project
- •Run Hadolint for static analysis (if available)
- •Audit against security checklist (Critical → High → Medium)
- •Audit against optimization checklist
- •Generate prioritized findings report with CWE references
- •Apply fixes (with user confirmation)
Part 1: Dockerfile Security Audit
Score: 🔴 Critical | 🟠 High | 🟡 Medium | 🟢 Good
SEC-001: Docker Socket Exposure
Severity: 🔴 Critical | CWE: CWE-250
# 🔴 CRITICAL: Full host compromise possible VOLUME ["/var/run/docker.sock"] # Also check for: -v /var/run/docker.sock:/var/run/docker.sock
Impact: Grants full control over host Docker daemon. Complete container escape. Fix: Never mount Docker socket. Use Docker API with TLS authentication if needed.
SEC-002: Running as Root
Severity: 🟠 High | CWE: CWE-250
# 🟠 BAD: No USER instruction (58% of images run as root!) CMD ["node", "server.js"] # 🟢 GOOD: Explicit non-root user RUN adduser -D myuser && chown -R myuser /app USER myuser CMD ["node", "server.js"]
Check: No USER instruction before final CMD/ENTRYPOINT
Note: Some orchestrators (OpenShift) block root containers by default
SEC-003: Secrets in ENV/ARG
Severity: 🔴 Critical | CWE: CWE-538
# 🔴 CRITICAL: Visible in docker history forever
ENV AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
ARG DATABASE_PASSWORD=secret123
# 🟢 GOOD: BuildKit secrets (never persisted)
RUN --mount=type=secret,id=db_password,target=/run/secrets/db_password \
cat /run/secrets/db_password | some-command
Check: ENV/ARG containing: key, secret, password, token, credential, api_key
Warning: ENV vars visible to child processes, logs, and linked containers
Note: ARG values visible in docker history even without default values
SEC-004: Sudo in Dockerfile
Severity: 🟡 Medium | CWE: CWE-250
# 🟡 BAD: Unnecessary privilege escalation RUN sudo apt-get install -y curl # 🟢 GOOD: Run as root before USER, then drop privileges RUN apt-get install -y curl USER appuser
Fix: Run privileged commands before USER instruction, not with sudo
SEC-005: ADD vs COPY
Severity: 🟡 Medium
# 🟡 BAD: ADD has hidden behaviors (auto-extract, remote fetch) ADD https://example.com/file.tar.gz /app/ ADD archive.tar.gz /app/ # 🟢 GOOD: COPY is explicit and predictable COPY archive.tar.gz /app/ RUN tar -xzf /app/archive.tar.gz && rm /app/archive.tar.gz
Check: Any ADD instruction → prefer COPY unless extraction needed
SEC-006: Shell Form vs Exec Form
Severity: 🟡 Medium
# 🟡 BAD: Shell form - no signal handling, extra shell process CMD npm start ENTRYPOINT /app/start.sh # 🟢 GOOD: Exec form - proper PID 1, signal handling CMD ["npm", "start"] ENTRYPOINT ["/app/start.sh"]
Impact: Shell form prevents proper SIGTERM handling, graceful shutdown fails
SEC-007: Missing pipefail
Severity: 🟡 Medium
# 🟡 BAD: Pipe failure ignored RUN curl -s https://example.com/script.sh | bash # 🟢 GOOD: Fail on any pipe component failure SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN curl -s https://example.com/script.sh | bash
Check: RUN with pipes (|) without set -o pipefail or SHELL override
SEC-008: Hardcoded UID
Severity: 🟡 Medium
# 🟡 BAD: Hardcoded UID breaks some orchestrators RUN useradd -u 1000 appuser RUN chown -R 1000:1000 /app # 🟢 GOOD: Flexible UID, write to /tmp for portability RUN adduser -D appuser USER appuser WORKDIR /tmp/app-workspace
Why: OpenShift and some Kubernetes configs use random UIDs. Don't assume UID 1000.
SEC-009: Writable Binaries
Severity: 🟡 Medium
# 🟡 BAD: App user owns executable (can be modified at runtime) COPY --chown=appuser:appuser myapp /app/myapp # 🟢 GOOD: Root owns binary, non-writable (immutable) COPY myapp /app/myapp RUN chmod 555 /app/myapp USER appuser
Why: Prevents runtime code modification attacks. Enforces container immutability.
SEC-010: Secrets "Deleted" in Later Layer
Severity: 🔴 Critical
# 🔴 CRITICAL: Secret still accessible in earlier layer!
COPY credentials.json /app/
RUN configure-app.sh && rm credentials.json
# 🟢 GOOD: Use BuildKit secrets
RUN --mount=type=secret,id=creds,target=/tmp/creds \
configure-app.sh /tmp/creds
Impact: Files removed in later layers can still be extracted from image history.
Part 2: Docker Compose Security Audit
COMPOSE-SEC-001: Privileged Mode
Severity: 🔴 Critical | CWE: CWE-250
# 🔴 CRITICAL: Disables ALL container isolation
services:
app:
privileged: true
Impact: Access to all devices, can load kernel modules, complete host escape
Fix: Remove privileged: true. Use specific capabilities if needed.
COMPOSE-SEC-002: Docker Socket Mount
Severity: 🔴 Critical | CWE: CWE-250
# 🔴 CRITICAL: Full Docker daemon access
services:
app:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
Fix: Remove socket mount. Use Docker API with TLS authentication.
COMPOSE-SEC-003: Seccomp Disabled
Severity: 🟠 High | CWE: CWE-284
# 🟠 BAD: Removes syscall filtering
services:
app:
security_opt:
- seccomp:unconfined
Fix: Remove or use custom seccomp profile: seccomp:./custom-seccomp.json
COMPOSE-SEC-004: Host Network Mode
Severity: 🟠 High | CWE: CWE-250
# 🟠 BAD: Bypasses network isolation
services:
app:
network_mode: host
Impact: Container shares host network stack, can access all host ports Fix: Use bridge network with explicit port mappings
COMPOSE-SEC-005: Dangerous Capabilities
Severity: 🟠 High | CWE: CWE-250
# 🟠 BAD: Overly permissive capabilities
services:
app:
cap_add:
- SYS_ADMIN # Near-root powers
- NET_ADMIN # Network manipulation
- SYS_PTRACE # Process debugging
- ALL # Everything
# 🟢 GOOD: Drop all, add only what's needed
services:
app:
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Only if binding <1024
COMPOSE-SEC-006: Host PID/IPC Namespace
Severity: 🟠 High | CWE: CWE-250
# 🟠 BAD: Shares host process/IPC namespace
services:
app:
pid: host
ipc: host
Impact: Can see/signal all host processes, access shared memory
Fix: Remove pid: host and ipc: host
COMPOSE-SEC-007: Missing no-new-privileges
Severity: 🟡 Medium | CWE: CWE-732
# 🟢 GOOD: Prevent privilege escalation
services:
app:
security_opt:
- no-new-privileges:true
Check: Missing no-new-privileges:true in security_opt
COMPOSE-SEC-008: SELinux/AppArmor Disabled
Severity: 🟡 Medium | CWE: CWE-732
# 🟡 BAD: Disables mandatory access controls
services:
app:
security_opt:
- label:disable
- apparmor:unconfined
COMPOSE-SEC-009: Writable Root Filesystem
Severity: 🟡 Medium
# 🟢 GOOD: Read-only root, explicit writable paths
services:
app:
read_only: true
tmpfs:
- /tmp
- /var/run
volumes:
- app-data:/app/data
Check: Missing read_only: true
Part 3: Optimization Checklist
OPT-001: Base Image Tags
# 🔴 BAD: Moving target, "your build can suddenly break" FROM node:latest # 🟡 BETTER: Pinned version FROM node:20.11.1-alpine3.19 # 🟢 BEST: Immutable digest (prevents tag mutation attacks) FROM node@sha256:abc123...
Why: Tags can change. Digests are immutable. Use tags for dev, digests for prod.
OPT-002: Image Size
# 🟡 BAD: Full OS - "more than 100 vulnerabilities" (Sysdig) FROM ubuntu:22.04 # 🟢 GOOD: Minimal base FROM node:20-alpine # ~180MB FROM python:3.12-slim # ~150MB FROM gcr.io/distroless/static # ~2MB (best for Go/Rust)
Prefer: distroless > alpine > slim > full
OPT-003: Layer Cache Order
# 🔴 BAD: Cache busted on every code change COPY . . RUN npm install # 🟢 GOOD: Dependencies cached separately COPY package*.json ./ RUN npm ci COPY . .
Rule: Order by change frequency. Rarely changed → frequently changed.
OPT-004: Package Manager Flags
# 🟡 BAD: Missing optimization flags
RUN apt-get update && apt-get install -y curl
RUN apk add curl
RUN pip install requests
# 🟢 GOOD: With cache/size optimization
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
RUN apk add --no-cache curl
RUN pip install --no-cache-dir requests
OPT-005: apt-get upgrade
# 🟡 BAD: Non-reproducible, breaks immutability RUN apt-get update && apt-get upgrade -y # 🟢 GOOD: Pin base image version instead FROM debian:12.4-slim
Why: Upgrades are unpredictable. Pin your base image for reproducibility.
OPT-006: Multi-Stage Builds
# 🟢 GOOD: Build tools don't ship to production FROM node:20-alpine AS builder RUN npm ci && npm run build FROM node:20-alpine AS production COPY --from=builder /app/dist ./dist
OPT-007: .dockerignore
Check: No .dockerignore file
Impact: Bloated context, slower builds, potential secret leakage
Risk: Build context can accidentally include credentials, backups, lock files
OPT-008: Health Checks
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD curl -f http://localhost:3000/health || exit 1
OPT-009: ENV/EXPOSE Placement
# 🟡 BAD: ENV early breaks cache for everything after FROM node:20-alpine ENV APP_VERSION=1.0.0 COPY . . RUN npm ci # 🟢 GOOD: ENV/EXPOSE at end (cache optimization) FROM node:20-alpine COPY package*.json ./ RUN npm ci COPY . . ENV APP_VERSION=1.0.0 EXPOSE 3000
Why: Changing ENV invalidates all subsequent layers. Put at end unless needed for build.
OPT-010: Multiple FROM Pitfall
# ⚠️ WARNING: Only LAST FROM is used (unless multi-stage) FROM ubuntu:22.04 FROM node:20-alpine # This is the actual base image!
Check: Multiple FROM without stage names (AS builder) may be a mistake
OPT-011: VOLUME Timing
# 🟡 BAD: Files written before VOLUME are lost at runtime WORKDIR /app/data RUN echo "config" > settings.json VOLUME /app/data # settings.json won't persist! # 🟢 GOOD: Write files after container starts, or use COPY COPY settings.json /app/data/ VOLUME /app/data
Why: VOLUME is runtime, not build time. Pre-existing files are shadowed.
OPT-012: Layer Consolidation
# 🟡 BAD: 4 layers, cleanup doesn't save space
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y wget
RUN apt-get clean
# 🟢 GOOD: 1 layer, cleanup actually works
RUN apt-get update && \
apt-get install -y --no-install-recommends curl wget && \
rm -rf /var/lib/apt/lists/*
OPT-013: OCI Labels
# 🟢 GOOD: Metadata for lifecycle management
LABEL org.opencontainers.image.title="MyApp" \
org.opencontainers.image.version="1.0.0" \
org.opencontainers.image.vendor="MyCompany" \
org.opencontainers.image.source="https://github.com/myorg/myapp"
Why: Helps with image inventory, vulnerability tracking, and compliance.
Report Format
# Docker Security & Optimization Audit ## Summary - 🔴 Critical: X issues (container escape risk) - 🟠 High: X issues (privilege escalation risk) - 🟡 Medium: X issues (best practice violations) - 🟢 Good: X checks passed ## Security Findings ### 🔴 [CRITICAL] Docker Socket Exposed (SEC-001) **File**: docker-compose.yml:12 **CWE**: CWE-250 (Execution with Unnecessary Privileges) **Current**: `- /var/run/docker.sock:/var/run/docker.sock` **Impact**: Complete host compromise possible **Fix**: Remove socket mount, use Docker API with TLS ### 🔴 [CRITICAL] Secret Persisted in Layer (SEC-010) **File**: Dockerfile:15-16 **Current**: `COPY secrets.json ... && rm secrets.json` **Impact**: Secret extractable from image layers **Fix**: Use BuildKit --mount=type=secret ## Optimization Findings ### 🟡 [MEDIUM] Unpinned Base Image (OPT-001) **File**: Dockerfile:1 **Current**: `FROM node:latest` **Fix**: `FROM node:20.11.1-alpine3.19` **Best**: `FROM node@sha256:...` (immutable digest) ## Recommended Actions (Priority Order) 1. 🔴 Remove Docker socket mount 2. 🔴 Fix secret in layer (use BuildKit) 3. 🟠 Disable privileged mode 4. 🟠 Add non-root user 5. 🟡 Pin base image versions
Secure docker-compose.yml Template
version: "3.8"
services:
app:
build:
context: .
dockerfile: Dockerfile
image: myapp:${VERSION:-latest}
# Security hardening
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Only if needed
# Resource limits
deploy:
resources:
limits:
cpus: '1'
memory: 512M
reservations:
memory: 256M
# Networking
networks:
- app-network
ports:
- "3000:3000"
# Writable paths only where needed
tmpfs:
- /tmp:size=100M
volumes:
- app-data:/app/data:rw
# Health check
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
# Environment (no secrets here)
environment:
- NODE_ENV=production
env_file:
- .env # Secrets in .env, not committed
networks:
app-network:
driver: bridge
volumes:
app-data:
Quick Reference: Dockerfile Template
# syntax=docker/dockerfile:1
FROM node:20.11.1-alpine AS builder
WORKDIR /app
# Dependencies first (cache optimization)
COPY package*.json ./
RUN npm ci
# Source code
COPY . .
RUN npm run build
# Production stage
FROM node:20.11.1-alpine AS production
WORKDIR /app
# Non-root user (flexible UID)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Production dependencies only
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
# Built artifacts (root-owned, non-writable for security)
COPY --from=builder /app/dist ./dist
RUN chown -R appuser:appgroup /app && chmod -R 555 /app/dist
USER appuser
# Metadata
LABEL org.opencontainers.image.title="MyApp" \
org.opencontainers.image.version="1.0.0"
# Runtime config at end (cache optimization)
ENV NODE_ENV=production
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
Workflow
- •
Find files
bashfind . -name "Dockerfile*" -o -name "docker-compose*.yml" -o -name "compose*.yml"
- •
Run Hadolint (static analysis)
bashhadolint Dockerfile # Or with Docker: docker run --rm -i hadolint/hadolint < Dockerfile
- •
Run security audit (Critical → High → Medium)
- •
Run optimization audit
- •
Scan for CVEs
bashdocker scout cves myapp:latest trivy image myapp:latest
- •
Apply fixes (with user confirmation)
- •
Verify
bashdocker build -t audit-after . docker images audit-after docker history audit-after
Important Rules
Security (Never Violate)
- •NEVER mount Docker socket
- •NEVER use
privileged: true - •NEVER put secrets in Dockerfile/docker-compose (even if "deleted" later)
- •NEVER run as root in production
- •NEVER disable seccomp/AppArmor without justification
- •NEVER make binaries writable by the app user
Optimization (Best Practice)
- •ALWAYS pin base image versions (digests for production)
- •ALWAYS use minimal base images (distroless > alpine > slim)
- •ALWAYS copy dependency files before source code
- •ALWAYS use exec form for CMD/ENTRYPOINT
- •ALWAYS include .dockerignore
- •ALWAYS use
--no-install-recommends/--no-cache/--no-cache-dir - •ALWAYS put ENV/EXPOSE at end unless needed for build
- •ALWAYS add OCI labels for image metadata
Standard .dockerignore
# Git .git .gitignore # Dependencies (reinstalled in container) node_modules vendor .venv __pycache__ *.pyc # Build artifacts dist build coverage .nyc_output # IDE/Editor .vscode .idea *.swp *.swo # Environment/Secrets (CRITICAL) .env .env.* *.pem *.key credentials.json secrets/ # Documentation *.md docs LICENSE # Tests (unless needed) tests __tests__ *.test.js *.spec.js # Docker (don't include in context) Dockerfile* docker-compose* .dockerignore # CI/CD .github .gitlab-ci.yml .circleci # Backups and temp files *.bak *.tmp *.log *~
Size Targets
| Stack | Target | Base Image |
|---|---|---|
| Node.js | < 200MB | node:XX-alpine |
| Python | < 300MB | python:XX-slim |
| Go | < 20MB | distroless/static |
| Rust | < 20MB | distroless/cc |
| Java | < 300MB | eclipse-temurin:XX-jre-alpine |
| Bun | < 200MB | oven/bun:XX-slim |