AI Tools Compared

Dockerfile quality separates 2GB images from 150MB ones, root-running containers from hardened ones, and builds that always pull the internet from builds that use the layer cache. AI tools generate working Dockerfiles quickly, but the difference between “works” and “production-ready” is large.

Prerequisites

Before you begin, make sure you have the following ready:

Step 1: The Test: Python FastAPI Application

All tools were given the same prompt:

“Write a production Dockerfile for a Python FastAPI application. Focus on security, image size, and build caching.”

Step 2: Claude’s Multi-Stage Build

# Build stage — install deps and compile
FROM python:3.12-slim AS builder

WORKDIR /build

# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

# Copy requirements first — better layer caching
# Changes to source code won't invalidate this layer
COPY requirements.txt .

# Install to a separate directory for easy copying
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# Runtime stage — minimal image with no build tools
FROM python:3.12-slim AS runtime

WORKDIR /app

# Install only runtime system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq5 \
    && rm -rf /var/lib/apt/lists/* \
    && apt-get clean

# Create non-root user
RUN groupadd --gid 1001 appuser \
    && useradd --uid 1001 --gid appuser --no-create-home --shell /sbin/nologin appuser

# Copy installed packages from builder
COPY --from=builder /install /usr/local

# Copy application code
COPY --chown=appuser:appuser . .

# Switch to non-root user
USER appuser

# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

# Expose port (documentation only — doesn't publish)
EXPOSE 8000

# Use exec form to receive signals properly
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]

Key features:

Step 3: Copilot’s Output

FROM python:3.12

WORKDIR /app

COPY . .

RUN pip install -r requirements.txt

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

This is a valid Dockerfile but:

Step 4: Node.js Application

# Claude's Node.js multi-stage Dockerfile

# Stage 1: Install dependencies
FROM node:20-alpine AS deps
WORKDIR /app

# Copy package files first
COPY package.json package-lock.json ./

# Install production dependencies only
RUN npm ci --omit=dev --prefer-offline

# Stage 2: Build (for TypeScript or Next.js)
FROM node:20-alpine AS builder
WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --prefer-offline

COPY . .
RUN npm run build

# Stage 3: Production runtime
FROM node:20-alpine AS runtime
WORKDIR /app

# Security hardening
RUN apk add --no-cache dumb-init \
    && addgroup --system --gid 1001 nodejs \
    && adduser --system --uid 1001 nextjs

# Copy only what's needed
COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --chown=nextjs:nodejs package.json ./

USER nextjs

# dumb-init: proper signal handling for Node.js
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]

HEALTHCHECK --interval=30s --timeout=3s \
    CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

Claude’s Node.js version uses dumb-init for proper PID 1 signal handling — a critical detail for graceful shutdown that almost no other AI tool includes.

Step 5: Go Application: Scratch Image

# Claude's Go Dockerfile — minimal final image

# Build stage
FROM golang:1.22-alpine AS builder

WORKDIR /build

# Install CA certificates for HTTPS calls from the binary
RUN apk add --no-cache ca-certificates git

# Download dependencies (cached separately from source)
COPY go.mod go.sum ./
RUN go mod download

# Build with security flags
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
    -ldflags="-w -s -extldflags '-static'" \
    -trimpath \
    -o server \
    ./cmd/server

# Runtime stage — empty scratch image
FROM scratch

# Copy CA certs for HTTPS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# Copy timezone data
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

# Copy binary
COPY --from=builder /build/server /server

# Non-root user in scratch (no /etc/passwd, use numeric UID)
USER 65534:65534

EXPOSE 8080

ENTRYPOINT ["/server"]

The scratch final image is under 10MB and has zero attack surface — no shell, no package manager, no OS utilities. Claude knows to copy CA certs (needed for HTTPS calls) and use a numeric UID since there’s no /etc/passwd in scratch.

Step 6: Security Scanning Integration

# .github/workflows/docker-security.yml
- name: Run Trivy security scanner
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: 'myapp:${{ github.sha }}'
    format: 'sarif'
    output: 'trivy-results.sarif'
    severity: 'CRITICAL,HIGH'
    exit-code: '1'  # Fail build on critical vulns
# Use Claude to interpret Trivy output
from anthropic import Anthropic
import json

client = Anthropic()

def interpret_trivy_report(trivy_json: dict) -> str:
    critical_vulns = [
        v for result in trivy_json.get("Results", [])
        for v in result.get("Vulnerabilities", [])
        if v.get("Severity") == "CRITICAL"
    ]

    if not critical_vulns:
        return "No critical vulnerabilities found."

    response = client.messages.create(
        model="claude-opus-4-6",
        max_tokens=1000,
        messages=[{
            "role": "user",
            "content": f"""Summarize these Docker image vulnerabilities and provide remediation steps.

Vulnerabilities:
{json.dumps(critical_vulns[:5], indent=2)}

For each: affected package, severity, CVE, and specific fix (usually a package version update or base image change)."""
        }]
    )
    return response.content[0].text

Tool Comparison

Feature Claude Code Copilot Cursor
Multi-stage builds Always Rare Sometimes
Non-root user Yes No Sometimes
Layer cache optimization Optimal Basic Partial
Signal handling (dumb-init) Yes No No
Health checks Yes No Rarely
Scratch images for Go Yes with CA certs Basic scratch No
Security hardening flags -w -s -extldflags Basic Partial

Troubleshooting

Configuration changes not taking effect

Restart the relevant service or application after making changes. Some settings require a full system reboot. Verify the configuration file path is correct and the syntax is valid.

Permission denied errors

Run the command with sudo for system-level operations, or check that your user account has the necessary permissions. On macOS, you may need to grant terminal access in System Settings > Privacy & Security.

Connection or network-related failures

Check your internet connection and firewall settings. If using a VPN, try disconnecting temporarily to isolate the issue. Verify that the target server or service is accessible from your network.