Dockerfile Validator
Validates, lints, and secures Dockerfiles by running syntax checking, detecting security vulnerabilities, validating layer ordering, checking for hardcoded secrets, verifying base image tags, and analyzing build optimization. Use when validating Dockerfile syntax, checking security best practices, optimizing image builds, auditing container security, or debugging Dockerfile errors. Applies to all Dockerfile variants (Dockerfile, Dockerfile.prod, Dockerfile.dev, etc.).
npx skills add github:pantheon-org/tekhne --skill infrastructure--dockerfile--validatorPhilosophy
- Lint first, scan second: syntax and style problems are cheapest to fix before security findings are triaged.
- A Dockerfile that builds is not a Dockerfile that is correct; reproducibility, layer caching and image size are part of correctness.
- Security findings outrank optimisation findings, so address secrets, root users and unpinned bases before shaving layers.
- Every suppression needs a written justification, because an unexplained skip hides a real finding from the next reader.
- Static checks and image scanning are complementary, so neither replaces the other.
When to Use
- Validating Dockerfile syntax or debugging a Dockerfile error such as a failed
COPYor a misplacedARG. - Auditing a Dockerfile (including
Dockerfile.prodandDockerfile.devvariants) for security issues such as hardcoded secrets or a missingUSER. - Reviewing layer ordering, cache cleanup and multi-stage opportunities to reduce build time or image size.
- Adding hadolint or Checkov checks to a CI/CD pipeline.
- Checking for a
.dockerignorefile and sensible base image tags before building.
When Not to Use
- Generating a new Dockerfile from scratch: use the
dockerfile-generatorskill instead. - Scanning a built image for CVEs: use an image scanner such as Trivy, which this skill only recommends as a follow-up.
- Validating Kubernetes manifests or Helm charts that reference the image: use
k8s-yaml-validatororhelm-validator. - Validating Compose files, which this skill does not cover.
Overview
This skill validates Dockerfiles using a single self-contained script (dockerfile-validate.sh) that handles everything: tool installation, validation, and cleanup.
Key Features:
- ✅ Single script execution - no dependencies on other scripts
- ✅ Auto-installs hadolint and Checkov in Python venvs if not found
- ✅ Runs all 4 validation stages (syntax, security, best practices, optimization)
- ✅ Auto-cleanup on exit using bash trap (success or failure)
- ✅ Zero configuration required
Quick Start
Single command to validate any Dockerfile:
bash scripts/dockerfile-validate.sh Dockerfile
The script automatically checks for hadolint and Checkov, installs them temporarily in Python venvs if needed, runs all 4 validation stages, then cleans up on exit.
Validation Workflow
The dockerfile-validate.sh script runs a comprehensive 4-stage validation:
- Auto-Install (if needed) — Check for hadolint and Checkov; install in Python venvs if absent; set flag to trigger cleanup on exit via bash trap
- [1/4] Syntax Validation (hadolint) — Dockerfile syntax checking, instruction validation, shell script validation via ShellCheck
- [2/4] Security Scan (Checkov) — Security policy validation, hardcoded secret detection, port exposure checks, USER directive validation
- [3/4] Best Practices Validation (custom) — Base image tag validation, non-root USER enforcement, HEALTHCHECK presence, layer efficiency, package cache cleanup, COPY ordering
- [4/4] Optimization Analysis (custom) — Base image size analysis, multi-stage build opportunities, layer count, .dockerignore check, build structure recommendations
- Auto-Cleanup (always runs) — Remove temp venvs on exit (success, failure, Ctrl+C, or error)
Core Capabilities
1. Syntax Validation with hadolint
Workflow:
# Run hadolint on Dockerfile
hadolint Dockerfile
# Run with JSON output for parsing
hadolint --format json Dockerfile
# Run with specific rules ignored
hadolint --ignore DL3006 --ignore DL3008 Dockerfile
# Using Docker if not installed
docker run --rm -i hadolint/hadolint < Dockerfile
For the full list of DL-prefixed (hadolint) and SC-prefixed (ShellCheck) rules, see references/docker_best_practices.md.
Rule Severity Levels: error → warning → info → style
Best Practices:
- Run hadolint before every docker build
- Integrate into CI/CD pipelines
- Configure
.hadolint.yamlfor project-specific rules - Address errors before warnings
2. Security Scanning with Checkov
Installation (permanent):
pip3 install checkov # direct
brew install checkov # macOS
Workflow:
# Scan a Dockerfile
checkov -f Dockerfile --framework dockerfile
# Scan a directory (finds all Dockerfiles)
checkov -d . --framework dockerfile
# Compact output (failures only)
checkov -f Dockerfile --framework dockerfile --compact
# Skip specific checks
checkov -f Dockerfile --framework dockerfile --skip-check CKV_DOCKER_2
For the full list of CKV_DOCKER_* security checks, see references/security_checklist.md.
Suppressing False Positives:
# checkov:skip=CKV_DOCKER_2:Health check not applicable for this init container
FROM alpine:3.21
Best Practices:
- Run Checkov after hadolint (syntax first, then security)
- Address high-severity findings first
- Document all suppressions with clear justification
- Integrate into CI/CD pipelines
3. Best Practices Validation
Custom Validation Checks:
# Check for :latest tag usage
grep -E "^FROM.*:latest" Dockerfile
# Count FROM statements (single FROM = multi-stage opportunity)
grep -c "^FROM" Dockerfile
# Ensure USER is set before CMD/ENTRYPOINT
grep "^USER" Dockerfile
# Verify HEALTHCHECK is defined for services
grep "^HEALTHCHECK" Dockerfile
# Count RUN commands (>5 suggests combination opportunity)
grep -c "^RUN" Dockerfile
# Verify cache cleanup in same RUN layer
grep "rm -rf /var/lib/apt/lists" Dockerfile
grep "--no-cache" Dockerfile # for apk
Non-Obvious Checks (project-specific):
| Category | Non-Obvious Convention |
|---|---|
| Base Images | Prefer digest pinning (FROM alpine@sha256:…) over version tags for reproducible builds |
| Layer Ordering | COPY dependency manifests before source code so edits don’t bust the install cache |
| Cache Cleanup | Must happen in the same RUN layer as the install; a separate RUN rm -rf … creates a new layer that doesn’t reduce size |
| Multi-Stage | Named stages (AS build) allow selective --target build for CI debugging without exposing secrets in final image |
| Secrets | BuildKit --mount=type=secret avoids secrets ever appearing in any layer, including intermediate ones |
4. Optimization Analysis
Optimization Categories:
Image Size Reduction:
# Bad: Full distro
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y curl
# Good: Minimal distro
FROM alpine:3.21
RUN apk add --no-cache curl
# Better: Multi-stage with distroless
FROM golang:1.21 AS build
WORKDIR /app
COPY . .
RUN go build -o myapp
FROM gcr.io/distroless/base-debian11
COPY --from=build /app/myapp /
ENTRYPOINT ["/myapp"]
Layer Optimization:
# Bad: Separate RUN commands (creates many layers)
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
# Good: Combined RUN (single layer)
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
Build Cache Efficiency:
# Bad: Copy all, then install dependencies
COPY . /app
RUN pip install -r requirements.txt
# Good: Copy dependency file first
COPY requirements.txt /app/
RUN pip install -r requirements.txt
COPY . /app
5. .dockerignore Validation
# Check if .dockerignore exists
if [ ! -f .dockerignore ]; then
echo "WARNING: .dockerignore file not found"
fi
Common patterns to include:
.git
.gitignore
.env
*.log
node_modules
Dockerfile*
docker-compose*.yml
Tool Prerequisites
The validation script automatically installs tools if not found. No manual installation required.
For permanent installations:
# hadolint
brew install hadolint # macOS
wget -O ~/.local/bin/hadolint https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64 && chmod +x ~/.local/bin/hadolint # Linux
# Checkov
pip3 install checkov
Minimum Versions: hadolint >= 2.12.0 | Checkov: latest | Python >= 3.8
Testing Auto-Install:
# Force temporary installation for testing
FORCE_TEMP_INSTALL=true bash scripts/dockerfile-validate.sh Dockerfile
Handling Missing Tools
When tools are not installed, the script auto-installs them temporarily. If auto-install fails:
- Complete available validations — continue with custom best practices checks and note skipped stages
- Provide installation guidance:
- hadolint:
brew install hadolint(macOS) or wget from GitHub releases (Linux) - Checkov:
pip3 install checkov
- hadolint:
- Offer to rerun after installation
Tool Priority:
- Required (always run): Custom best practices validation, file existence checks
- Recommended: hadolint (syntax/linting), Checkov (security scanning)
- Optional: docker (test builds), trivy (vulnerability scanning)
Error Troubleshooting
| Error | Solution |
|---|---|
FROM instruction must be first non-comment |
Move ARG VERSION=18 before FROM node:${VERSION} |
| Unknown instruction (typo) | Check spelling: common typos are RUNS, COPIES, FRUM |
| Chained RUN command fails | Use apt-get install -y package || exit 1 or set -e |
COPY failed: file not found |
Check path is relative to build context; verify not excluded by .dockerignore |
| Hardcoded secrets detected | Use BuildKit secrets: docker build --secret id=api_key,src=api_key.txt instead of ENV API_KEY=secret123 |
| Slow builds | Optimize layer caching (COPY package files first), use .dockerignore, enable BuildKit (export DOCKER_BUILDKIT=1), use multi-stage builds |
Mandatory Workflow
Follow these steps in order for every validation request:
1. Pre-Validation
- Read the Dockerfile first — Use the Read tool to examine the Dockerfile before running validation.
2. Validation
- Run the validation script — Execute
bash scripts/dockerfile-validate.sh <Dockerfile>to run all 4 stages.
3. Post-Validation
-
Summarize findings by severity:
- Critical (security vulnerabilities, hardcoded secrets)
- High (missing USER, HEALTHCHECK, :latest tags)
- Medium (layer optimization, version pinning)
- Low (style, informational)
-
Propose specific fixes — Use the Read tool to load appropriate reference files before proposing fixes:
Issue Type Reference File Security issues (secrets, USER, ports) references/security_checklist.mdSize/performance optimization references/optimization_guide.mdGeneral best practices references/docker_best_practices.md -
Offer to apply fixes — Ask the user if they want fixes applied, then apply if approved.
Example interaction:
User: "Validate my Dockerfile"
1. Read the Dockerfile using Read tool
2. Run: bash scripts/dockerfile-validate.sh Dockerfile
3. Review output from all 4 stages
4. Summarize findings by severity (critical → low)
5. Use Read tool to load relevant reference files:
- Read references/security_checklist.md (if security issues found)
- Read references/optimization_guide.md (if optimization issues found)
- Read references/docker_best_practices.md (if best practice issues found)
6. Propose specific fixes with code examples from reference content
7. Ask: "Would you like me to apply these fixes?"
8. Apply fixes if user approves
For multi-Dockerfile projects: find all Dockerfile* files, validate each sequentially, aggregate results, and provide a unified report with project-wide improvement suggestions.
Integration with Other Skills
- dockerfile-generator — Generate optimized Dockerfiles
- k8s-yaml-validator — Validate Kubernetes deployments that reference Docker images
- helm-validator — Validate Helm charts with container configurations
Notes
- Always validate before building images
- Address security issues before optimizations
- Test builds after applying fixes
- Version pin base images for reproducibility
- Use multi-stage builds for compiled languages
- Keep production images minimal (distroless, Alpine)
- Never commit Dockerfiles with hardcoded secrets
- Document inline suppressions with clear justification
- Regularly update base images for security patches
- Integrate validation into CI/CD pipelines
Anti-Patterns
NEVER skip Hadolint because the image builds successfully
- WHY: A Dockerfile that builds can still violate security, layer caching, and size best practices; hadolint catches these before they reach production.
- BAD: Skip linting if
docker buildexits 0. - GOOD: Run
hadolint Dockerfilein CI as a required check; treat W rules as warnings and DL rules as errors.
NEVER ignore DL3008 (unpinned apt packages)
- WHY: Unpinned packages produce non-reproducible images; the same Dockerfile built a month apart may install different package versions with different security postures.
- BAD:
RUN apt-get install -y curl - GOOD:
RUN apt-get install -y curl=7.88.1-*or use a digest-pinned base image with pre-installed versions.
NEVER validate a Dockerfile without also scanning the built image
- WHY: Static analysis misses runtime vulnerabilities introduced by base images and installed packages; image scanning (Trivy, Grype) is complementary.
- BAD: Pass hadolint and ship the image without scanning it.
- GOOD: Run
trivy image myapp:latestafter build in the CI pipeline.
NEVER treat --no-cache as a security measure
- WHY:
--no-cacheprevents layer caching during the current build but does not remove vulnerabilities in the base image or installed packages; image scanning is still required. - BAD: Rely on
--no-cacheto ensure a “fresh” secure image. - GOOD: Use
--no-cachefor reproducibility in CI; pair with image scanning for security assurance.
NEVER use the :latest tag for a base image
- WHY:
:latestmoves silently, so two builds of the same Dockerfile can produce different images and the validator’s tag check fails. - BAD:
FROM node:latest - GOOD:
FROM node:20-alpine, or a digest-pinnedFROM alpine@sha256:...for fully reproducible builds.
NEVER put secrets in ENV or ARG
- WHY: Values set with
ENVpersist in image metadata and layers, so anyone who can pull the image can read them. - BAD:
ENV API_KEY=secret123 - GOOD:
docker build --secret id=api_key,src=api_key.txtwith a BuildKit--mount=type=secretin theRUNstep that needs it.
NEVER run the final container as root
- WHY: A compromised process running as root has far more power inside and outside the container; the best practices stage enforces a non-root
USER. - BAD: A Dockerfile with no
USERinstruction beforeCMDorENTRYPOINT. - GOOD: Create an unprivileged user and switch to it with
USERbeforeCMD.
NEVER clean package caches in a separate RUN layer
- WHY: A later cleanup
RUNcreates a new layer on top of the old one, so the cached files stay in the earlier layer and the image does not shrink. - BAD:
RUN apt-get update && apt-get install -y curlfollowed by a separateRUNthat deletes/var/lib/apt/lists. - GOOD: One
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*layer.
NEVER copy the whole source tree before installing dependencies
- WHY: Any source edit invalidates the cache for the install step, so dependencies are reinstalled on every build.
- BAD:
COPY . /appfollowed byRUN pip install -r requirements.txt - GOOD:
COPY requirements.txt /app/, thenRUN pip install -r requirements.txt, thenCOPY . /app.
NEVER build without a .dockerignore file
- WHY: Without one the whole build context is sent to the daemon, including
.git,.envandnode_modules, which slows builds and risks leaking files into layers. - BAD: A project root with a Dockerfile and no
.dockerignore. - GOOD: Start from
assets/.dockerignore.exampleand exclude.git,.env,*.logandnode_modules.
References
Internal:
scripts/
dockerfile-validate.sh
- Single self-contained validation script
- Auto-installs hadolint and Checkov if needed
- Runs all 4 validation stages (syntax, security, best practices, optimization)
- Auto-cleanup on exit
- Usage:
bash scripts/dockerfile-validate.sh [Dockerfile]
assets/
| File | Purpose |
|---|---|
good-example.Dockerfile |
Best practices and optimal structure |
bad-example.Dockerfile |
Common mistakes and anti-patterns |
security-issues.Dockerfile |
Intentional security vulnerabilities for testing |
python-optimized.Dockerfile |
Python-specific optimizations and multi-stage build |
golang-distroless.Dockerfile |
Minimal Go application using distroless base image |
.dockerignore.example |
Example .dockerignore for build context optimization |
references/
| File | Contents |
|---|---|
docker_best_practices.md |
Official Docker best practices; full hadolint DL/SC rule listings |
optimization_guide.md |
Layer optimization and image size reduction techniques |
security_checklist.md |
Container security best practices; full CKV_DOCKER_* check listings |
External:
Official Docker Documentation:
Security Guidelines:
Best Practices Resources:
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
No eval scenarios available.
Docker 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 | bash
Clean 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 package2
Use –no-install-recommends for apt
RUN apt-get install -y --no-install-recommends package
Pin 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.3
Sort 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 > /number
COPY 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 /app
Don’t use RUN cd
# Bad
RUN cd /app && npm install
# Good
WORKDIR /app
RUN npm install
USER
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 appuser
Use high UID (>10000) for better security
RUN useradd -u 10001 -m appuser
USER appuser
CMD 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 443
HEALTHCHECK
Add health checks for services
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
LABEL
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:tag
2. 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 appuser
5. Use Read-Only Filesystem
docker run --read-only myimage
6. Limit Capabilities
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myimage
Common 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/yum
Pip (Python):
RUN pip install --no-cache-dir package
NPM (Node.js):
RUN npm ci --only=production
# Or with cache mount:
RUN --mount=type=cache,target=/root/.npm \
npm ci --only=production
5. Use .dockerignore
Problem: Entire project copied into image
.dockerignore contents:
.git/
node_modules/
*.log
.env
tests/
docs/
README.md
Impact:
- 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 build
Why 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=1
Use 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 app
Benefits:
- 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 1
Benefits:
- 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=4
Language-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 ci
docker 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.git
docker 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.2GB
After Optimization
docker images myapp-optimized
# REPOSITORY TAG SIZE
# myapp-optimized latest 50MB
Build 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
-
Use specific base image tags
FROM alpine:3.21 # Not alpine:latest -
Run as non-root
USER appuser -
Clean package cache
RUN apk add --no-cache package -
Don’t expose unnecessary ports
# Only expose what's needed EXPOSE 8080 -
Add health checks
HEALTHCHECK CMD curl -f http://localhost/ || exit 1
Security 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 |