
Dockerfile Validator
- 465 installs
- 286 repo stars
- Updated July 26, 2026
- akin-ozer/cc-devops-skills
dockerfile-validator is a DevOps agent skill that runs dockerfile-validate.sh with hadolint and Checkov to audit Dockerfiles for security, layer bloat, and CI policy violations before container deploys.
About
dockerfile-validator is an akin-ozer/cc-devops-skills agent skill that validates Dockerfiles through a deterministic eight-step flow using scripts/dockerfile-validate.sh, classifying findings into Critical, High, Medium, and Low severity buckets. It checks for hardcoded secrets, root runtime, unpinned :latest base images, missing HEALTHCHECK and USER directives, Checkov CKV_DOCKER failures, and hadolint errors, with explicit fallbacks when Docker, hadolint, or Checkov are unavailable. The skill bundles three reference guides—security_checklist.md, optimization_guide.md, and docker_best_practices.md—plus five example Dockerfiles and a test_validate.sh CI entrypoint, loading references only when issues exist to keep no-finding runs fast. Reach for dockerfile-validator when reviewing Dockerfile, Dockerfile.prod, or Dockerfile.dev before merge, CI, or production image builds, and rerun after applying recommended fixes.
- Base image and tag hygiene
- Non-root and least-privilege checks
- Layer size and cache efficiency
- Healthcheck and signal handling
- CI-friendly policy feedback
Dockerfile Validator by the numbers
- 465 all-time installs (skills.sh)
- Ranked #268 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/akin-ozer/cc-devops-skills --skill dockerfile-validatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 465 |
|---|---|
| repo stars | ★ 286 |
| Last updated | July 26, 2026 |
| Repository | akin-ozer/cc-devops-skills ↗ |
How do you audit Dockerfile security before deploy?
Audit Dockerfiles before deploy for insecure base images, root users, missing healthchecks, layer bloat, and CI policy violations in containerized services.
Who is it for?
Developers shipping containerized services who need deterministic Dockerfile linting with hadolint, Checkov, and severity-bucketed reports before CI or production builds.
Skip if: Skip dockerfile-validator when generating a new Dockerfile from scratch (use dockerfile-generator) or debugging running container runtime behavior instead of static Dockerfile policy.
When should I use this skill?
User asks to validate, lint, security-scan, or optimize a Dockerfile before merge, CI, or production deploy.
What you get
Dockerfile Validation Report with severity-bucketed findings, recommended fixes, references used, and pass/fail status after optional rerun.
- Dockerfile Validation Report
- severity-classified finding list
- recommended Dockerfile patches
By the numbers
- Bundles 3 reference guides and 5 example Dockerfiles
- Reports findings in 4 severity buckets: Critical, High, Medium, Low
- Deterministic 8-step validation flow with CI entrypoint test_validate.sh
Files
Dockerfile Validator
Validate Dockerfiles with deterministic stages, clear severity reporting, and explicit fallbacks when tools or network access are constrained.
Trigger Phrases
Use this skill when the user asks for tasks like:
- "validate this Dockerfile"
- "lint/check my Dockerfile"
- "security scan Dockerfile"
- "optimize Docker image size/build time"
- "review Dockerfile before merge"
- "find issues in Dockerfile.prod/Dockerfile.dev"
Use / Do Not Use
Use this skill for:
- Syntax and lint validation
- Security and secrets checks
- Best-practice and performance review
- Dockerfile hardening before CI/CD or production
Do not use this skill for:
- Generating a new Dockerfile from scratch (use
dockerfile-generator) - Running containers, debugging runtime behavior, or image registry operations
Local Files In This Skill
- Validator script:
scripts/dockerfile-validate.sh - References:
references/security_checklist.mdreferences/optimization_guide.mdreferences/docker_best_practices.md- Example Dockerfiles:
examples/*.Dockerfile
Deterministic Execution Flow (Required)
Run these steps in order. Do not skip steps unless a documented fallback branch applies.
1. Preflight and Path Setup
Assume repo root as working directory:
cd /path/to/repo
SKILL_DIR="devops-skills-plugin/skills/dockerfile-validator"
TARGET_DOCKERFILE="Dockerfile" # replace when user provides a pathValidate inputs before running tools:
test -f "$SKILL_DIR/scripts/dockerfile-validate.sh"
test -f "$TARGET_DOCKERFILE"If either check fails, stop and report the exact missing path.
2. Read the Target Dockerfile Explicitly
Use explicit file-read commands (not abstract "Read tool" wording):
sed -n '1,220p' "$TARGET_DOCKERFILE"If needed for long files:
sed -n '220,440p' "$TARGET_DOCKERFILE"3. Run Validation Script
Primary command:
bash "$SKILL_DIR/scripts/dockerfile-validate.sh" "$TARGET_DOCKERFILE"Optional captured run for structured reporting:
bash "$SKILL_DIR/scripts/dockerfile-validate.sh" "$TARGET_DOCKERFILE" | tee /tmp/dockerfile-validator.out4. Classify Findings by Severity (Standard)
Use this standard severity model:
Critical- Hardcoded secrets/credentials
- Explicit root runtime with high-risk context
- High-impact security policy failures
High- Checkov failures for container hardening
- hadolint errors likely to cause insecure/unreliable builds
- Missing or unsafe runtime-user posture (
USER) Medium:latestimage tags, missing pinning, cache-cleanup misses- Build cache inefficiency and layered install anti-patterns
Low- Style/info guidance and non-blocking optimization suggestions
5. No-Issue Fast Path (Required)
If validation has no actionable findings:
- Return a concise pass summary.
- Do not open reference files.
- Do not generate fix diffs.
Use fast path when all are true:
- Script reports overall pass.
- No security failures.
- No error/warning findings requiring user action.
6. Reference Loading Rules (Only When Findings Exist)
Only read references that match actual findings. Read each required file once.
Issue-to-reference mapping:
| Issue category | Trigger examples | Read this file |
|---|---|---|
| Secrets, root user, exposed sensitive ports, hardening gaps | CKV_DOCKER_*, hardcoded token/password, root runtime | references/security_checklist.md |
Image size, layer count, multi-stage opportunities, cache efficiency, .dockerignore gaps | too many RUN, single-stage with build deps, cache misses | references/optimization_guide.md |
| Tag pinning, instruction usage, COPY vs ADD, WORKDIR/CMD/ENTRYPOINT conventions | :latest, unpinned packages, instruction-level best practices | references/docker_best_practices.md |
Explicit read commands:
sed -n '1,220p' "$SKILL_DIR/references/security_checklist.md"
sed -n '1,220p' "$SKILL_DIR/references/optimization_guide.md"
sed -n '1,220p' "$SKILL_DIR/references/docker_best_practices.md"For targeted extraction:
rg -n "USER|secrets|EXPOSE|HEALTHCHECK" "$SKILL_DIR/references/security_checklist.md"
rg -n "multi-stage|cache|layer|dockerignore" "$SKILL_DIR/references/optimization_guide.md"
rg -n "FROM|COPY|ADD|WORKDIR|CMD|ENTRYPOINT|latest" "$SKILL_DIR/references/docker_best_practices.md"7. Produce Standard Report Output
Use this template for every non-fast-path run:
## Dockerfile Validation Report
- Target: <path>
- Command: `bash <skill-script> <target>`
- Overall result: PASS | FAIL | PARTIAL (fallback)
### Critical
- <issue or `None`>
### High
- <issue or `None`>
### Medium
- <issue or `None`>
### Low
- <issue or `None`>
### Recommended Fixes
- <specific code-level fix per actionable issue>
### References Used
- <list only files actually read>
### Fallbacks Used
- `None` or exact fallback branch + reason8. Offer Fix Application
After reporting:
- Ask whether to apply fixes.
- If user approves, patch the Dockerfile and rerun validation.
Fallback Behavior (Explicit)
When the primary script cannot complete, use deterministic fallback branches and report them.
Fallback A: Python/Tool Install Constraint
Condition:
- Script exits with tool-install failure (for example Python missing, package install blocked, or restricted environment).
Action: 1. Report primary failure and why. 2. Run manual minimum checks:
# Basic syntax signal (if Docker is available)
DOCKERFILE_DIR="$(dirname "$TARGET_DOCKERFILE")"
docker build --no-cache -f "$TARGET_DOCKERFILE" "$DOCKERFILE_DIR"
# High-value static checks
grep -nEi "^[[:space:]]*FROM[[:space:]]+.*:latest" "$TARGET_DOCKERFILE" || true
grep -nEi "^[[:space:]]*(ENV|ARG)[[:space:]].*(password|secret|token|api[_-]?key)[[:space:]]*=" "$TARGET_DOCKERFILE" || true
grep -nEi "^[[:space:]]*USER[[:space:]]+(root|0(:0)?)$" "$TARGET_DOCKERFILE" || true
grep -nEi "^[[:space:]]*HEALTHCHECK[[:space:]]+" "$TARGET_DOCKERFILE" || true3. Classify output with PARTIAL result and clearly label skipped checks.
Fallback B: hadolint Not Available but Docker Available
Use hadolint container image:
docker run --rm -i hadolint/hadolint < "$TARGET_DOCKERFILE"Fallback C: No Docker, No hadolint/checkov
Run only manual regex-based checks (Fallback A step 2), clearly mark as PARTIAL, and state which scanners were skipped.
Quick Command Set
Validate one Dockerfile
cd /path/to/repo
bash devops-skills-plugin/skills/dockerfile-validator/scripts/dockerfile-validate.sh DockerfileValidate alternate file
cd /path/to/repo
bash devops-skills-plugin/skills/dockerfile-validator/scripts/dockerfile-validate.sh Dockerfile.prodValidate skill examples
cd /path/to/repo/devops-skills-plugin/skills/dockerfile-validator
bash scripts/dockerfile-validate.sh examples/good-example.Dockerfile
bash scripts/dockerfile-validate.sh examples/security-issues.DockerfileRun regression checks (CI entrypoint)
cd /path/to/repo
bash devops-skills-plugin/skills/dockerfile-validator/scripts/test_validate.shOptional strict mode for CI environments that must enforce ShellCheck:
STRICT_SHELLCHECK=true bash devops-skills-plugin/skills/dockerfile-validator/scripts/test_validate.shProgressive Disclosure Rules
- Always read the target Dockerfile first.
- Do not read any reference files unless findings require them.
- Read only the matching reference file(s) from the issue-to-reference mapping.
- Do not reread the same reference unless new issue categories appear.
Done Criteria
Consider this skill execution complete only when all conditions below are satisfied:
- Trigger matched a Dockerfile validation/lint/security/optimization request.
- Target Dockerfile path was explicitly verified.
- Validation command (or explicit fallback) was executed.
- Findings were reported using severity buckets (
Critical,High,Medium,Low). - Reference usage matched issue categories and was explicitly listed.
- No-issue fast path skipped unnecessary reference reads.
- If fixes were applied, validation was rerun and final status reported.
Resources
- Script:
scripts/dockerfile-validate.sh - CI/regression entrypoint:
scripts/test_validate.sh - Security reference:
references/security_checklist.md - Optimization reference:
references/optimization_guide.md - Best-practices reference:
references/docker_best_practices.md - Examples:
examples/good-example.Dockerfile,examples/bad-example.Dockerfile,examples/security-issues.Dockerfile,examples/python-optimized.Dockerfile,examples/golang-distroless.Dockerfile
Source Links
# Ignore temporary validation output files
*.tmp
*.log
validation_results.json
# Ignore test build artifacts
test_build/
# VCS
.git
.gitignore
# Local environment and secrets
.env
.env.*
*.pem
*.key
# Build and dependency artifacts
node_modules/
dist/
build/
target/
venv/
.venv/
__pycache__/
*.pyc
# Logs and temp files
*.log
*.tmp
tmp/
# Tooling and editor files
.idea/
.vscode/
.DS_Store
# Docker-related files not needed in image
Dockerfile*
docker-compose*.yml
.dockerignore
# Bad Example - Demonstrates common mistakes and anti-patterns
# DO NOT USE THIS IN PRODUCTION
# Issue 1: Using :latest tag (unpredictable, not reproducible)
FROM ubuntu:latest
# Issue 2: Running as root (security risk)
# Issue 3: No WORKDIR set (unclear where commands run)
# Issue 4: Separate RUN commands (creates unnecessary layers)
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN apt-get install -y git
# Issue 5: No cache cleanup (increases image size)
# Issue 6: Using shell form instead of exec form
WORKDIR app
# Issue 7: Copying everything before installing dependencies (poor caching)
COPY . .
# Issue 8: No version pinning for packages
RUN pip install flask
# Issue 9: Potential secret exposure
ENV API_KEY=secret123
ENV PASSWORD=admin
# Issue 10: Exposing SSH port (security risk)
EXPOSE 22
EXPOSE 80
# Issue 11: No HEALTHCHECK defined
# Issue 12: No USER directive (runs as root)
# Issue 13: Shell form instead of exec form (poor signal handling)
CMD python app.py# syntax=docker/dockerfile:1
# Multi-stage Go Dockerfile with distroless runtime
# Demonstrates minimal attack surface and optimal size
# Build stage
FROM golang:1.21-alpine AS builder
# Install build dependencies
# hadolint ignore=DL3018
RUN apk add --no-cache git ca-certificates
WORKDIR /src
# Copy go mod files for dependency caching
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build static binary
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags='-w -s -extldflags "-static"' \
-a \
-o /app/server \
./cmd/server
# Runtime stage - distroless (minimal, secure)
# checkov:skip=CKV_DOCKER_2:Distroless runtime uses external liveness/readiness probes instead of in-container HEALTHCHECK.
# checkov:skip=CKV_DOCKER_3:distroless:nonroot image already runs as a non-root user.
FROM gcr.io/distroless/static-debian11:nonroot
# Copy CA certificates for HTTPS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy binary from builder
COPY --from=builder /app/server /server
# Expose port
EXPOSE 8080
# Already running as non-root (distroless:nonroot)
# User is automatically set to uid:gid 65532:65532
# Health check (limited in distroless, use external checks)
# HEALTHCHECK not available in distroless
# Exec form entrypoint
ENTRYPOINT ["/server"]
# syntax=docker/dockerfile:1
# Example of a well-structured, secure, and optimized Dockerfile
# This demonstrates Docker best practices for a Node.js application
# Build stage - includes all build dependencies
FROM node:21-alpine AS builder
# Set working directory
WORKDIR /app
# Copy package files first for better layer caching
COPY package.json package-lock.json ./
# Install dependencies with cache mount for faster rebuilds
RUN --mount=type=cache,target=/root/.cache/npm \
npm ci --only=production
# Copy application source
COPY . .
# Build the application (if needed)
RUN npm run build
# Runtime stage - minimal image with only necessary runtime dependencies
FROM node:21-alpine AS runtime
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Set working directory
WORKDIR /app
# Copy built application and dependencies from builder stage
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./
# Switch to non-root user
USER nodejs
# Expose application port
EXPOSE 3000
# Add healthcheck for container monitoring
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); })"
# Use exec form for better signal handling
ENTRYPOINT ["node"]
CMD ["dist/index.js"]# syntax=docker/dockerfile:1
# Multi-stage Python Dockerfile with best practices
# Optimized for size and security
# Build stage
FROM python:3.12-slim AS builder
WORKDIR /app
# Install build dependencies
# hadolint ignore=DL3008
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first for layer caching
COPY requirements.txt ./
# Install Python dependencies with cache mount
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --user --no-cache-dir -r requirements.txt
# Runtime stage - minimal
FROM python:3.12-slim AS runtime
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
# Copy Python packages from builder
COPY --from=builder /root/.local /root/.local
# Copy application code
COPY --chown=appuser:appuser . .
# Add local bin to PATH
ENV PATH=/root/.local/bin:$PATH
# Switch to non-root user
USER appuser
# Expose application port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=3s \
CMD python -c "import requests; requests.get('http://localhost:8000/health')" || exit 1
# Use exec form
ENTRYPOINT ["python"]
CMD ["-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# Dockerfile with Intentional Security Issues
# FOR TESTING VALIDATION ONLY - DO NOT USE IN PRODUCTION
# Issue: Using :latest tag
FROM python:latest
# Issue: Running as root user throughout
WORKDIR /app
# Issue: Hardcoded secrets
ENV DATABASE_PASSWORD=super_secret_password
ENV API_TOKEN=abc123xyz789
ARG SECRET_KEY=my_secret_key
# Issue: Installing unnecessary packages, no version pinning
RUN apt-get update && apt-get install -y \
openssh-server \
telnet \
ftp \
vim \
nano
# Issue: No cache cleanup
# (apt lists remain, increasing image size)
# Issue: Using ADD instead of COPY
ADD . /app
# Issue: Installing packages without version pins
RUN pip install flask requests sqlalchemy
# Issue: Exposing SSH port
EXPOSE 22
EXPOSE 23
EXPOSE 5000
# Issue: No USER directive - will run as root
# Issue: No HEALTHCHECK
# Issue: Shell form (doesn't handle signals properly)
CMD python app.pyDocker Best Practices Reference
This document summarizes official Docker best practices based on current recommendations from Docker documentation and industry standards.
General Principles
1. Create Ephemeral Containers
- Containers should be as stateless and ephemeral as possible
- Should be able to stop, destroy, and recreate with minimal setup
- Align with Twelve-Factor App methodology
2. Understand Build Context
- Use
.dockerignoreto exclude unnecessary files - Keep context size minimal for faster builds
- Don't include secrets or sensitive data in context
3. Use Multi-Stage Builds
- Separate build dependencies from runtime
- Dramatically reduce final image size
- Improve security by minimizing attack surface
4. One Concern Per Container
- Each container should address a single concern
- Makes containers more reusable and easier to scale
- Simplifies debugging and updates
Dockerfile Instructions Best Practices
FROM
Use specific tags, not :latest
# Bad
FROM node:latest
# Good
FROM node:21-alpine
# Better
FROM node:21-alpine@sha256:abc123...Choose minimal base images
- Alpine Linux: ~5 MB base (vs ~80 MB for Ubuntu)
- Distroless: No shell, package manager (minimal attack surface)
- Scratch: Absolutely minimal (for static binaries)
Prefer official images
- Look for "Official Image" or "Verified Publisher" badges
- Official images are maintained and regularly updated
RUN
Chain commands to reduce layers
# Bad - creates 4 layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN curl -sL https://example.com/script.sh | bash
# Good - creates 1 layer
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
vim \
&& rm -rf /var/lib/apt/lists/* \
&& curl -sL https://example.com/script.sh | bashClean up in same layer
# Package manager cache must be removed in same RUN
RUN apt-get update && apt-get install -y \
package1 \
package2 \
&& rm -rf /var/lib/apt/lists/*
# For Alpine
RUN apk add --no-cache package1 package2Use --no-install-recommends for apt
RUN apt-get install -y --no-install-recommends packagePin package versions
# For apt
RUN apt-get install -y package=1.2.3-1
# For apk
RUN apk add package=1.2.3-r0
# For pip
RUN pip install package==1.2.3Sort multi-line arguments
RUN apt-get update && apt-get install -y \
curl \
git \
vim \
wget \
&& rm -rf /var/lib/apt/lists/*Use pipefail for pipes
RUN set -o pipefail && wget -O - https://example.com | wc -l > /numberCOPY vs ADD
Prefer COPY over ADD
# Use COPY for files and directories
COPY app.py /app/
# Only use ADD for auto-extraction or remote URLs
ADD https://example.com/file.tar.gz /tmp/Use COPY --chown to avoid extra layer
# Bad - creates extra layer
COPY app.py /app/
RUN chown user:user /app/app.py
# Good - single layer
COPY --chown=user:user app.py /app/WORKDIR
Use absolute paths
# Bad
WORKDIR app
# Good
WORKDIR /appDon't use RUN cd
# Bad
RUN cd /app && npm install
# Good
WORKDIR /app
RUN npm installUSER
Don't run as root
# Create user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Or for Alpine
RUN addgroup -g 1001 -S appuser && adduser -S appuser -u 1001
# Switch to user
USER appuserUse high UID (>10000) for better security
RUN useradd -u 10001 -m appuser
USER appuserCMD and ENTRYPOINT
Use exec form for proper signal handling
# Bad - shell form (doesn't handle signals)
CMD python app.py
# Good - exec form
CMD ["python", "app.py"]Combine ENTRYPOINT and CMD
# ENTRYPOINT defines the executable
ENTRYPOINT ["python"]
# CMD provides default arguments (can be overridden)
CMD ["app.py"]EXPOSE
Document ports even though it doesn't publish
EXPOSE 8080
EXPOSE 443HEALTHCHECK
Add health checks for services
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1LABEL
Add metadata
LABEL org.opencontainers.image.authors="team@example.com"
LABEL org.opencontainers.image.version="1.0.0"
LABEL org.opencontainers.image.description="Application description"Build Optimization
Layer Caching
Order instructions from least to most frequently changing
# 1. Base image (rarely changes)
FROM node:21-alpine
# 2. System packages (rarely change)
RUN apk add --no-cache curl
# 3. Dependencies (change occasionally)
COPY package*.json ./
RUN npm ci
# 4. Source code (changes frequently)
COPY . .Multi-Stage Builds
Separate build and runtime
# Build stage
FROM node:21 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Runtime stage
FROM node:21-alpine
COPY --from=builder /app/dist /app
CMD ["node", "/app/index.js"]BuildKit Features
Enable modern features
# syntax=docker/dockerfile:1
# Use cache mounts
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
# Use secret mounts (secrets not in final image)
RUN --mount=type=secret,id=aws,target=/root/.aws/credentials \
aws s3 cp s3://bucket/file .Security Best Practices
1. Scan Images
docker scan myimage:tag
# or
trivy image myimage:tag2. Use Minimal Base Images
- Fewer packages = fewer vulnerabilities
- Alpine, distroless, or scratch
3. Don't Store Secrets in Images
# Bad
ENV DATABASE_PASSWORD=secret123
# Good - use runtime config or secrets
# Pass at runtime: docker run -e DATABASE_PASSWORD=...4. Run as Non-Root
USER appuser5. Use Read-Only Filesystem
docker run --read-only myimage6. Limit Capabilities
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myimageCommon Anti-Patterns
❌ Using :latest tag
- Unpredictable
- Not reproducible
- Can break without warning
❌ Not cleaning package cache
# Missing cleanup increases image by hundreds of MB
RUN apt-get update && apt-get install -y package
# Missing: && rm -rf /var/lib/apt/lists/*❌ Running as root
- Security risk
- Violates principle of least privilege
❌ Installing unnecessary packages
# Bloated image
RUN apt-get install -y vim nano emacs curl wget❌ Using ADD instead of COPY
- ADD has implicit behavior
- Can extract archives unexpectedly
❌ Multiple FROM in non-multi-stage context
- Creates confusion
- Use multi-stage builds properly
Resources
Dockerfile Optimization Guide
Comprehensive guide for optimizing Docker images for size, build time, and runtime performance.
Image Size Optimization
1. Choose Minimal Base Images
Size Comparison:
ubuntu:22.04 ~80 MB
alpine:3.21 ~5 MB
distroless/base ~20 MB
scratch ~0 MB (empty)When to use each:
Alpine - General purpose minimal Linux
FROM alpine:3.21
RUN apk add --no-cache python3- ✅ Very small (5 MB)
- ✅ Has package manager
- ✅ Good for interpreted languages
- ⚠️ Uses musl libc (compatibility issues with some C libraries)
Distroless - Production containers
FROM gcr.io/distroless/python3
COPY --from=builder /app /app- ✅ No shell, package manager (secure)
- ✅ Minimal attack surface
- ✅ Small size
- ⚠️ Cannot exec into container for debugging
- ⚠️ Must use multi-stage builds
Scratch - Static binaries only
FROM scratch
COPY --from=builder /app/binary /- ✅ Absolutely minimal
- ✅ Perfect for Go, Rust static binaries
- ⚠️ No OS utilities
- ⚠️ No debug capabilities
2. Multi-Stage Builds
Problem: Build tools bloat production images
Single-stage (bloated):
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o server
CMD ["./server"]
# Result: ~1 GB (includes Go toolchain)Multi-stage (optimized):
# Build stage
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o server
# Production stage
FROM alpine:3.21
COPY --from=builder /app/server /server
CMD ["/server"]
# Result: ~10 MB (100x smaller!)3. Layer Optimization
Combine RUN commands:
# Bad - 4 layers, poor caching
RUN apt-get update
RUN apt-get install -y curl
RUN curl -O https://example.com/file
RUN rm -f file
# Good - 1 layer, cache cleaned
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& curl -O https://example.com/file \
&& rm -rf /var/lib/apt/lists/*4. Package Manager Cache Cleanup
APT (Debian/Ubuntu):
RUN apt-get update && apt-get install -y --no-install-recommends \
package1 \
package2 \
&& rm -rf /var/lib/apt/lists/*- Saves ~100-200 MB per layer
- Must be in same RUN command
APK (Alpine):
RUN apk add --no-cache package1 package2- Doesn't create cache at all
- Or:
apk add package && rm -rf /var/cache/apk/*
YUM/DNF (RHEL/Fedora):
RUN yum install -y package \
&& yum clean all \
&& rm -rf /var/cache/yumPip (Python):
RUN pip install --no-cache-dir packageNPM (Node.js):
RUN npm ci --only=production
# Or with cache mount:
RUN --mount=type=cache,target=/root/.npm \
npm ci --only=production5. Use .dockerignore
Problem: Entire project copied into image
.dockerignore contents:
.git/
node_modules/
*.log
.env
tests/
docs/
README.mdImpact:
- Faster builds (smaller context)
- Smaller images (fewer files)
- Prevents accidental secret leaks
Build Time Optimization
1. Leverage Build Cache
Order matters - least to most frequently changing:
# 1. Base image (rarely changes)
FROM node:21-alpine
# 2. System dependencies (rarely change)
RUN apk add --no-cache curl
# 3. Application dependencies (change occasionally)
COPY package*.json ./
RUN npm ci
# 4. Application code (changes frequently)
COPY . .
RUN npm run buildWhy this works:
- Docker caches each layer
- Layers rebuild when files change
- Putting frequently-changing files last preserves cache for earlier layers
2. BuildKit Cache Mounts
Enable BuildKit:
export DOCKER_BUILDKIT=1Use cache mounts:
# syntax=docker/dockerfile:1
# Python with pip cache
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
# Node.js with npm cache
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Go with module cache
RUN --mount=type=cache,target=/go/pkg/mod \
go build -o appBenefits:
- Persistent cache across builds
- Dramatically faster dependency installation
- Shared cache between projects
3. Parallel Multi-Stage Builds
# These stages run in parallel
FROM alpine AS fetch-1
RUN wget https://example.com/file1
FROM alpine AS fetch-2
RUN wget https://example.com/file2
# This stage waits for both
FROM alpine
COPY --from=fetch-1 /file1 .
COPY --from=fetch-2 /file2 .Runtime Performance Optimization
1. Exec Form for CMD/ENTRYPOINT
# Bad - shell form (extra shell process)
CMD python app.py
# Good - exec form (direct execution)
CMD ["python", "app.py"]Benefits:
- Faster startup (no shell)
- Proper signal handling (SIGTERM)
- Lower memory usage
2. Health Checks
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD curl -f http://localhost:8080/health || exit 1Benefits:
- Container orchestrators can detect unhealthy containers
- Automatic restarts
- Better uptime
3. Resource Awareness
# Use all available CPUs
ENV GOMAXPROCS=0
# Or limit to specific count
ENV GOMAXPROCS=4Language-Specific Optimizations
Node.js
FROM node:21-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:21-alpine
COPY --from=builder /app/node_modules ./node_modules
COPY . .
USER node
CMD ["node", "server.js"]Tips:
- Use
npm ciinstead ofnpm install - Install only production dependencies
- Use Alpine variant (node:21-alpine vs node:21 = 150MB vs 900MB)
Python
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
FROM python:3.12-slim
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
COPY . .
USER nobody
CMD ["python", "app.py"]Tips:
- Use slim variant (python:3.12-slim vs python:3.12 = 50MB vs 1GB)
- Install to --user to copy to final stage
- Use --no-cache-dir to avoid pip cache
Go
FROM golang:1.21-alpine AS builder
WORKDIR /src
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app
FROM scratch
COPY --from=builder /app /app
ENTRYPOINT ["/app"]Tips:
- Use scratch for static binaries
- Disable CGO for static linking
- Use
-ldflags="-s -w"to strip debug info (smaller binary)
Java
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package
FROM eclipse-temurin:21-jre-alpine
COPY --from=builder /app/target/*.jar /app.jar
CMD ["java", "-jar", "/app.jar"]Tips:
- Use JRE instead of JDK for runtime (smaller)
- Download dependencies separately for caching
- Consider custom JRE with jlink for minimal image
Advanced Techniques
1. Multi-Architecture Builds
docker buildx build --platform linux/amd64,linux/arm64 -t myapp .2. Build Secrets
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm cidocker build --secret id=npmrc,src=$HOME/.npmrc .Benefits:
- Secrets not in final image
- Not in build history
- Secure credential usage
3. SSH Mounts
RUN --mount=type=ssh \
git clone git@github.com:private/repo.gitdocker build --ssh default .4. Layer Squashing
docker build --squash -t myapp .Benefits:
- Single layer in final image
- Smaller size if cleanup commands are separate
Drawbacks:
- Loses layer caching benefits
- Slower rebuilds
Optimization Checklist
- [ ] Use minimal base image (Alpine, distroless, scratch)
- [ ] Implement multi-stage builds
- [ ] Combine RUN commands
- [ ] Clean package manager cache
- [ ] Order layers by change frequency
- [ ] Use BuildKit cache mounts
- [ ] Create .dockerignore file
- [ ] Use exec form for CMD/ENTRYPOINT
- [ ] Add HEALTHCHECK for services
- [ ] Pin dependency versions
- [ ] Remove development dependencies
- [ ] Use --no-install-recommends for apt
- [ ] Consider language-specific optimizations
- [ ] Enable BuildKit features
Measuring Optimization
Before Optimization
docker images myapp
# REPOSITORY TAG SIZE
# myapp latest 1.2GBAfter Optimization
docker images myapp-optimized
# REPOSITORY TAG SIZE
# myapp-optimized latest 50MBBuild Time Comparison
time docker build -t myapp .
# real 5m30s
time docker build -t myapp-optimized .
# real 0m45s (with cache)Tools for Analysis
dive - Layer Analysis
dive myapp:latest- Shows layer-by-layer size
- Identifies wasted space
- Suggests optimizations
docker history
docker history myapp:latest- Shows each layer's size
- Identifies large layers
docker scout
docker scout cves myapp:latest- Scans for vulnerabilities
- Recommends base image updates
Resources
Container Security Checklist
A comprehensive security checklist for Dockerfiles and container images.
Build-Time Security
Base Image Security
- [ ] Use official or verified base images
- [ ] Pin base image to specific tag (not :latest)
- [ ] Consider digest pinning for critical applications
- [ ] Prefer minimal base images (Alpine, distroless, scratch)
- [ ] Scan base images for known vulnerabilities
- [ ] Keep base images updated regularly
Secrets Management
- [ ] Never hardcode secrets in Dockerfile
- [ ] Don't use ENV or ARG for sensitive data
- [ ] Use Docker build secrets (--secret flag)
- [ ] Use runtime configuration for secrets
- [ ] Scan for accidentally committed secrets
- [ ] Use .dockerignore to exclude secret files
Package Management
- [ ] Pin package versions for reproducibility
- [ ] Only install necessary packages (--no-install-recommends)
- [ ] Clean package manager cache in same layer
- [ ] Verify package signatures when possible
- [ ] Use official package repositories
- [ ] Audit dependencies for known vulnerabilities
User and Permissions
- [ ] Create and use non-root user
- [ ] Set USER directive before CMD/ENTRYPOINT
- [ ] Use high UID (>10000) for better isolation
- [ ] Set proper file ownership with COPY --chown
- [ ] Don't use sudo in containers
- [ ] Avoid privileged operations
Layer and File Security
- [ ] Use .dockerignore to exclude sensitive files
- [ ] Don't copy unnecessary files (use specific COPY)
- [ ] Remove secrets after use in same layer
- [ ] Don't log sensitive information
- [ ] Minimize number of layers
- [ ] Use multi-stage builds to exclude build secrets
Common Vulnerabilities
SSH/Remote Access
- [ ] Don't install or expose SSH (port 22)
- [ ] Don't install telnet, FTP, or other insecure protocols
- [ ] Use
docker execfor debugging instead of SSH - [ ] Don't run sshd in containers
Network Exposure
- [ ] Only EXPOSE necessary ports
- [ ] Don't bind to 0.0.0.0 in development images
- [ ] Use internal networks for inter-container communication
- [ ] Implement proper firewall rules
- [ ] Use TLS for network communications
File System Security
- [ ] Consider read-only root filesystem
- [ ] Use tmpfs for temporary files
- [ ] Set proper file permissions
- [ ] Don't store secrets in environment variables
- [ ] Use volume mounts for sensitive data
Runtime Security
Container Configuration
- [ ] Run with --read-only flag when possible
- [ ] Drop unnecessary capabilities (--cap-drop)
- [ ] Use security profiles (AppArmor, SELinux)
- [ ] Set resource limits (CPU, memory)
- [ ] Use user namespaces
- [ ] Enable content trust (DOCKER_CONTENT_TRUST)
Health and Monitoring
- [ ] Implement HEALTHCHECK in Dockerfile
- [ ] Monitor container logs
- [ ] Set up security scanning in CI/CD
- [ ] Use runtime security tools
- [ ] Monitor for anomalous behavior
- [ ] Implement proper logging without secrets
Network Security
- [ ] Use custom bridge networks
- [ ] Implement network segmentation
- [ ] Use encrypted overlays for swarm
- [ ] Configure DNS properly
- [ ] Use service mesh for microservices
- [ ] Implement network policies
Image Registry Security
Registry Configuration
- [ ] Use private registries for internal images
- [ ] Enable image scanning in registry
- [ ] Implement access controls
- [ ] Use image signing (Docker Content Trust)
- [ ] Scan for vulnerabilities before pull
- [ ] Regularly update registry software
Image Distribution
- [ ] Sign images before distribution
- [ ] Verify image signatures on pull
- [ ] Use TLS for registry communication
- [ ] Implement role-based access control
- [ ] Audit image pull/push events
- [ ] Use image provenance metadata
Security Scanning Tools
Static Analysis
- hadolint - Dockerfile linting
- Checkov - Policy-as-code scanning
- dockerfilelint - Best practices checker
Vulnerability Scanning
- Trivy - Comprehensive vulnerability scanner
- Snyk - Dependency vulnerability scanner
- Clair - Container vulnerability analysis
- Anchore - Deep image inspection
Runtime Security
- Falco - Runtime threat detection
- Aqua Security - Container security platform
- Sysdig - Container monitoring and security
Compliance and Standards
Industry Standards
- [ ] Follow CIS Docker Benchmark
- [ ] Comply with NIST guidelines
- [ ] Adhere to OWASP Container Security
- [ ] Meet PCI DSS requirements (if applicable)
- [ ] Follow SOC 2 controls (if applicable)
Security Policies
- [ ] Document security requirements
- [ ] Implement security review process
- [ ] Define incident response procedures
- [ ] Regular security audits
- [ ] Security training for developers
- [ ] Maintain security documentation
Quick Security Wins
Easy Fixes
1. Use specific base image tags
FROM alpine:3.21 # Not alpine:latest2. Run as non-root
USER appuser3. Clean package cache
RUN apk add --no-cache package4. Don't expose unnecessary ports
# Only expose what's needed
EXPOSE 80805. Add health checks
HEALTHCHECK CMD curl -f http://localhost/ || exit 1Security Checklist Summary
| Category | Critical | High | Medium |
|---|---|---|---|
| Base Image | Use official, pin version | Scan for CVEs | Update regularly |
| Secrets | Never in code | Use secrets mgmt | Scan commits |
| Users | Run as non-root | High UID | Proper permissions |
| Network | TLS only | Minimal exposure | Firewall rules |
| Runtime | Drop capabilities | Read-only FS | Resource limits |
Resources
#!/bin/bash
################################################################################
# Dockerfile Validator - Complete Lifecycle Management
#
# Single self-contained script that handles:
# - Tool installation (hadolint + Checkov in Python venvs)
# - Syntax validation (hadolint)
# - Security scanning (Checkov)
# - Best practices validation (custom checks)
# - Optimization analysis (custom checks)
# - Automatic cleanup on exit (success or failure)
#
# Usage: ./dockerfile-validate.sh [Dockerfile]
################################################################################
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
PURPLE='\033[0;35m'
BOLD='\033[1m'
NC='\033[0m'
# Configuration
DOCKERFILE="${1:-Dockerfile}"
VENV_BASE_DIR=""
HADOLINT_VENV=""
CHECKOV_VENV=""
TEMP_INSTALL=false
HADOLINT_CMD=""
CHECKOV_CMD=""
HADOLINT_MISSING=false
CHECKOV_MISSING=false
# Environment variable to force temporary installation (for testing cleanup)
# Usage: FORCE_TEMP_INSTALL=true bash scripts/dockerfile-validate.sh Dockerfile
FORCE_TEMP_INSTALL="${FORCE_TEMP_INSTALL:-false}"
# Exit codes
EXIT_CODE=0
# Counters for custom checks
BP_ERRORS=0
BP_WARNINGS=0
BP_INFO=0
BP_HAS_WARNINGS=false # set true when BP has warnings but no errors (drives WARN summary state)
RUN_COUNT=0
################################################################################
# Cleanup Function - Called on EXIT
################################################################################
cleanup() {
local exit_code=$?
if [ "$TEMP_INSTALL" = true ] && [ -n "$VENV_BASE_DIR" ]; then
echo ""
echo -e "${YELLOW}Cleaning up temporary installation...${NC}"
if [ -d "$VENV_BASE_DIR" ]; then
rm -rf "$VENV_BASE_DIR"
echo -e "${GREEN}✓ Removed temporary venvs${NC}"
fi
echo -e "${GREEN}✓ Cleanup complete${NC}"
fi
exit $exit_code
}
# Set trap for cleanup on any exit
trap cleanup EXIT INT TERM
################################################################################
# Tool Installation Functions
################################################################################
check_python() {
if command -v python3 &> /dev/null; then
PYTHON_CMD="python3"
elif command -v python &> /dev/null; then
PYTHON_CMD="python"
else
echo -e "${RED}ERROR: Python 3 is required but not installed${NC}" >&2
exit 2
fi
# Verify Python version (need 3.8+)
PYTHON_VERSION=$($PYTHON_CMD --version 2>&1 | awk '{print $2}')
MAJOR=$(echo $PYTHON_VERSION | cut -d. -f1)
MINOR=$(echo $PYTHON_VERSION | cut -d. -f2)
if [ "$MAJOR" -lt 3 ] || ([ "$MAJOR" -eq 3 ] && [ "$MINOR" -lt 8 ]); then
echo -e "${RED}ERROR: Python 3.8+ required (found $PYTHON_VERSION)${NC}" >&2
exit 2
fi
}
check_tools() {
# If FORCE_TEMP_INSTALL is set, skip tool check and force installation
if [ "$FORCE_TEMP_INSTALL" = "true" ]; then
echo -e "${YELLOW}FORCE_TEMP_INSTALL=true: Forcing temporary tool installation for testing${NC}"
HADOLINT_MISSING=true
CHECKOV_MISSING=true
return 1
fi
HADOLINT_MISSING=false
CHECKOV_MISSING=false
# Check for hadolint (system-installed)
if command -v hadolint &> /dev/null; then
HADOLINT_CMD="hadolint"
else
HADOLINT_MISSING=true
fi
# Check for Checkov (system-installed)
if command -v checkov &> /dev/null; then
CHECKOV_CMD="checkov"
else
CHECKOV_MISSING=true
fi
# Return 0 if both found, 1 if installation needed
[ "$HADOLINT_MISSING" = false ] && [ "$CHECKOV_MISSING" = false ]
}
install_hadolint() {
echo -e "${BLUE}Installing hadolint...${NC}"
mkdir -p "$HADOLINT_VENV"
$PYTHON_CMD -m venv "$HADOLINT_VENV" 2>&1 | grep -v "upgrade pip" || true
"$HADOLINT_VENV/bin/pip" install --quiet --upgrade pip
"$HADOLINT_VENV/bin/pip" install --quiet hadolint-bin
if "$HADOLINT_VENV/bin/hadolint" --version &> /dev/null; then
HADOLINT_CMD="$HADOLINT_VENV/bin/hadolint"
VERSION=$("$HADOLINT_VENV/bin/hadolint" --version | head -n1)
echo -e "${GREEN}✓ hadolint installed: $VERSION${NC}"
else
echo -e "${RED}✗ hadolint installation failed${NC}" >&2
exit 2
fi
}
install_checkov() {
echo -e "${BLUE}Installing Checkov...${NC}"
mkdir -p "$CHECKOV_VENV"
$PYTHON_CMD -m venv "$CHECKOV_VENV" 2>&1 | grep -v "upgrade pip" || true
"$CHECKOV_VENV/bin/pip" install --quiet --upgrade pip
"$CHECKOV_VENV/bin/pip" install --quiet checkov
if "$CHECKOV_VENV/bin/checkov" --version &> /dev/null; then
CHECKOV_CMD="$CHECKOV_VENV/bin/checkov"
VERSION=$("$CHECKOV_VENV/bin/checkov" --version 2>&1)
echo -e "${GREEN}✓ Checkov installed: $VERSION${NC}"
else
echo -e "${RED}✗ Checkov installation failed${NC}" >&2
exit 2
fi
}
install_tools() {
if [ "$HADOLINT_MISSING" = false ] && [ "$CHECKOV_MISSING" = false ]; then
return 0
fi
echo -e "${YELLOW}${BOLD}Installing validation tools...${NC}"
echo ""
TEMP_INSTALL=true
VENV_BASE_DIR=$(mktemp -d "${TMPDIR:-/tmp}/dockerfile-validator.XXXXXX")
HADOLINT_VENV="${VENV_BASE_DIR}/hadolint-venv"
CHECKOV_VENV="${VENV_BASE_DIR}/checkov-venv"
check_python
if [ "$HADOLINT_MISSING" = true ]; then
install_hadolint
fi
if [ "$CHECKOV_MISSING" = true ]; then
install_checkov
fi
echo ""
}
################################################################################
# Dockerfile Preprocessing - Handle Multi-line Instructions
################################################################################
# Normalize Dockerfile by joining continuation lines (lines ending with \)
# This allows accurate counting of multi-line instructions
normalize_dockerfile() {
local dockerfile="$1"
# Use awk to join lines that end with backslash
awk '
/\\$/ {
sub(/\\$/, "")
printf "%s", $0
next
}
{ print }
' "$dockerfile"
}
count_instruction() {
local content="$1"
local instruction="$2"
printf '%s\n' "$content" | awk -v instruction="$instruction" '
BEGIN { IGNORECASE=1 }
$0 ~ "^[[:space:]]*" instruction "[[:space:]]" { count++ }
END { print count + 0 }
'
}
from_images() {
local content="$1"
printf '%s\n' "$content" | awk '
BEGIN { IGNORECASE=1 }
function parse_from_image(line, n, token, i) {
sub(/^[[:space:]]*FROM[[:space:]]+/, "", line)
sub(/[[:space:]]+#.*/, "", line)
n = split(line, token, /[[:space:]]+/)
i = 1
while (i <= n && token[i] ~ /^--/) {
# Handle both --platform=<value> and --platform <value> forms.
if (token[i] == "--platform" && i < n) {
i += 2
continue
}
i++
}
if (i <= n) {
return token[i]
}
return ""
}
/^[[:space:]]*FROM[[:space:]]+/ {
image = parse_from_image($0)
if (image != "") {
print image
}
}
'
}
final_from_image() {
local content="$1"
from_images "$content" | tail -n1
}
is_nonroot_base_image() {
local image="$1"
if echo "$image" | grep -qiE 'distroless[^[:space:]]*:nonroot|:nonroot$'; then
return 0
fi
return 1
}
################################################################################
# Validation Functions
################################################################################
run_hadolint() {
echo -e "${CYAN}${BOLD}[1/4] Syntax Validation (hadolint)${NC}"
echo "====================================="
echo ""
if "$HADOLINT_CMD" "$DOCKERFILE" 2>&1; then
echo ""
echo -e "${GREEN}✓ Syntax validation passed${NC}"
return 0
else
local hadolint_exit=$?
echo ""
echo -e "${YELLOW}⚠ Syntax issues found${NC}"
EXIT_CODE=1
return $hadolint_exit
fi
}
run_checkov() {
echo -e "${CYAN}${BOLD}[2/4] Security Scan (Checkov)${NC}"
echo "================================"
echo ""
if "$CHECKOV_CMD" -f "$DOCKERFILE" --framework dockerfile --compact 2>&1; then
echo ""
echo -e "${GREEN}✓ Security scan passed${NC}"
return 0
else
local checkov_exit=$?
echo ""
echo -e "${YELLOW}⚠ Security issues found${NC}"
EXIT_CODE=1
return $checkov_exit
fi
}
run_best_practices() {
echo -e "${CYAN}${BOLD}[3/4] Best Practices Validation${NC}"
echo "===================================="
echo ""
# Reset counters
BP_ERRORS=0
BP_WARNINGS=0
BP_INFO=0
# Create normalized version for accurate multi-line instruction counting
local normalized_content
normalized_content=$(normalize_dockerfile "$DOCKERFILE")
local final_image
final_image=$(final_from_image "$normalized_content")
# Check for :latest tag
if grep -qiE "^[[:space:]]*FROM[[:space:]]+[^[:space:]]+:latest([[:space:]]|$)" "$DOCKERFILE"; then
echo -e "${YELLOW}[WARNING] Base image using :latest tag${NC}"
echo " → Use specific version tags for reproducibility"
((BP_WARNINGS++))
fi
# Check for USER directive
if ! grep -qiE "^[[:space:]]*USER[[:space:]]+" "$DOCKERFILE"; then
if is_nonroot_base_image "$final_image"; then
echo -e "${PURPLE}[INFO] No USER directive, but final base image is non-root: $final_image${NC}"
echo " → Confirm runtime user requirements for your platform"
((BP_INFO++))
else
echo -e "${YELLOW}[WARNING] No USER directive - container will run as root${NC}"
echo " → Add 'USER <non-root-user>' before CMD/ENTRYPOINT"
((BP_WARNINGS++))
fi
else
LAST_USER=$(grep -iE "^[[:space:]]*USER[[:space:]]+" "$DOCKERFILE" | tail -n1 | awk '{print $2}')
LAST_USER_LOWER=$(echo "$LAST_USER" | tr '[:upper:]' '[:lower:]')
if [[ "$LAST_USER_LOWER" == "root" ]] || [[ "$LAST_USER" == "0" ]] || [[ "$LAST_USER" == "0:0" ]]; then
echo -e "${RED}[ERROR] Last USER directive sets user to root${NC}"
echo " → Container should not run as root user"
((BP_ERRORS++))
EXIT_CODE=1
fi
fi
# Check for HEALTHCHECK
if ! grep -qiE "^[[:space:]]*HEALTHCHECK[[:space:]]+" "$DOCKERFILE"; then
if grep -qiE "^[[:space:]]*EXPOSE[[:space:]]+|^[[:space:]]*(CMD|ENTRYPOINT)[[:space:]].*server" "$DOCKERFILE"; then
echo -e "${PURPLE}[INFO] No HEALTHCHECK defined for service container${NC}"
echo " → Consider adding HEALTHCHECK for monitoring"
((BP_INFO++))
fi
fi
# Check RUN command efficiency (using normalized content for accurate counting)
RUN_COUNT=$(count_instruction "$normalized_content" "RUN")
if [ "$RUN_COUNT" -gt "5" ]; then
echo -e "${PURPLE}[INFO] High number of RUN commands ($RUN_COUNT)${NC}"
echo " → Consider combining related commands to reduce layers"
((BP_INFO++))
fi
# Check for apt-get cache cleanup (must happen in same RUN instruction)
APT_INSTALL_WITHOUT_CLEAN=$(printf '%s\n' "$normalized_content" | awk '
BEGIN { IGNORECASE=1 }
/^[[:space:]]*RUN[[:space:]]+/ && /apt-get[[:space:]]+install/ {
has_clean = ($0 ~ /rm[[:space:]]+-rf[[:space:]]+\/var\/lib\/apt\/lists/ || $0 ~ /apt-get[[:space:]]+clean/)
if (!has_clean) { count++ }
}
END { print count + 0 }
')
if [ "$APT_INSTALL_WITHOUT_CLEAN" -gt 0 ]; then
echo -e "${YELLOW}[WARNING] apt-get install found without same-layer cache cleanup${NC}"
echo " → Add '&& rm -rf /var/lib/apt/lists/*' to the same RUN instruction"
((BP_WARNINGS++))
fi
# Check for apk --no-cache (using normalized content)
APK_ADD_WITHOUT_NOCACHE=$(printf '%s\n' "$normalized_content" | awk '
BEGIN { IGNORECASE=1 }
/^[[:space:]]*RUN[[:space:]]+/ && /apk[[:space:]]+add/ {
has_no_cache = ($0 ~ /apk[[:space:]]+add[^#]*--no-cache/)
has_manual_cleanup = ($0 ~ /rm[[:space:]]+-rf[[:space:]]+\/var\/cache\/apk/)
if (!has_no_cache && !has_manual_cleanup) { count++ }
}
END { print count + 0 }
')
if [ "$APK_ADD_WITHOUT_NOCACHE" -gt 0 ]; then
echo -e "${YELLOW}[WARNING] apk add without --no-cache or manual cache cleanup${NC}"
echo " → Use 'apk add --no-cache' to avoid cache in image"
((BP_WARNINGS++))
fi
# Check for hardcoded secrets (ignore comments)
# Use tolower() instead of IGNORECASE=1 for ~ operator: BSD awk (macOS) does not
# honour IGNORECASE when using the dynamic ~ operator, only for literal /patterns/.
# Dockerfile convention is UPPERCASE variable names (ENV PASSWORD=, ENV API_KEY=),
# so without tolower() all secrets are silently missed on macOS.
if awk '
/^[[:space:]]*#/ { next }
/^[[:space:]]*(ENV|ARG)[[:space:]]/ {
lower = tolower($0)
if (lower ~ /(password|secret|api_key|api-key|apikey|token)[[:space:]]*=/) {
found=1
}
}
END { exit found ? 0 : 1 }
' "$DOCKERFILE"; then
echo -e "${RED}[ERROR] Potential hardcoded secrets in ENV/ARG${NC}"
echo " → Never hardcode secrets in Dockerfiles"
((BP_ERRORS++))
EXIT_CODE=1
fi
# Check for poor COPY ordering (COPY . before dependency installation)
# This hurts build cache efficiency - dependencies should be copied first.
# Stage-aware: resets tracking on each FROM so a COPY . in a builder stage
# does not produce a false positive against installs in a separate runtime stage.
COPY_ORDER_ISSUE=$(printf '%s\n' "$normalized_content" | awk '
{
lower = tolower($0)
# New build stage — reset per-stage COPY . tracking
if (lower ~ /^[[:space:]]*from[[:space:]]+/) {
stage_copy_line = 0
next
}
# Skip comment lines
if (lower ~ /^[[:space:]]*#/) { next }
# Record the first COPY . in the current stage (ignore COPY --from=)
if (stage_copy_line == 0 && lower ~ /^[[:space:]]*copy[[:space:]]+/) {
stripped = lower
sub(/^[[:space:]]*copy[[:space:]]+/, "", stripped)
while (stripped ~ /^--[^[:space:]]+[[:space:]]+/) {
sub(/^--[^[:space:]]+[[:space:]]+/, "", stripped)
}
split(stripped, parts, /[[:space:]]+/)
if (parts[1] == "." || parts[1] == "./") {
stage_copy_line = NR
}
}
# Flag if a package install follows COPY . within the same stage
if (stage_copy_line > 0 && NR > stage_copy_line && lower ~ /^[[:space:]]*run[[:space:]]+/) {
if (lower ~ /pip[[:space:]]+install|npm[[:space:]]+(install|ci)|yarn([[:space:]]|$)|go[[:space:]]+mod[[:space:]]|apt-get[[:space:]]+install|apk[[:space:]]+add/) {
found = 1
exit
}
}
}
END { print found + 0 }
')
if [ "$COPY_ORDER_ISSUE" -gt 0 ]; then
echo -e "${YELLOW}[WARNING] COPY . appears before dependency installation${NC}"
echo " → Copy dependency files (package.json, requirements.txt) first for better cache"
echo " → Then install dependencies, then COPY . for source code"
((BP_WARNINGS++))
fi
echo ""
echo "Best Practices Summary:"
echo -e " Errors: ${RED}$BP_ERRORS${NC}"
echo -e " Warnings: ${YELLOW}$BP_WARNINGS${NC}"
echo -e " Info: ${PURPLE}$BP_INFO${NC}"
echo ""
if [ $BP_ERRORS -eq 0 ] && [ $BP_WARNINGS -eq 0 ]; then
echo -e "${GREEN}✓ Best practices validation passed${NC}"
return 0
elif [ $BP_ERRORS -eq 0 ]; then
echo -e "${YELLOW}⚠ Best practices completed with warnings${NC}"
BP_HAS_WARNINGS=true
return 0
else
echo -e "${RED}✗ Best practices validation failed${NC}"
return 1
fi
}
run_optimization() {
echo -e "${CYAN}${BOLD}[4/4] Optimization Analysis${NC}"
echo "=============================="
echo ""
# Create normalized version for accurate multi-line instruction counting
local normalized_content
normalized_content=$(normalize_dockerfile "$DOCKERFILE")
# Analyze base images
BASE_IMAGES=$(from_images "$normalized_content")
echo -e "${BLUE}Base Image Analysis:${NC}"
for image in $BASE_IMAGES; do
if echo "$image" | grep -qi "distroless"; then
continue
fi
if echo "$image" | grep -qiE "ubuntu|debian|centos|fedora"; then
echo -e " ${PURPLE}[OPTIMIZATION] Consider Alpine alternative for: $image${NC}"
echo " → Alpine images are 10-100x smaller"
fi
done
echo ""
# Multi-stage analysis (using normalized content)
FROM_COUNT=$(count_instruction "$normalized_content" "FROM")
echo -e "${BLUE}Build Structure:${NC}"
if [ "$FROM_COUNT" -eq "1" ]; then
if echo "$normalized_content" | grep -qiE "apt-get install.*(gcc|make|build)" || \
echo "$normalized_content" | grep -qiE "apk add.*(gcc|make|build)"; then
echo -e " ${PURPLE}[OPTIMIZATION] Build tools detected in single-stage build${NC}"
echo " → Consider multi-stage build to exclude build tools from final image"
fi
else
FINAL_FROM=$(final_from_image "$normalized_content")
if echo "$FINAL_FROM" | grep -qiE "distroless|alpine|scratch"; then
echo -e " ${GREEN}✓ Using minimal base for final stage: $FINAL_FROM${NC}"
else
echo -e " ${PURPLE}[OPTIMIZATION] Final stage could use smaller base image${NC}"
echo " → Consider: alpine, distroless, or scratch"
fi
fi
echo ""
# Layer count (reuse RUN_COUNT from best practices if available, otherwise calculate)
RUN_COUNT=$(count_instruction "$normalized_content" "RUN")
echo -e "${BLUE}Layer Optimization:${NC}"
echo " RUN commands: $RUN_COUNT"
if [ "$RUN_COUNT" -gt "7" ]; then
echo -e " ${PURPLE}[OPTIMIZATION] Consider combining RUN commands${NC}"
echo " → Reduces layer count and image size"
fi
echo ""
# .dockerignore check
DOCKERFILE_DIR=$(dirname "$DOCKERFILE")
if [ ! -f "$DOCKERFILE_DIR/.dockerignore" ]; then
echo -e "${YELLOW}[INFO] No .dockerignore file found${NC}"
echo " → Create .dockerignore to optimize build context"
echo ""
fi
echo -e "${GREEN}✓ Optimization analysis complete${NC}"
return 0
}
################################################################################
# Main Execution
################################################################################
show_help() {
# Use echo -e so that BOLD/NC variables (which hold \033[...m escape sequences)
# are interpreted correctly. cat << EOF expands variables but does not process
# \033 escape sequences, causing raw escape codes to appear in the output.
echo -e "${BOLD}Dockerfile Validator - Complete Lifecycle${NC}"
echo ""
echo "Validates Dockerfiles with automatic tool management and cleanup."
echo ""
echo -e "${BOLD}Usage:${NC}"
echo " $(basename "$0") [Dockerfile]"
echo ""
echo -e "${BOLD}Validation Stages:${NC}"
echo " 1. Syntax validation (hadolint)"
echo " 2. Security scanning (Checkov)"
echo " 3. Best practices validation"
echo " 4. Optimization analysis"
echo ""
echo -e "${BOLD}Features:${NC}"
echo " • Auto-installs tools if not found"
echo " • Runs all validation stages"
echo " • Auto-cleanup on exit"
echo ""
echo -e "${BOLD}Examples:${NC}"
echo " $(basename "$0") # Validate ./Dockerfile"
echo " $(basename "$0") Dockerfile.prod # Validate specific file"
echo ""
echo -e "${BOLD}Exit Codes:${NC}"
echo " 0 All validations passed"
echo " 1 One or more validations failed"
echo " 2 Critical error"
echo ""
}
# Check for help
ARG1="${1:-}"
if [[ "$ARG1" == "-h" ]] || [[ "$ARG1" == "--help" ]]; then
show_help
exit 0
fi
# Validate input
if [ ! -f "$DOCKERFILE" ]; then
echo -e "${RED}ERROR: Dockerfile not found: $DOCKERFILE${NC}" >&2
echo ""
echo "Usage: $(basename "$0") [Dockerfile]"
exit 2
fi
# Print header
echo ""
echo -e "${CYAN}${BOLD}========================================${NC}"
echo -e "${CYAN}${BOLD} Dockerfile Validator${NC}"
echo -e "${CYAN}${BOLD}========================================${NC}"
echo ""
echo -e "${BOLD}Target:${NC} $DOCKERFILE"
echo -e "${BOLD}Date:${NC} $(date '+%Y-%m-%d %H:%M:%S')"
echo ""
# Check and install tools if needed
if ! check_tools; then
install_tools
fi
echo -e "${CYAN}${BOLD}Running Validations...${NC}"
echo ""
# Track results
HADOLINT_RESULT="SKIP"
CHECKOV_RESULT="SKIP"
BEST_PRACTICES_RESULT="SKIP"
OPTIMIZATION_RESULT="SKIP"
# Run all validations
run_hadolint && HADOLINT_RESULT="PASS" || HADOLINT_RESULT="FAIL"
echo ""
run_checkov && CHECKOV_RESULT="PASS" || CHECKOV_RESULT="FAIL"
echo ""
run_best_practices && {
[ "$BP_HAS_WARNINGS" = true ] && BEST_PRACTICES_RESULT="WARN" || BEST_PRACTICES_RESULT="PASS"
} || BEST_PRACTICES_RESULT="FAIL"
echo ""
run_optimization && OPTIMIZATION_RESULT="INFO"
echo ""
# Print summary
echo -e "${CYAN}${BOLD}========================================${NC}"
echo -e "${CYAN}${BOLD} Validation Summary${NC}"
echo -e "${CYAN}${BOLD}========================================${NC}"
echo ""
# Print results
[ "$HADOLINT_RESULT" = "PASS" ] && echo -e " Syntax (hadolint): ${GREEN}✓ PASSED${NC}" || echo -e " Syntax (hadolint): ${RED}✗ FAILED${NC}"
[ "$CHECKOV_RESULT" = "PASS" ] && echo -e " Security (Checkov): ${GREEN}✓ PASSED${NC}" || echo -e " Security (Checkov): ${RED}✗ FAILED${NC}"
if [ "$BEST_PRACTICES_RESULT" = "PASS" ]; then
echo -e " Best Practices: ${GREEN}✓ PASSED${NC}"
elif [ "$BEST_PRACTICES_RESULT" = "WARN" ]; then
echo -e " Best Practices: ${YELLOW}⚠ WARNED${NC}"
else
echo -e " Best Practices: ${RED}✗ FAILED${NC}"
fi
echo -e " Optimization: ${BLUE}ℹ INFORMATIONAL${NC}"
echo ""
echo -e "${CYAN}${BOLD}========================================${NC}"
echo ""
# Overall result
if [ $EXIT_CODE -eq 0 ]; then
echo -e "${GREEN}${BOLD}✓ Overall Result: PASSED${NC}"
echo ""
echo "Your Dockerfile meets validation requirements."
else
echo -e "${RED}${BOLD}✗ Overall Result: FAILED${NC}"
echo ""
echo "Please address the issues identified above."
fi
echo ""
# Exit (cleanup trap will run automatically)
exit $EXIT_CODE
#!/usr/bin/env bash
#
# CI-friendly regression entrypoint for dockerfile-validator.
# Runs syntax checks, regression tests, and optional ShellCheck linting.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
readonly SKILL_DIR
VALIDATOR_SCRIPT="$SKILL_DIR/scripts/dockerfile-validate.sh"
REGRESSION_SCRIPT="$SKILL_DIR/tests/test_regression.sh"
STRICT_SHELLCHECK="${STRICT_SHELLCHECK:-false}"
echo "Running dockerfile-validator CI checks..."
# Fast syntax guard for scripts used by the regression suite.
bash -n "$VALIDATOR_SCRIPT" "$REGRESSION_SCRIPT" "$0"
# Regression coverage for parser/best-practice branches.
bash "$REGRESSION_SCRIPT"
if command -v shellcheck >/dev/null 2>&1; then
shellcheck "$VALIDATOR_SCRIPT" "$REGRESSION_SCRIPT" "$0"
echo "ShellCheck: PASS"
elif [[ "$STRICT_SHELLCHECK" == "true" ]]; then
echo "ShellCheck: required but not installed (STRICT_SHELLCHECK=true)" >&2
exit 1
else
echo "ShellCheck: SKIP (not installed; set STRICT_SHELLCHECK=true to require it)"
fi
echo "PASS: dockerfile-validator CI checks"
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN cat yarn.lock >/dev/null 2>&1 || true
USER 1000
CMD ["node", "server.js"]
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN yarn
USER 1000
CMD ["node", "server.js"]
FROM --platform=linux/amd64 golang:1.24-alpine AS build
WORKDIR /src
RUN printf '#!/bin/sh\necho hello\n' > /out && chmod +x /out
FROM --platform linux/amd64 gcr.io/distroless/static-debian11:nonroot AS runtime
COPY --from=build /out /out
ENTRYPOINT ["/out"]
#!/usr/bin/env bash
set -euo pipefail
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VALIDATOR="$SKILL_DIR/scripts/dockerfile-validate.sh"
FIXTURES_DIR="$SKILL_DIR/tests/fixtures"
TMP_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
PASS=0
FAIL=0
pass() {
echo " PASS: $1"
PASS=$((PASS + 1))
}
fail() {
echo " FAIL: $1"
FAIL=$((FAIL + 1))
}
make_stub_tools() {
mkdir -p "$TMP_DIR/bin"
cat > "$TMP_DIR/bin/hadolint" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
exit 0
EOF
cat > "$TMP_DIR/bin/checkov" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
exit 0
EOF
chmod +x "$TMP_DIR/bin/hadolint" "$TMP_DIR/bin/checkov"
}
validator_output() {
local dockerfile="$1"
PATH="$TMP_DIR/bin:$PATH" bash "$VALIDATOR" "$dockerfile" 2>&1 || true
}
validator_exit_code() {
local dockerfile="$1"
local exit_code=0
PATH="$TMP_DIR/bin:$PATH" bash "$VALIDATOR" "$dockerfile" >/dev/null 2>&1 || exit_code=$?
echo "$exit_code"
}
assert_output_contains() {
local label="$1"
local dockerfile="$2"
local pattern="$3"
local output
output="$(validator_output "$dockerfile")"
if echo "$output" | grep -qE "$pattern"; then
pass "$label"
else
fail "$label (missing pattern: $pattern)"
echo " --- validator output ---"
echo "$output" | sed 's/^/ /'
echo " --- end output ---"
fi
}
assert_output_not_contains() {
local label="$1"
local dockerfile="$2"
local pattern="$3"
local output
output="$(validator_output "$dockerfile")"
if echo "$output" | grep -qE "$pattern"; then
fail "$label (unexpected pattern: $pattern)"
echo " --- validator output ---"
echo "$output" | sed 's/^/ /'
echo " --- end output ---"
else
pass "$label"
fi
}
assert_exit_zero() {
local label="$1"
local dockerfile="$2"
local exit_code
exit_code="$(validator_exit_code "$dockerfile")"
if [[ "$exit_code" -eq 0 ]]; then
pass "$label"
else
fail "$label (expected 0, got $exit_code)"
fi
}
echo "Running dockerfile-validator regression tests..."
echo ""
make_stub_tools
COPY_BEFORE_YARN="$FIXTURES_DIR/copy-before-yarn.Dockerfile"
COPY_BEFORE_YARN_LOCK="$FIXTURES_DIR/copy-before-yarn-lock-read.Dockerfile"
FROM_PLATFORM_NONROOT="$FIXTURES_DIR/from-platform-nonroot.Dockerfile"
echo "[copy-before-yarn]"
assert_output_contains \
"flags COPY . before bare RUN yarn" \
"$COPY_BEFORE_YARN" \
"COPY \\. appears before dependency installation"
assert_exit_zero \
"warn-only result stays non-failing" \
"$COPY_BEFORE_YARN"
echo ""
echo "[copy-before-yarn-lock-read]"
assert_output_not_contains \
"does not misclassify yarn.lock as yarn install" \
"$COPY_BEFORE_YARN_LOCK" \
"COPY \\. appears before dependency installation"
assert_exit_zero \
"non-install RUN after COPY . remains non-failing" \
"$COPY_BEFORE_YARN_LOCK"
echo ""
echo "[from-platform-nonroot]"
assert_output_contains \
"parses --platform final image as non-root base" \
"$FROM_PLATFORM_NONROOT" \
"No USER directive, but final base image is non-root: gcr.io/distroless/static-debian11:nonroot"
assert_output_not_contains \
"does not emit root warning for non-root distroless base" \
"$FROM_PLATFORM_NONROOT" \
"container will run as root"
assert_output_contains \
"optimization stage reports parsed minimal final base" \
"$FROM_PLATFORM_NONROOT" \
"Using minimal base for final stage: gcr.io/distroless/static-debian11:nonroot"
assert_output_not_contains \
"base image analysis never treats --platform as image" \
"$FROM_PLATFORM_NONROOT" \
"Consider Alpine alternative for: --platform"
assert_exit_zero \
"platform flag fixture exits successfully" \
"$FROM_PLATFORM_NONROOT"
echo ""
echo "Summary: $PASS passed, $FAIL failed"
if [[ "$FAIL" -ne 0 ]]; then
exit 1
fi
echo "PASS: dockerfile-validator regression tests"
Related skills
How it compares
Choose dockerfile-validator over ad-hoc hadolint runs when you want Checkov plus severity-bucketed reports, reference-guided fixes, and documented CI fallbacks in one skill.
FAQ
What tools does dockerfile-validator run?
dockerfile-validator runs bash devops-skills-plugin/skills/dockerfile-validator/scripts/dockerfile-validate.sh, which invokes hadolint and Checkov for Dockerfile scanning, with fallbacks to docker run hadolint/hadolint or manual grep checks when tools are unavailable.
What severity levels does dockerfile-validator report?
dockerfile-validator maps findings to four buckets—Critical for secrets and root runtime, High for Checkov hardening failures, Medium for :latest tags and cache issues, and Low for style guidance—using a standard report template.
When does dockerfile-validator load reference files?
dockerfile-validator uses a no-issue fast path that skips references when the script passes cleanly; it reads security_checklist.md, optimization_guide.md, or docker_best_practices.md only when matching issue categories appear.