Build an AI Dockerfile Security Scanner with Claude API
Tutorial to build a tool that analyzes Dockerfiles for security vulnerabilities, bad practices, and layer optimization issues — using Claude API to generate specific, actionable fixes with a corrected Dockerfile.
A Dockerfile that installs software as root, copies in .env files, uses latest tags, and has 20 unnecessary layers is depressingly common. Most teams write Dockerfiles once and never revisit them.
An AI Dockerfile scanner that catches these issues before CI builds them into production images is a practical tool that can run in pre-commit hooks or as a pull request check.
What We're Building
A Python tool + GitHub Actions workflow that:
- Reads a Dockerfile
- Sends it to Claude API for security and best practice analysis
- Returns specific issues with severity ratings
- Outputs a corrected Dockerfile with all critical issues fixed
- Can block CI if critical issues are found
Setup
pip install anthropic python-dotenvStep 1: Dockerfile Parser
import anthropic
import json
import os
import sys
import re
from pathlib import Path
from typing import Optional
def parse_dockerfile(content: str) -> dict:
"""Extract key information from Dockerfile for context-aware analysis."""
lines = [l.strip() for l in content.split('\n') if l.strip() and not l.strip().startswith('#')]
context = {
"base_images": [],
"exposed_ports": [],
"run_commands": [],
"copy_paths": [],
"user_instructions": [],
"environment_vars": [],
"has_healthcheck": False,
"is_multi_stage": False,
"total_layers": 0
}
for line in lines:
upper = line.upper()
if upper.startswith("FROM"):
context["base_images"].append(line)
context["total_layers"] += 1
if len(context["base_images"]) > 1:
context["is_multi_stage"] = True
elif upper.startswith("EXPOSE"):
ports = re.findall(r'\d+', line)
context["exposed_ports"].extend(ports)
elif upper.startswith("RUN"):
context["run_commands"].append(line[:200])
context["total_layers"] += 1
elif upper.startswith("COPY") or upper.startswith("ADD"):
context["copy_paths"].append(line)
context["total_layers"] += 1
elif upper.startswith("USER"):
context["user_instructions"].append(line)
elif upper.startswith("ENV"):
var_name = line.split('=')[0].replace('ENV', '').strip()
context["environment_vars"].append(var_name)
elif upper.startswith("HEALTHCHECK"):
context["has_healthcheck"] = True
return context
def detect_obvious_issues(content: str, context: dict) -> list[str]:
"""Fast pre-scan for obvious patterns before sending to Claude."""
issues = []
if not context["user_instructions"]:
issues.append("CRITICAL: No USER instruction — container runs as root by default")
if any("latest" in img.lower() for img in context["base_images"]):
issues.append("HIGH: Using :latest tag — non-reproducible builds")
# Check for copied sensitive files
sensitive_patterns = [".env", "*.pem", "*.key", "id_rsa", "credentials", ".aws"]
for path in context["copy_paths"]:
for pattern in sensitive_patterns:
if pattern.replace("*", "") in path.lower():
issues.append(f"CRITICAL: Potentially copying sensitive file: {path}")
if not context["has_healthcheck"]:
issues.append("MEDIUM: No HEALTHCHECK instruction")
if context["total_layers"] > 20:
issues.append(f"MEDIUM: High layer count ({context['total_layers']}) — consider layer optimization")
return issuesStep 2: Claude API Analysis
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
def analyze_dockerfile_with_claude(
dockerfile_content: str,
context: dict,
pre_scan_issues: list[str]
) -> dict:
"""
Send Dockerfile to Claude for comprehensive security and optimization analysis.
"""
dockerfile_block = "```dockerfile\n" + dockerfile_content + "\n```"
pre_scan_text = "\n".join(pre_scan_issues) if pre_scan_issues else "None"
prompt = (
"You are a Docker security expert reviewing a Dockerfile for production deployment.\n\n"
"## Dockerfile to Review\n\n"
+ dockerfile_block + "\n\n"
"## Pre-scan Findings\n" + pre_scan_text + "\n\n"
"## Context\n"
f"- Base images: {context['base_images']}\n"
f"- Is multi-stage: {context['is_multi_stage']}\n"
f"- Exposed ports: {context['exposed_ports']}\n"
f"- Has USER instruction: {bool(context['user_instructions'])}\n"
f"- Has HEALTHCHECK: {context['has_healthcheck']}\n"
f"- Total layers: {context['total_layers']}\n"
f"- Environment vars: {context['environment_vars']}\n\n"
"---\n\n"
'Provide a comprehensive analysis in this exact JSON structure:\n\n'
'{"overall_score": 0-100, "production_ready": true/false, '
'"security_issues": [{"severity": "critical|high|medium|low", '
'"line_hint": "...", "issue": "...", "attack_vector": "...", "fix": "..."}], '
'"optimization_issues": [{"severity": "high|medium|low", "issue": "...", '
'"impact": "...", "fix": "..."}], '
'"positive_practices": ["..."], '
'"corrected_dockerfile": "Complete corrected Dockerfile", '
'"summary": "2-3 sentence verdict"}\n\n'
"Security: check for root user, sensitive files in COPY, hardcoded secrets, :latest tags, no HEALTHCHECK.\n"
"Optimization: unchained RUN commands, COPY . . before deps, oversized base image, dev deps in prod.\n"
"Return ONLY valid JSON."
)
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=4000,
messages=[{"role": "user", "content": prompt}]
)
result_text = response.content[0].text.strip()
if result_text.startswith("```"):
result_text = result_text.split("```")[1]
if result_text.startswith("json"):
result_text = result_text[4:]
if result_text.endswith("```"):
result_text = result_text[:-3]
return json.loads(result_text)Step 3: CLI with Exit Codes for CI
def format_report(analysis: dict, filepath: str) -> str:
"""Format analysis as terminal-friendly report."""
score = analysis.get("overall_score", 0)
production_ready = analysis.get("production_ready", False)
status_icon = "✅" if production_ready else "❌"
score_color = "🟢" if score >= 80 else "🟡" if score >= 60 else "🔴"
lines = [
f"\n{'='*60}",
f" DOCKERFILE SECURITY SCAN: {filepath}",
f"{'='*60}",
f"{status_icon} Score: {score}/100 — {'Production Ready' if production_ready else 'NOT Production Ready'}",
f"",
]
security_issues = analysis.get("security_issues", [])
if security_issues:
lines.append("🔒 SECURITY ISSUES")
for issue in security_issues:
sev = issue.get("severity", "").upper()
icon = "🔴" if sev == "CRITICAL" else "🟠" if sev == "HIGH" else "🟡"
lines.append(f" {icon} [{sev}] {issue.get('line_hint', '')}:")
lines.append(f" {issue.get('issue', '')}")
lines.append(f" Attack vector: {issue.get('attack_vector', '')}")
lines.append(f" Fix: {issue.get('fix', '')}")
lines.append("")
opt_issues = analysis.get("optimization_issues", [])
if opt_issues:
lines.append("⚡ OPTIMIZATION ISSUES")
for issue in opt_issues:
sev = issue.get("severity", "").upper()
lines.append(f" [{sev}] {issue.get('issue', '')}")
lines.append(f" Fix: {issue.get('fix', '')}")
lines.append("")
if analysis.get("positive_practices"):
lines.append("✅ GOOD PRACTICES")
for p in analysis["positive_practices"]:
lines.append(f" • {p}")
lines.append("")
lines.append(f"📋 VERDICT")
lines.append(f" {analysis.get('summary', '')}")
return "\n".join(lines)
def scan_dockerfile(
dockerfile_path: str,
save_fixed: bool = False,
fail_on_critical: bool = True
) -> int:
"""
Scan a Dockerfile. Returns exit code:
0 = no critical issues
1 = critical or high security issues found
2 = file not found or parse error
"""
try:
content = Path(dockerfile_path).read_text()
except FileNotFoundError:
print(f"Error: {dockerfile_path} not found")
return 2
context = parse_dockerfile(content)
pre_scan = detect_obvious_issues(content, context)
print(f"Analyzing {dockerfile_path}...")
analysis = analyze_dockerfile_with_claude(content, context, pre_scan)
print(format_report(analysis, dockerfile_path))
if save_fixed and analysis.get("corrected_dockerfile"):
fixed_path = dockerfile_path.replace("Dockerfile", "Dockerfile.fixed")
Path(fixed_path).write_text(analysis["corrected_dockerfile"])
print(f"\n💾 Fixed Dockerfile saved to: {fixed_path}")
# Determine exit code for CI
has_critical = any(
i.get("severity") in ["critical", "high"]
for i in analysis.get("security_issues", [])
)
if fail_on_critical and has_critical:
print("\n❌ CRITICAL/HIGH security issues found. Failing CI.")
return 1
return 0
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="AI Dockerfile security scanner")
parser.add_argument("dockerfile", nargs="?", default="Dockerfile")
parser.add_argument("--save-fixed", action="store_true")
parser.add_argument("--no-fail", action="store_true", help="Don't exit 1 on critical issues")
args = parser.parse_args()
from dotenv import load_dotenv
load_dotenv()
sys.exit(scan_dockerfile(args.dockerfile, args.save_fixed, not args.no_fail))GitHub Actions Integration
# .github/workflows/dockerfile-scan.yml
name: Dockerfile Security Scan
on:
pull_request:
paths:
- '**/Dockerfile'
- '**/Dockerfile.*'
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: pip install anthropic
- name: Scan all Dockerfiles
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
EXIT_CODE=0
for dockerfile in $(find . -name "Dockerfile*" -not -path "./.git/*"); do
python dockerfile_scanner.py "$dockerfile" || EXIT_CODE=1
done
exit $EXIT_CODEExample Output
============================================================
DOCKERFILE SECURITY SCAN: Dockerfile
============================================================
❌ Score: 32/100 — NOT Production Ready
🔒 SECURITY ISSUES
🔴 [CRITICAL] No USER instruction:
Container runs as root, giving attackers full system access if container is compromised
Attack vector: If any RUN command downloads malicious content, attacker has root
Fix: Add "USER appuser" and create the user: RUN addgroup -S app && adduser -S appuser -G app
🔴 [CRITICAL] COPY . . before npm install:
.env file in project root will be copied into the image
Attack vector: Anyone with image access can extract all environment secrets
Fix: Add .dockerignore with .env, *.pem, .git; use Secrets at runtime not build time
🎯 VERDICT
This Dockerfile has critical security vulnerabilities — running as root and potentially
leaking secrets through the build context. Not suitable for production. Fix the USER
instruction and .dockerignore before deployment.
More AI security tools? Read our AI Kubernetes YAML explainer and validator and Dockerfile security best practices.
Today I Fixed
Short real fixes from production — posted daily
Stay ahead of the curve
Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.
Related Articles
Build an AI-Powered Dockerfile Security Scanner with Claude
Build a tool that scans Dockerfiles for security issues using Claude API — finds hardcoded secrets, root users, unscanned base images, and missing security best practices.
Build an AI Kubernetes Deployment Readiness Checker with Claude API
Build a Python CLI tool using Claude API that analyzes Kubernetes YAML manifests before deployment — catches missing resource limits, root containers, and security issues with a go/no-go score.
LLM Guardrails in Production — Input/Output Validation with NeMo Guardrails and Claude
Deploying LLMs without guardrails causes prompt injection and data leakage. Here's how to build a layered safety system using regex, Claude-as-judge, and NeMo Guardrails.