.claude/skills/docker-skills/docker-fastapi/SKILL.md
Comprehensive Docker containerization for Python/FastAPI applications from development to production deployments. Use when Claude needs to containerize Python/FastAPI applications with proper multi-stage builds, production-ready configurations, security best practices, and optimized Docker images for deployment to cloud platforms or container orchestration systems.
npx skillsauth add Asmayaseen/hackathon-2 docker-fastapiInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill provides comprehensive guidance for containerizing Python/FastAPI applications using Docker. It includes best practices for Dockerfile creation, multi-stage builds, production configurations, security hardening, and deployment strategies from development to production environments.
When optimizing containers, resolve conflicts in this order:
P1: Multi-Stage Always - Even if current deps don't require compilation, future deps might.
P2: Layer Order Matters - Copy dependency files first, install, then copy source:
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-cache
COPY src/ ./src/
P3: Single RUN for Related Operations - Combine related commands to reduce layers:
RUN groupadd -g 10001 appgroup && \
useradd -u 10001 -g appgroup -s /sbin/nologin appuser && \
chown -R appuser:appgroup /app
For production deployments with optimized security and size:
# syntax=docker/dockerfile:1
# ============================================
# BUILD STAGE - Has UV, compilers, dev tools
# ============================================
FROM ghcr.io/astral-sh/uv:python3.12-slim AS builder
WORKDIR /app
# P2: Dependency files first (changes less frequently)
COPY pyproject.toml uv.lock ./
# P3: Install deps into virtual env, not system Python
RUN uv sync --frozen --no-cache --no-dev
# P2: Source code last (changes most frequently)
COPY src/ ./src/
COPY main.py ./
# ============================================
# RUNTIME STAGE - Minimal, no build tools
# ============================================
FROM python:3.12-slim AS runtime
WORKDIR /app
# Runtime environment
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PATH="/app/.venv/bin:$PATH"
# P3: User setup in single layer with secure defaults
# UID 10001+ for Kubernetes pod security compliance
RUN groupadd -g 10001 appgroup && \
useradd -u 10001 -g appgroup -s /sbin/nologin appuser && \
mkdir -p /app && \
chown -R appuser:appgroup /app
# P1: Copy only runtime artifacts from builder (no UV, no build tools)
COPY --from=builder --chown=appuser:appgroup /app/.venv /app/.venv
COPY --from=builder --chown=appuser:appgroup /app/src /app/src
COPY --from=builder --chown=appuser:appgroup /app/main.py /app/
# Switch to non-root user (use numeric UID for portability)
USER 10001
EXPOSE 8000
# Health check using Python (no curl needed - smaller attack surface)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
# Production command
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# syntax=docker/dockerfile:1
# ============================================
# BUILD STAGE
# ============================================
FROM ghcr.io/astral-sh/uv:python3.12-slim AS builder
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-cache --no-dev
COPY src/ ./src/
COPY main.py ./
# ============================================
# RUNTIME STAGE
# ============================================
FROM python:3.12-slim AS runtime
WORKDIR /app
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PATH="/app/.venv/bin:$PATH" \
WORKERS=4 \
TIMEOUT=120 \
KEEP_ALIVE=5 \
MAX_REQUESTS=1000 \
MAX_REQUESTS_JITTER=100
RUN groupadd -g 10001 appgroup && \
useradd -u 10001 -g appgroup -s /sbin/nologin appuser && \
mkdir -p /app && \
chown -R appuser:appgroup /app
COPY --from=builder --chown=appuser:appgroup /app/.venv /app/.venv
COPY --from=builder --chown=appuser:appgroup /app/src /app/src
COPY --from=builder --chown=appuser:appgroup /app/main.py /app/
USER 10001
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
CMD ["sh", "-c", "gunicorn main:app --workers ${WORKERS} --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 --timeout ${TIMEOUT} --keep-alive ${KEEP_ALIVE} --max-requests ${MAX_REQUESTS} --max-requests-jitter ${MAX_REQUESTS_JITTER}"]
# syntax=docker/dockerfile:1
# ============================================
# BUILD STAGE
# ============================================
FROM ghcr.io/astral-sh/uv:python3.12-alpine AS builder
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-cache --no-dev
COPY src/ ./src/
COPY main.py ./
# ============================================
# RUNTIME STAGE
# ============================================
FROM python:3.12-alpine AS runtime
WORKDIR /app
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PATH="/app/.venv/bin:$PATH"
# Alpine uses addgroup/adduser with different flags
RUN addgroup -g 10001 appgroup && \
adduser -D -u 10001 -G appgroup -s /sbin/nologin appuser && \
mkdir -p /app && \
chown -R appuser:appgroup /app
COPY --from=builder --chown=appuser:appgroup /app/.venv /app/.venv
COPY --from=builder --chown=appuser:appgroup /app/src /app/src
COPY --from=builder --chown=appuser:appgroup /app/main.py /app/
USER 10001
EXPOSE 8000
# Alpine has wget built-in (no curl needed)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --spider -q http://localhost:8000/health || exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
For development with automatic reloading (security relaxed for convenience):
FROM python:3.12-slim
WORKDIR /app
# Install UV for fast dependency installation
RUN pip install --no-cache-dir uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-cache
COPY . .
ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 8000
# Development command with auto-reload
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
If not using pyproject.toml/uv.lock:
# syntax=docker/dockerfile:1
FROM ghcr.io/astral-sh/uv:python3.12-slim AS builder
WORKDIR /app
COPY requirements.txt ./
RUN uv venv && uv pip install --no-cache -r requirements.txt
COPY src/ ./src/
COPY main.py ./
FROM python:3.12-slim AS runtime
WORKDIR /app
ENV PYTHONUNBUFFERED=1 \
PATH="/app/.venv/bin:$PATH"
RUN groupadd -g 10001 appgroup && \
useradd -u 10001 -g appgroup -s /sbin/nologin appuser && \
mkdir -p /app && \
chown -R appuser:appgroup /app
COPY --from=builder --chown=appuser:appgroup /app/.venv /app/.venv
COPY --from=builder --chown=appuser:appgroup /app/src /app/src
COPY --from=builder --chown=appuser:appgroup /app/main.py /app/
USER 10001
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
For local development with database and other services:
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "8000:8000"
volumes:
- .:/app
- /app/.venv # Don't mount venv from host
environment:
- DATABASE_URL=postgresql://user:password@db:5432/mydb
depends_on:
db:
condition: service_healthy
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
version: '3.8'
services:
app:
image: myapp:latest
ports:
- "8000:8000"
environment:
- DATABASE_URL=${DATABASE_URL}
- API_KEY=${API_KEY}
- WORKERS=4
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
reservations:
memory: 256M
cpus: '0.25'
restart: unless-stopped
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 5s
Always run containers as a non-root user with high UID for K8s compliance:
# Debian/Ubuntu
RUN groupadd -g 10001 appgroup && \
useradd -u 10001 -g appgroup -s /sbin/nologin appuser && \
chown -R appuser:appgroup /app
USER 10001
# Alpine
RUN addgroup -g 10001 appgroup && \
adduser -D -u 10001 -G appgroup -s /sbin/nologin appuser && \
chown -R appuser:appgroup /app
USER 10001
Why UID 10001+?
runAsNonRoot and MustRunAsNonRoot policiesDon't install curl just for health checks - use native tools:
# Python (recommended - no extra dependencies)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
# Alpine (wget is built-in)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --spider -q http://localhost:8000/health || exit 1
Never hardcode secrets - pass them at runtime:
# ❌ NEVER do this
ENV DATABASE_URL=postgresql://user:password@host/db
ENV API_KEY=sk_live_abc123
# ✅ Do this - empty defaults, inject at runtime
ENV DATABASE_URL="" \
API_KEY=""
# Pass secrets at runtime
docker run -e DATABASE_URL=$DATABASE_URL -e API_KEY=$API_KEY myapp:latest
# Or use Docker secrets (Swarm) / Kubernetes secrets
/sbin/nologin as shell to prevent interactive accessOrder matters for cache efficiency:
# 1. Base image (rarely changes)
FROM python:3.12-slim
# 2. System deps (changes occasionally)
RUN apt-get update && apt-get install -y ...
# 3. Python deps (changes when pyproject.toml changes)
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-cache
# 4. Application code (changes frequently)
COPY src/ ./src/
| Approach | Typical Size |
|----------|-------------|
| python:3.12 (full) | ~900MB |
| python:3.12-slim | ~150MB |
| python:3.12-alpine | ~50MB |
| Multi-stage slim | ~100-200MB |
| Multi-stage alpine | ~50-100MB |
# Build with BuildKit (recommended)
DOCKER_BUILDKIT=1 docker build -t my-fastapi-app .
# Build with metadata
docker build \
--build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
--build-arg VERSION=1.0.0 \
-t my-fastapi-app:1.0.0 .
# Run the container
docker run -p 8000:8000 my-fastapi-app
# Run with environment variables
docker run -p 8000:8000 -e WORKERS=8 my-fastapi-app
# Test 1: Verify non-root user
docker run --rm my-fastapi-app whoami
# Expected: appuser (not root)
# Test 2: Verify UID
docker run --rm my-fastapi-app id
# Expected: uid=10001(appuser) gid=10001(appgroup)
# Test 3: Verify health check works
docker run -d --name test my-fastapi-app
sleep 10
docker inspect test | jq '.[0].State.Health.Status'
# Expected: "healthy"
docker rm -f test
# Test 4: Check image size
docker images my-fastapi-app
For K8s, HEALTHCHECK is ignored - use pod probes:
apiVersion: apps/v1
kind: Deployment
metadata:
name: fastapi-app
spec:
replicas: 3
selector:
matchLabels:
app: fastapi-app
template:
metadata:
labels:
app: fastapi-app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
containers:
- name: app
image: my-fastapi-app:latest
ports:
- containerPort: 8000
resources:
limits:
memory: "512Mi"
cpu: "500m"
requests:
memory: "256Mi"
cpu: "250m"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 2
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
docker logs <container>docker run --rm myapp ls -la /app--chown with COPY commandsdocker exec <container> python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"--start-period for slow-starting apps__pycache__ and .pyc filesThis skill includes the following resources:
development
Systematic methodology for debugging bugs, test failures, and unexpected behavior. Use when encountering any technical issue before proposing fixes. Covers root cause investigation, pattern analysis, hypothesis testing, and fix implementation. Use ESPECIALLY when under time pressure, "just one quick fix" seems obvious, or you've already tried multiple fixes. NOT for exploratory code reading.
development
Build beautiful, accessible UIs with shadcn/ui components in Next.js. Use when creating forms, dialogs, tables, sidebars, or any UI components. Covers installation, component patterns, react-hook-form + Zod validation, and dark mode setup. NOT when building non-React applications or using different component libraries.
tools
Implement real-time streaming UI patterns for AI chat applications. Use when adding response lifecycle handlers, progress indicators, client effects, or thread state synchronization. Covers onResponseStart/End, onEffect, ProgressUpdateEvent, and client tools. NOT when building basic chat without real-time feedback.
tools
Builds AI agents using OpenAI Agents SDK with async/await patterns and multi-agent orchestration. Use when creating tutoring agents, building agent handoffs, implementing tool-calling agents, or orchestrating multiple specialists. Covers Agent class, Runner patterns, function tools, guardrails, and streaming responses. NOT when using raw OpenAI API without SDK or other agent frameworks like LangChain.