
Docker Agents Review
- 9 installs
- 9 repo stars
- Updated July 8, 2026
- openaec-foundation/docker-claude-skill-package
Helps with devops & ci/cd tasks.
About
docker-agents-review is a Claude Code skill for devops & ci/cd. It helps solo builders move faster with AI-assisted development.
- docker-agents-review
- DevOps & CI/CD
- AI-coding skill
Docker Agents Review by the numbers
- 9 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,020 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/docker-claude-skill-package --skill docker-agents-reviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 9 |
|---|---|
| repo stars | ★ 9 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/docker-claude-skill-package ↗ |
What it does
Helps with devops & ci/cd tasks.
Files
docker-agents-review
Review Workflow
Execute these checklists in order. Each section is independent -- report all findings, do not stop at the first issue.
Review Order:
1. Dockerfile Validation --> Base image, instructions, layers, signals
2. Compose Validation --> Structure, depends_on, volumes, env
3. Security Audit --> Non-root, capabilities, secrets, scanning
4. Production Readiness --> Health checks, restart, logging, limits
5. Anti-Pattern Scan --> Known mistakes across all areas---
Checklist 1: Dockerfile Validation
1A: Base Image
[ ] Base image uses a specific version tag (NEVER `latest`)
FAIL: FROM node:latest | FROM nginx
PASS: FROM node:20.11-bookworm-slim
[ ] Production stage uses minimal base (alpine, slim, distroless, or scratch)
FAIL: Full OS image in final stage (ubuntu:22.04, debian:bookworm)
[ ] Multi-stage build separates build dependencies from runtime
FAIL: Compiler/SDK present in final image
[ ] Build stage named with AS keyword
FAIL: COPY --from=0 (fragile numeric index)
PASS: FROM golang:1.22 AS build ... COPY --from=build
[ ] For reproducible builds, digest pinning used where required
BEST: FROM alpine:3.21@sha256:abc123...1B: Instructions & Layer Optimization
[ ] syntax directive present at top of Dockerfile
EXPECTED: # syntax=docker/dockerfile:1
[ ] COPY used instead of ADD for local files
FAIL: ADD config.json /app/ (when no extraction needed)
[ ] apt-get update && install combined in single RUN
FAIL: Separate RUN apt-get update and RUN apt-get install
[ ] apt cache cleaned after install
EXPECTED: && rm -rf /var/lib/apt/lists/*
[ ] --no-install-recommends used with apt-get install
[ ] Package versions pinned where determinism required
[ ] Related commands combined with && to minimize layers
FAIL: Separate RUN for each apt-get install package
[ ] WORKDIR used instead of RUN cd
FAIL: RUN cd /app && npm install
[ ] .dockerignore file exists and excludes node_modules, .git, build artifacts
[ ] COPY instruction ordered for cache efficiency
EXPECTED: COPY package*.json first, then RUN npm install, then COPY . .1C: Entrypoint & Signals
[ ] ENTRYPOINT uses exec form (JSON array)
FAIL: ENTRYPOINT /usr/bin/myapp (shell form)
PASS: ENTRYPOINT ["/usr/bin/myapp"]
[ ] CMD uses exec form (JSON array)
FAIL: CMD node server.js
PASS: CMD ["node", "server.js"]
[ ] Entrypoint scripts use exec "$@" to replace shell process
[ ] Application runs as PID 1 for proper signal handling
[ ] Shell form NOT used for ENTRYPOINT (prevents SIGTERM delivery)
[ ] Pipe commands use set -o pipefail
FAIL: RUN wget -O - https://url | wc -l > /count
PASS: RUN set -o pipefail && wget -O - https://url | wc -l > /count1D: Metadata & Documentation
[ ] EXPOSE documents all listening ports
[ ] OCI standard labels present (org.opencontainers.image.*)
[ ] HEALTHCHECK defined in Dockerfile
[ ] No deprecated MAINTAINER instruction (use LABEL instead)---
Checklist 2: Compose Validation
2A: File Structure
[ ] No `version:` field (deprecated and ignored by modern Compose)
FAIL: version: "3.8"
[ ] File named compose.yaml (preferred) or docker-compose.yml
[ ] Services use specific image tags (NEVER latest)
[ ] Project name defined via `name:` field or CLI flag2B: Service Dependencies
[ ] depends_on uses condition: service_healthy (not bare dependency)
FAIL: depends_on: [db] (only waits for start, not ready)
PASS: depends_on: { db: { condition: service_healthy } }
[ ] Services with depends_on: service_healthy have healthcheck defined
FAIL: condition: service_healthy without healthcheck on target
[ ] Database services have appropriate healthcheck
EXAMPLE: test: ["CMD-SHELL", "pg_isready -U postgres"]2C: Volumes & Storage
[ ] Named volumes used for persistent data (NEVER anonymous)
FAIL: volumes: [/var/lib/postgresql/data]
PASS: volumes: [db-data:/var/lib/postgresql/data]
[ ] Named volumes declared in top-level volumes: section
[ ] Bind mounts use absolute paths or named volumes
[ ] Database data directories use named volumes2D: Environment & Secrets
[ ] Secrets NOT hardcoded in environment values
FAIL: DATABASE_PASSWORD: "my-secret"
PASS: DATABASE_PASSWORD: ${DATABASE_PASSWORD:?Required}
[ ] env_file used for environment-specific configuration
[ ] Sensitive values use Compose secrets (not environment)
[ ] Variable interpolation uses error syntax for required vars
EXPECTED: ${VAR:?Error message}2E: Networking & Ports
[ ] Development ports bound to localhost
FAIL: ports: ["8080:80"]
PASS: ports: ["127.0.0.1:8080:80"]
[ ] Services that do not need external access use expose (not ports)
[ ] Network isolation implemented where services should not communicate
EXAMPLE: frontend/backend network separation
[ ] container_name NOT used on scalable services
FAIL: container_name: my-nginx (prevents scaling)2F: Resource Management
[ ] Resource limits defined (deploy.resources.limits)
EXPECTED: cpus and memory limits set
[ ] restart policy appropriate (unless-stopped or on-failure, NOT always without limits)
FAIL: restart: always without deploy.resources.limits
[ ] Optional/debug services use profiles
FAIL: phpmyadmin always running in production
PASS: phpmyadmin with profiles: [debug]---
Checklist 3: Security Audit
3A: Container User
[ ] Dockerfile creates and switches to non-root USER
FAIL: No USER instruction (runs as root)
PASS: RUN groupadd -r app && useradd -r -g app app ... USER app
[ ] USER instruction uses explicit UID/GID for determinism
[ ] --no-log-init flag used with useradd
[ ] gosu used instead of sudo in entrypoint scripts3B: Capabilities & Privileges
[ ] --privileged NOT used in production
FAIL: privileged: true in compose.yaml
[ ] Capabilities dropped and only needed ones added
BEST: cap_drop: [ALL] + cap_add: [NET_BIND_SERVICE]
[ ] no-new-privileges security option set
EXPECTED: security_opt: [no-new-privileges:true]
[ ] Read-only root filesystem where possible
EXPECTED: read_only: true with tmpfs for /tmp, /run3C: Secrets & Sensitive Data
[ ] No secrets in ENV or ARG instructions
FAIL: ENV API_KEY=sk-123 or ARG DATABASE_PASSWORD=secret
[ ] Build secrets use --mount=type=secret
PASS: RUN --mount=type=secret,id=token cat /run/secrets/token
[ ] No secrets committed to .dockerignore-excluded files
[ ] No sensitive data in docker history (check with docker history)3D: Image Security
[ ] Images scanned for vulnerabilities (docker scout cves)
[ ] Base images from official/trusted sources
[ ] No unnecessary packages installed in final image
[ ] Build tools NOT present in production image (use multi-stage)3E: Network Security
[ ] Internal services NOT exposed to host network
[ ] Ports bound to specific interfaces where possible
[ ] Network isolation between frontend and backend tiers
[ ] No host network mode without justification---
Checklist 4: Production Readiness
4A: Health Checks
[ ] HEALTHCHECK defined in Dockerfile or Compose healthcheck
[ ] Health check interval, timeout, retries configured
EXPECTED: interval=30s, timeout=5s, start_period=10s, retries=3
[ ] Health check command tests actual service readiness
FAIL: CMD true (always passes)
PASS: CMD curl -f http://localhost/health || exit 1
[ ] start_period allows for application startup time4B: Restart & Recovery
[ ] Restart policy set (unless-stopped or on-failure)
[ ] on-failure has max_attempts limit to prevent crash loops
[ ] Resource limits prevent runaway container resource consumption
[ ] Logging configured with size rotation
EXPECTED: logging driver with max-size and max-file options4C: Observability
[ ] Structured logging to stdout/stderr (NOT to files inside container)
[ ] Log rotation configured (max-size, max-file)
FAIL: No log rotation (fills disk)
PASS: logging: { options: { max-size: "10m", max-file: "3" } }
[ ] Container metrics accessible via docker stats
[ ] Health status queryable via docker inspect4D: Build Reproducibility
[ ] Base images pinned to specific version (or digest)
[ ] Package versions pinned where critical
[ ] Build cache strategy defined (cache-from, cache-to)
[ ] .dockerignore prevents build context bloat
[ ] Multi-platform build configured if needed---
Checklist 5: Anti-Pattern Scan
Scan the codebase for these known issues. See references/anti-patterns.md for full details.
Dockerfile Anti-Patterns
[ ] No FROM with latest tag
[ ] No shell form ENTRYPOINT
[ ] No separate apt-get update and install
[ ] No missing apt cache cleanup
[ ] No ADD when COPY suffices
[ ] No RUN cd instead of WORKDIR
[ ] No secrets in ENV or ARG
[ ] No ENV persistence leak (unset in same RUN)
[ ] No missing .dockerignore
[ ] No running as root without justification
[ ] No too-many-layers (combine related RUN)
[ ] No missing syntax directiveCompose Anti-Patterns
[ ] No version: field present
[ ] No depends_on without healthcheck condition
[ ] No anonymous volumes for persistent data
[ ] No hardcoded secrets in environment
[ ] No container_name on scalable services
[ ] No restart: always without resource limits
[ ] No ports exposed to all interfaces (0.0.0.0)
[ ] No debug services without profilesSecurity Anti-Patterns
[ ] No --privileged in production
[ ] No running as root without USER instruction
[ ] No host network without justification
[ ] No missing capability drops
[ ] No secrets baked into image layers---
Decision Trees
Is the Dockerfile production-ready?
Does it use multi-stage build?
+-- No --> Add build + runtime stages
+-- Yes
|
Does the final stage use a minimal base?
+-- No --> Switch to alpine/slim/distroless
+-- Yes
|
Does it run as non-root?
+-- No --> CRITICAL: Add USER instruction
+-- Yes
|
Does it have a HEALTHCHECK?
+-- No --> Add health check
+-- Yes
|
Does it use exec form for ENTRYPOINT/CMD?
+-- No --> Convert to JSON array format
+-- Yes --> PASSIs the Compose file production-ready?
Does it have a version: field?
+-- Yes --> Remove it (deprecated)
+-- No
|
Do all depends_on use service_healthy?
+-- No --> Add healthchecks and conditions
+-- Yes
|
Are all persistent volumes named?
+-- No --> CRITICAL: Replace anonymous volumes
+-- Yes
|
Are resource limits set?
+-- No --> Add deploy.resources.limits
+-- Yes
|
Are secrets properly managed?
+-- No --> Move to Compose secrets or env_file
+-- Yes --> PASS---
Review Report Template
After completing all checklists, produce a report:
## Docker Configuration Review Report
### Summary
- Total issues found: X
- Critical (blocks deployment): X
- Warning (should fix): X
- Info (improvement suggestion): X
### Critical Issues
1. [CRIT-001] Description -- Location -- Fix
### Warnings
1. [WARN-001] Description -- Location -- Fix
### Passed Checks
- Dockerfile Validation: PASS/FAIL (X/Y checks passed)
- Compose Validation: PASS/FAIL
- Security Audit: PASS/FAIL
- Production Readiness: PASS/FAIL
- Anti-Pattern Scan: PASS/FAIL---
Reference Links
- references/checklist.md -- Complete validation checklist organized by area
- references/examples.md -- Review scenarios with good and bad examples
- references/anti-patterns.md -- All anti-patterns consolidated from research
Official Sources
- https://docs.docker.com/build/building/best-practices/
- https://docs.docker.com/compose/compose-file/
- https://docs.docker.com/engine/security/
- https://docs.docker.com/reference/dockerfile/
- https://docs.docker.com/scout/
Docker Anti-Patterns -- Consolidated Reference
All anti-patterns from Docker research, organized by domain. Each entry includes detection criteria, severity, and the correct pattern.
---
Dockerfile Anti-Patterns
AP-D01: Using latest Tag
- Severity: Critical
- Detection:
FROM <image>without tag, orFROM <image>:latest - Risk: Non-deterministic builds -- different image on each build
- Fix: Pin to specific version tag or digest
# BAD
FROM node:latest
FROM node
# GOOD
FROM node:20.11-bookworm-slim
# BEST (supply chain security)
FROM node:20.11-bookworm-slim@sha256:abc123...AP-D02: Running as Root
- Severity: Critical
- Detection: No
USERinstruction in Dockerfile - Risk: Container process has root privileges, escalation attack vector
- Fix: Create non-root user and switch to it
# BAD
FROM node:20
COPY . /app
CMD ["node", "app.js"]
# GOOD
FROM node:20
RUN groupadd -r appuser && useradd --no-log-init -r -g appuser appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
USER appuser
CMD ["node", "app.js"]AP-D03: Secrets in ENV or ARG
- Severity: Critical
- Detection:
ENVorARGcontaining passwords, tokens, keys, credentials - Risk: Secrets visible in
docker historyand image layers - Fix: Use
--mount=type=secretfor build-time secrets, runtime env vars for runtime secrets
# BAD
ENV API_KEY=sk-1234567890
ARG DATABASE_PASSWORD=secret123
# GOOD (build-time)
RUN --mount=type=secret,id=api_key \
cat /run/secrets/api_key | some-command
# GOOD (runtime)
# Pass via: docker run -e API_KEY="$(cat key.txt)" myappAP-D04: Shell Form ENTRYPOINT
- Severity: Warning
- Detection:
ENTRYPOINTwithout JSON array syntax - Risk: Application is NOT PID 1, SIGTERM not forwarded, no graceful shutdown
- Fix: Use exec form (JSON array)
# BAD -- application is NOT PID 1
ENTRYPOINT /usr/bin/myapp
# GOOD -- application IS PID 1
ENTRYPOINT ["/usr/bin/myapp"]AP-D05: Separate apt-get update and install
- Severity: Warning
- Detection:
RUN apt-get updateandRUN apt-get installas separate instructions - Risk: Cached update layer becomes stale, install uses outdated package index
- Fix: ALWAYS combine in single RUN
# BAD
RUN apt-get update
RUN apt-get install -y curl
# GOOD
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*AP-D06: Not Cleaning apt Cache
- Severity: Warning
- Detection:
apt-get installwithoutrm -rf /var/lib/apt/lists/*in same RUN - Risk: 30-100MB wasted per install in image size
- Fix: Clean in same RUN layer
# BAD
RUN apt-get update && apt-get install -y curl
# GOOD
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*AP-D07: ADD When COPY Suffices
- Severity: Warning
- Detection:
ADDused for local file copy (no tar extraction, no URL download) - Risk: Implicit behavior (auto-extraction, URL download) causes surprises
- Fix: Use COPY for local files, ADD only for tar extraction or URLs
# BAD
ADD config.json /app/config.json
# GOOD
COPY config.json /app/config.jsonAP-D08: Using cd Instead of WORKDIR
- Severity: Warning
- Detection:
RUN cd /path && command - Risk: cd does not persist across layers, fragile
- Fix: Use WORKDIR instruction
# BAD
RUN cd /app && npm install
# GOOD
WORKDIR /app
RUN npm installAP-D09: ENV Persistence Leak
- Severity: Warning
- Detection:
ENV VAR=valuefollowed byRUN unset VAR - Risk: ENV persists in image despite unset in later RUN (unset only affects that layer)
- Fix: Use shell variable within single RUN
# BAD -- ADMIN_USER persists in final image
ENV ADMIN_USER="mark"
RUN echo $ADMIN_USER > ./mark
RUN unset ADMIN_USER
# GOOD -- variable is temporary
RUN export ADMIN_USER="mark" \
&& echo $ADMIN_USER > ./mark \
&& unset ADMIN_USERAP-D10: Missing .dockerignore
- Severity: Warning
- Detection: No
.dockerignorefile in build context root - Risk: Sends entire directory to builder (node_modules 500MB+, .git history, IDE files)
- Fix: Create .dockerignore excluding non-essential files
# .dockerignore
.git
node_modules
dist
build
*.md
.env
.env.*
.vscode
.idea
Dockerfile
docker-compose*.ymlAP-D11: Too Many Layers
- Severity: Info
- Detection: Multiple consecutive RUN instructions for related operations
- Risk: Unnecessary layers increase image size and pull time
- Fix: Combine related operations
# BAD
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN rm -rf /var/lib/apt/lists/*
# GOOD
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
&& rm -rf /var/lib/apt/lists/*AP-D12: Missing Syntax Directive
- Severity: Info
- Detection: No
# syntax=docker/dockerfile:1at top of Dockerfile - Risk: Cannot use BuildKit features (heredocs, cache mounts, secret mounts)
- Fix: Add as first line
# syntax=docker/dockerfile:1
FROM alpine:3.21AP-D13: Poor COPY Ordering (Cache Bust)
- Severity: Warning
- Detection:
COPY . .beforeRUN npm installor similar dependency install - Risk: Any source code change invalidates dependency cache
- Fix: Copy dependency manifest first, install, then copy source
# BAD -- any change busts npm install cache
COPY . .
RUN npm install
# GOOD -- only package.json change triggers reinstall
COPY package.json package-lock.json ./
RUN npm ci
COPY . .AP-D14: Missing Pipe Failure Handling
- Severity: Warning
- Detection: Piped commands in RUN without
set -o pipefail - Risk: Intermediate pipe failures masked by last command success
- Fix: Add
set -o pipefailbefore piped commands
# BAD -- wget failure masked by wc success
RUN wget -O - https://example.com | wc -l > /count
# GOOD
RUN set -o pipefail && wget -O - https://example.com | wc -l > /countAP-D15: No Multi-Stage Build
- Severity: Warning
- Detection: Build tools (gcc, make, go, npm) present in single-stage Dockerfile
- Risk: Final image contains compilers, source code, build artifacts (100MB-1GB waste)
- Fix: Separate build and runtime stages
# BAD -- Go SDK in production (800MB+)
FROM golang:1.22
COPY . .
RUN go build -o server .
CMD ["./server"]
# GOOD -- only binary in production
FROM golang:1.22 AS build
COPY . .
RUN go build -o /server .
FROM scratch
COPY --from=build /server /server
CMD ["/server"]---
Compose Anti-Patterns
AP-C01: Using version: Field
- Severity: Warning
- Detection:
version:key present in compose.yaml - Risk: Deprecated, ignored by modern Compose, misleading
- Fix: Remove entirely
# BAD
version: "3.8"
services:
web:
image: nginx
# GOOD
services:
web:
image: nginxAP-C02: depends_on Without Healthcheck
- Severity: Warning
- Detection:
depends_on: [service]withoutcondition: service_healthy - Risk: Dependent service starts before dependency is actually ready
- Fix: Add healthcheck to dependency, use condition
# BAD
depends_on:
- db
# GOOD
depends_on:
db:
condition: service_healthyAP-C03: Anonymous Volumes for Data
- Severity: Critical
- Detection: Volume path without name (e.g.,
volumes: [/var/lib/data]) - Risk: Data lost on
docker compose down, cannot easily backup or share - Fix: Use named volumes
# BAD
volumes:
- /var/lib/postgresql/data
# GOOD
volumes:
- db-data:/var/lib/postgresql/dataAP-C04: Hardcoded Secrets
- Severity: Critical
- Detection: Plain-text passwords/tokens in environment values
- Risk: Secrets committed to version control, visible to all
- Fix: Use variable interpolation with .env file or Compose secrets
# BAD
environment:
DATABASE_PASSWORD: "my-secret-password"
# GOOD
environment:
DATABASE_PASSWORD: ${DATABASE_PASSWORD:?Password required}AP-C05: container_name on Scalable Services
- Severity: Warning
- Detection:
container_name:attribute on services that may need scaling - Risk: Container names must be unique -- prevents
docker compose scale - Fix: Let Compose manage container names
# BAD
services:
web:
image: nginx
container_name: my-nginx
# GOOD
services:
web:
image: nginxAP-C06: restart: always Without Resource Limits
- Severity: Warning
- Detection:
restart: alwayswithoutdeploy.resources.limits - Risk: Crashing container in infinite restart loop consumes all system resources
- Fix: Combine restart policy with resource limits
# BAD
restart: always
# GOOD
restart: unless-stopped
deploy:
resources:
limits:
cpus: '0.50'
memory: 512MAP-C07: Ports Exposed to All Interfaces
- Severity: Warning
- Detection: Port mapping without host IP (e.g.,
"8080:80") - Risk: Service accessible from all network interfaces (default 0.0.0.0)
- Fix: Bind to localhost for development
# BAD -- exposed to all interfaces
ports:
- "8080:80"
# GOOD -- localhost only
ports:
- "127.0.0.1:8080:80"AP-C08: Debug Services Without Profiles
- Severity: Info
- Detection: Debug/admin tools (phpmyadmin, adminer, mailhog) without profiles
- Risk: Unnecessary resource consumption, potential security exposure
- Fix: Assign to debug profile
# BAD
services:
phpmyadmin:
image: phpmyadmin
# GOOD
services:
phpmyadmin:
image: phpmyadmin
profiles: [debug]---
Security Anti-Patterns
AP-S01: Using --privileged
- Severity: Critical
- Detection:
privileged: truein Compose or--privilegedin run command - Risk: Container has full host access -- equivalent to running on bare metal as root
- Fix: Use specific capabilities
# BAD
privileged: true
# GOOD
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICEAP-S02: No Capability Management
- Severity: Warning
- Detection: No
cap_droporcap_addconfiguration - Risk: Container runs with default capability set (broader than needed)
- Fix: Drop all, add back only what is needed
# BAD -- default capabilities
services:
app:
image: myapp
# GOOD -- minimal capabilities
services:
app:
image: myapp
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:trueAP-S03: Host Network Without Justification
- Severity: Warning
- Detection:
network_mode: hostor--network host - Risk: No network isolation -- container shares host's network namespace
- Fix: Use bridge network with port mapping unless performance requires host mode
AP-S04: Mounting Host Root
- Severity: Critical
- Detection: Volume mount of
/or/etcor/var/run/docker.sock - Risk: Container can read/write host filesystem, docker socket gives root access
- Fix: Mount only specific needed directories with minimal permissions
# BAD
volumes:
- /:/host
- /var/run/docker.sock:/var/run/docker.sock
# GOOD -- mount only what is needed
volumes:
- ./config:/app/config:roAP-S05: No Image Scanning
- Severity: Warning
- Detection: No
docker scout cvesin CI/CD pipeline - Risk: Vulnerable base images or dependencies deployed to production
- Fix: Add scanning to CI/CD
# Add to CI pipeline
docker scout cves --only-severity critical,high --exit-code myapp:latest---
Build Performance Anti-Patterns
AP-B01: Large Build Context
- Severity: Warning
- Detection: No .dockerignore, slow
docker buildstartup - Risk: Sends GB of files to builder (node_modules, .git, test data)
- Fix: Create comprehensive .dockerignore
AP-B02: No Cache Mounts
- Severity: Info
- Detection:
RUN pip installorRUN npm installwithout--mount=type=cache - Risk: Full package download on every build
- Fix: Use BuildKit cache mounts for package managers
# BAD -- downloads all packages every build
RUN pip install -r requirements.txt
# GOOD -- reuses cached packages
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txtAP-B03: No Build Cache Strategy in CI
- Severity: Info
- Detection: No
--cache-from/--cache-toin CI build commands - Risk: Full rebuild on every CI run (slow, wasteful)
- Fix: Configure registry or GHA cache backend
# GOOD -- registry cache
docker buildx build \
--cache-from type=registry,ref=registry/app:buildcache \
--cache-to type=registry,ref=registry/app:buildcache,mode=max \
-t registry/app:latest .---
Quick Detection Checklist
Use this for rapid scanning of a Docker project:
Dockerfile:
[ ] grep -c "FROM.*latest\|FROM [a-z]*/[a-z]*$" Dockerfile # AP-D01
[ ] grep -c "^USER " Dockerfile # AP-D02 (should be >0)
[ ] grep -c "ENV.*KEY\|ENV.*SECRET\|ENV.*PASSWORD" Dockerfile # AP-D03
[ ] grep -c "^ENTRYPOINT [^[]" Dockerfile # AP-D04
[ ] grep -c "^ADD " Dockerfile # AP-D07 (review each)
[ ] test -f .dockerignore # AP-D10
Compose:
[ ] grep -c "^version:" compose.yaml # AP-C01
[ ] grep -c "container_name:" compose.yaml # AP-C05
[ ] grep -c "privileged: true" compose.yaml # AP-S01
[ ] grep -c "restart: always" compose.yaml # AP-C06 (check limits)Docker Review Checklist -- Complete Reference
This checklist expands on the SKILL.md checklists with detailed verification steps, expected states, and common failure modes for each check.
---
Area 1: Dockerfile
Base Image Checks
| # | Check | Expected State | Common Failure |
|---|---|---|---|
| D-01 | Base image has specific version tag | FROM node:20.11-bookworm-slim | Using latest or no tag |
| D-02 | Final stage uses minimal base | alpine, slim, distroless, or scratch | Full OS image (ubuntu, debian) in production |
| D-03 | Multi-stage build separates build from runtime | Build tools only in build stage | Compiler/SDK in final image |
| D-04 | Build stages named with AS | FROM golang:1.22 AS build | COPY --from=0 (numeric index) |
| D-05 | Digest pinning for supply chain security | FROM alpine@sha256:abc... | Tag-only reference in CI/CD |
Instruction Checks
| # | Check | Expected State | Common Failure |
|---|---|---|---|
| D-06 | syntax directive at top | # syntax=docker/dockerfile:1 | Missing (no BuildKit features) |
| D-07 | COPY over ADD for local files | COPY config.json /app/ | ADD config.json /app/ without extraction need |
| D-08 | Combined apt-get update and install | Single RUN with && | Separate RUN layers |
| D-09 | apt cache cleaned | && rm -rf /var/lib/apt/lists/* | Cache left in image (+30-100MB) |
| D-10 | --no-install-recommends | apt-get install -y --no-install-recommends | Extra packages installed |
| D-11 | Package versions pinned | curl=7.88.1-10+deb12u5 | Unversioned curl |
| D-12 | Related commands combined | Single RUN per logical operation | One RUN per command |
| D-13 | WORKDIR over RUN cd | WORKDIR /app | RUN cd /app && ... |
| D-14 | .dockerignore exists | Excludes node_modules, .git, build/ | Missing or incomplete |
| D-15 | COPY ordered for cache | Dependencies first, source code last | COPY . . before npm install |
Signal & Process Checks
| # | Check | Expected State | Common Failure |
|---|---|---|---|
| D-16 | ENTRYPOINT exec form | ENTRYPOINT ["/usr/bin/app"] | ENTRYPOINT /usr/bin/app (shell form) |
| D-17 | CMD exec form | CMD ["node", "server.js"] | CMD node server.js |
| D-18 | Entrypoint script uses exec | exec "$@" at end of script | Shell wraps process (not PID 1) |
| D-19 | Pipe failure safety | set -o pipefail before pipes | Pipe failure masked by last command |
Metadata Checks
| # | Check | Expected State | Common Failure |
|---|---|---|---|
| D-20 | EXPOSE documents ports | EXPOSE 8080/tcp | Missing port documentation |
| D-21 | OCI labels present | LABEL org.opencontainers.image.* | No metadata or deprecated MAINTAINER |
| D-22 | HEALTHCHECK defined | HEALTHCHECK CMD curl -f http://localhost/ | No health check in Dockerfile |
---
Area 2: Compose
Structure Checks
| # | Check | Expected State | Common Failure |
|---|---|---|---|
| C-01 | No version field | Omitted entirely | version: "3.8" present |
| C-02 | Preferred filename | compose.yaml | docker-compose.yml (legacy) |
| C-03 | Specific image tags | image: postgres:16 | image: postgres or postgres:latest |
| C-04 | Project name defined | name: my-project | Relies on directory name |
Dependency Checks
| # | Check | Expected State | Common Failure |
|---|---|---|---|
| C-05 | depends_on with health condition | condition: service_healthy | Bare depends_on: [db] |
| C-06 | Target has healthcheck | Healthcheck defined on dependency | service_healthy without healthcheck on target |
| C-07 | Database healthcheck appropriate | pg_isready, mysqladmin ping, etc. | Generic or missing healthcheck |
Volume Checks
| # | Check | Expected State | Common Failure |
|---|---|---|---|
| C-08 | Named volumes for data | db-data:/var/lib/postgresql/data | Anonymous volume /var/lib/postgresql/data |
| C-09 | Top-level volumes declared | volumes: { db-data: } | Named volume used but not declared |
| C-10 | Bind mounts use absolute paths | /host/path:/container/path | Relative path without ./ prefix |
Environment Checks
| # | Check | Expected State | Common Failure |
|---|---|---|---|
| C-11 | No hardcoded secrets | ${DB_PASS:?Required} | DB_PASS: "plaintext-secret" |
| C-12 | env_file for config | env_file: [.env] | All values inline in compose.yaml |
| C-13 | Compose secrets for sensitive data | secrets: section used | Passwords in environment variables |
| C-14 | Required vars use error syntax | ${VAR:?Error message} | ${VAR} silently empty |
Network Checks
| # | Check | Expected State | Common Failure |
|---|---|---|---|
| C-15 | Dev ports bound to localhost | 127.0.0.1:8080:80 | 8080:80 (all interfaces) |
| C-16 | Internal services use expose | expose: ["3000"] | ports on internal-only services |
| C-17 | Network isolation | Separate frontend/backend networks | All services on default network |
| C-18 | No container_name on scalable services | Let Compose manage names | container_name: my-web |
Resource Checks
| # | Check | Expected State | Common Failure |
|---|---|---|---|
| C-19 | Resource limits set | deploy.resources.limits.memory: 512M | No limits (unbounded) |
| C-20 | Restart with limits | restart: unless-stopped + limits | restart: always without limits |
| C-21 | Debug services profiled | profiles: [debug] | phpmyadmin always enabled |
---
Area 3: Security
User Checks
| # | Check | Expected State | Common Failure |
|---|---|---|---|
| S-01 | Non-root USER | USER appuser or USER 1001 | No USER instruction |
| S-02 | Explicit UID/GID | USER 1001:1001 | USER appuser without known UID |
| S-03 | --no-log-init flag | useradd --no-log-init -r | faillog fills with NULLs |
| S-04 | gosu over sudo | exec gosu appuser "$@" | sudo -u appuser in entrypoint |
Privilege Checks
| # | Check | Expected State | Common Failure |
|---|---|---|---|
| S-05 | No --privileged | Absent from Compose and run commands | privileged: true |
| S-06 | Capabilities managed | cap_drop: [ALL] + specific adds | Default capabilities (too broad) |
| S-07 | no-new-privileges | security_opt: [no-new-privileges:true] | Missing security option |
| S-08 | Read-only root FS | read_only: true + tmpfs mounts | Writable root filesystem |
Secret Checks
| # | Check | Expected State | Common Failure |
|---|---|---|---|
| S-09 | No secrets in ENV/ARG | Build secrets via --mount=type=secret | ENV API_KEY=sk-123 |
| S-10 | Secret mount in build | RUN --mount=type=secret,id=token | COPY secrets.txt /app/ |
| S-11 | Clean docker history | No sensitive data in layer history | ARG with default secret value |
Image Security Checks
| # | Check | Expected State | Common Failure |
|---|---|---|---|
| S-12 | Vulnerability scan | docker scout cves run, no critical | Never scanned |
| S-13 | Official base images | Docker Official Images or verified | Unverified third-party images |
| S-14 | Minimal packages | Only runtime dependencies in final image | curl, vim, build-essential in production |
| S-15 | No build tools in prod | Multi-stage separates build from runtime | gcc, make in final image |
---
Area 4: Production Readiness
Health Check Configuration
| # | Check | Expected State | Common Failure |
|---|---|---|---|
| P-01 | HEALTHCHECK defined | Present in Dockerfile or Compose | No health monitoring |
| P-02 | Appropriate intervals | interval=30s, timeout=5s, retries=3 | Default 30s timeout (too long) |
| P-03 | Meaningful test command | Tests actual service endpoint | CMD true or CMD exit 0 |
| P-04 | start_period configured | Allows for startup time | Marked unhealthy during startup |
Recovery Configuration
| # | Check | Expected State | Common Failure |
|---|---|---|---|
| P-05 | Restart policy set | unless-stopped or on-failure | restart: "no" in production |
| P-06 | Max restart attempts | on-failure:5 or max_attempts: 5 | Infinite restart loop |
| P-07 | Resource limits | Memory and CPU limits set | Runaway container consumes all RAM |
| P-08 | Log rotation | max-size: 10m, max-file: 3 | Logs fill disk |
Observability Configuration
| # | Check | Expected State | Common Failure |
|---|---|---|---|
| P-09 | Logs to stdout/stderr | Application logs to standard streams | Logs to /var/log/app.log inside container |
| P-10 | Log rotation active | Logging driver options set | No rotation (disk fills up) |
| P-11 | Metrics accessible | docker stats shows useful data | No resource monitoring |
Build Configuration
| # | Check | Expected State | Common Failure |
|---|---|---|---|
| P-12 | Pinned base images | Specific version tag or digest | latest tag in CI/CD |
| P-13 | Pinned packages | Version constraints on critical packages | apt-get install curl (any version) |
| P-14 | Cache strategy | cache-from / cache-to configured | Full rebuild every CI run |
| P-15 | .dockerignore complete | Excludes .git, node_modules, docs, tests | Sends GB of context to builder |
| P-16 | Multi-platform ready | --platform configured if needed | amd64-only in mixed environments |
Docker Review Examples
Real-world review scenarios showing good configurations, bad configurations, and the fixes needed.
---
Scenario 1: Node.js Application Dockerfile
Bad Dockerfile
FROM node:latest
COPY . /app
WORKDIR /app
RUN npm install
EXPOSE 3000
CMD npm startIssues Found
| ID | Severity | Issue | Location |
|---|---|---|---|
| CRIT-001 | Critical | Running as root (no USER instruction) | Entire Dockerfile |
| CRIT-002 | Critical | Using latest tag (non-deterministic) | FROM line |
| WARN-001 | Warning | Missing syntax directive | Top of file |
| WARN-002 | Warning | CMD in shell form (no signal handling) | CMD line |
| WARN-003 | Warning | COPY . . before npm install (cache bust) | COPY line |
| WARN-004 | Warning | No .dockerignore (node_modules sent to context) | Project root |
| WARN-005 | Warning | No HEALTHCHECK defined | Entire Dockerfile |
| WARN-006 | Warning | No multi-stage build (dev deps in prod) | Entire Dockerfile |
| INFO-001 | Info | No OCI labels | Entire Dockerfile |
Fixed Dockerfile
# syntax=docker/dockerfile:1
# ---- Build Stage ----
FROM node:20.11-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production
COPY . .
# ---- Production Stage ----
FROM node:20.11-bookworm-slim AS production
LABEL org.opencontainers.image.title="My Node App" \
org.opencontainers.image.version="1.0.0"
RUN groupadd -r appuser && useradd --no-log-init -r -g appuser appuser
WORKDIR /app
COPY --from=build --chown=appuser:appuser /app ./
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) })" || exit 1
CMD ["node", "server.js"]---
Scenario 2: Python API Dockerfile
Bad Dockerfile
FROM python:3.12
ADD . /app
WORKDIR /app
RUN pip install -r requirements.txt
ENV SECRET_KEY=my-super-secret-key
ENTRYPOINT python app.pyIssues Found
| ID | Severity | Issue | Location |
|---|---|---|---|
| CRIT-001 | Critical | Secret in ENV (visible in docker history) | ENV SECRET_KEY line |
| CRIT-002 | Critical | Running as root | Entire Dockerfile |
| CRIT-003 | Critical | ENTRYPOINT shell form (signals not forwarded) | ENTRYPOINT line |
| WARN-001 | Warning | ADD used instead of COPY | ADD line |
| WARN-002 | Warning | Full Python image (900MB+) | FROM line |
| WARN-003 | Warning | No cache mount for pip | RUN pip install |
| WARN-004 | Warning | Missing syntax directive | Top of file |
| WARN-005 | Warning | No HEALTHCHECK | Entire Dockerfile |
Fixed Dockerfile
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS production
LABEL org.opencontainers.image.title="My Python API"
RUN groupadd -r appuser && useradd --no-log-init -r -g appuser appuser
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-compile -r requirements.txt
COPY --chown=appuser:appuser . .
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
ENTRYPOINT ["python", "app.py"]Secret handling at runtime:
# Pass secret via environment at runtime (NOT baked into image)
docker run -e SECRET_KEY="$(cat secret.txt)" myapp
# Or use Docker secrets in Compose---
Scenario 3: Go Application with Multi-Stage Build
Bad Dockerfile
FROM golang:1.22
WORKDIR /app
COPY . .
RUN go build -o server .
CMD ["./server"]Issues Found
| ID | Severity | Issue | Location |
|---|---|---|---|
| WARN-001 | Warning | No multi-stage (Go SDK in production ~800MB) | FROM line |
| WARN-002 | Warning | No cache mounts for Go modules | RUN go build |
| WARN-003 | Warning | Running as root | Entire Dockerfile |
| WARN-004 | Warning | Missing syntax directive | Top of file |
| INFO-001 | Info | Could use scratch for static binary | FROM line |
Fixed Dockerfile
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -o /bin/server .
FROM scratch
COPY --from=build /bin/server /server
USER 65534:65534
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD ["/server", "healthcheck"]
ENTRYPOINT ["/server"]---
Scenario 4: Compose File Review
Bad Compose File
version: "3.8"
services:
web:
build: .
ports:
- "8080:80"
depends_on:
- db
environment:
DATABASE_URL: "postgres://admin:secret123@db:5432/myapp"
restart: always
container_name: my-web
db:
image: postgres
volumes:
- /var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: secret123
adminer:
image: adminer
ports:
- "9090:8080"Issues Found
| ID | Severity | Issue | Location |
|---|---|---|---|
| CRIT-001 | Critical | Hardcoded database password | web.environment, db.environment |
| CRIT-002 | Critical | Anonymous volume for database data | db.volumes |
| WARN-001 | Warning | version field present (deprecated) | Top of file |
| WARN-002 | Warning | depends_on without health condition | web.depends_on |
| WARN-003 | Warning | postgres using latest tag | db.image |
| WARN-004 | Warning | Ports exposed to all interfaces | web.ports, adminer.ports |
| WARN-005 | Warning | restart: always without resource limits | web.restart |
| WARN-006 | Warning | container_name prevents scaling | web.container_name |
| WARN-007 | Warning | No healthcheck on db | db service |
| WARN-008 | Warning | Adminer always running (no profile) | adminer service |
| WARN-009 | Warning | No resource limits | All services |
| WARN-010 | Warning | No log rotation configured | All services |
| INFO-001 | Info | No network isolation | All services on default network |
Fixed Compose File
name: my-project
services:
web:
build: .
ports:
- "127.0.0.1:8080:80"
depends_on:
db:
condition: service_healthy
env_file:
- .env
environment:
DATABASE_URL: "postgres://${DB_USER}:${DB_PASS:?Database password required}@db:5432/${DB_NAME}"
restart: unless-stopped
networks:
- frontend
- backend
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
logging:
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
db:
image: postgres:16
volumes:
- db-data:/var/lib/postgresql/data
env_file:
- .env.db
environment:
POSTGRES_PASSWORD: ${DB_PASS:?Database password required}
restart: unless-stopped
networks:
- backend
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
logging:
options:
max-size: "10m"
max-file: "3"
adminer:
image: adminer:4
ports:
- "127.0.0.1:9090:8080"
profiles:
- debug
networks:
- backend
deploy:
resources:
limits:
cpus: '0.25'
memory: 128M
networks:
frontend:
backend:
volumes:
db-data:---
Scenario 5: Security Audit -- Privileged Container
Bad Configuration
services:
app:
image: myapp:latest
privileged: true
ports:
- "80:80"
volumes:
- /:/host
network_mode: hostIssues Found
| ID | Severity | Issue | Location |
|---|---|---|---|
| CRIT-001 | Critical | privileged: true (full host access) | app.privileged |
| CRIT-002 | Critical | Host root mounted into container | app.volumes |
| CRIT-003 | Critical | Host network mode (no isolation) | app.network_mode |
| CRIT-004 | Critical | Using latest tag | app.image |
| WARN-001 | Warning | No resource limits | app service |
| WARN-002 | Warning | No healthcheck | app service |
Fixed Configuration
services:
app:
image: myapp:1.5.2
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
- /run
ports:
- "127.0.0.1:80:80"
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/health"]
interval: 30s
timeout: 5s
retries: 3---
Review Report Example
## Docker Configuration Review Report
### Summary
- Total issues found: 12
- Critical (blocks deployment): 3
- Warning (should fix): 7
- Info (improvement suggestion): 2
### Critical Issues
1. [CRIT-001] Hardcoded database password in compose.yaml -- web.environment.DATABASE_URL -- Move to .env file with ${DB_PASS:?Required}
2. [CRIT-002] Anonymous volume for PostgreSQL data -- db.volumes -- Use named volume: db-data:/var/lib/postgresql/data
3. [CRIT-003] Running as root in Dockerfile -- No USER instruction -- Add non-root user and USER directive
### Warnings
1. [WARN-001] version field present -- compose.yaml line 1 -- Remove (deprecated)
2. [WARN-002] depends_on without health condition -- web.depends_on -- Add condition: service_healthy
3. [WARN-003] PostgreSQL using implicit latest tag -- db.image -- Pin to postgres:16
4. [WARN-004] Ports exposed to 0.0.0.0 -- web.ports -- Bind to 127.0.0.1
5. [WARN-005] No resource limits -- All services -- Add deploy.resources.limits
6. [WARN-006] No log rotation -- All services -- Add logging.options with max-size
7. [WARN-007] Adminer always running -- adminer service -- Add profiles: [debug]
### Info
1. [INFO-001] No network isolation -- All services on default -- Add frontend/backend networks
2. [INFO-002] No OCI labels in Dockerfile -- Dockerfile -- Add org.opencontainers.image.* labels
### Passed Checks
- Dockerfile Validation: FAIL (8/15 checks passed)
- Compose Validation: FAIL (3/12 checks passed)
- Security Audit: FAIL (2/11 checks passed)
- Production Readiness: FAIL (1/8 checks passed)
- Anti-Pattern Scan: FAIL (5/20 patterns detected)