Awesome-omni-skill docker-optimize
Audit and optimize Dockerfiles and docker-compose files for size, security, build speed, and best practices. Triggers on: optimize dockerfile, audit docker, fix dockerfile, docker best practices, docker compose security.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/devops/docker-optimize" ~/.claude/skills/diegosouzapw-awesome-omni-skill-docker-optimize && rm -rf "$T"
skills/devops/docker-optimize/SKILL.mdDocker 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
find . -name "Dockerfile*" -o -name "docker-compose*.yml" -o -name "compose*.yml" -
Run Hadolint (static analysis)
hadolint Dockerfile # Or with Docker: docker run --rm -i hadolint/hadolint < Dockerfile -
Run security audit (Critical → High → Medium)
-
Run optimization audit
-
Scan for CVEs
docker scout cves myapp:latest trivy image myapp:latest -
Apply fixes (with user confirmation)
-
Verify
docker 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 |