Claude Skill

container-orchestration

Docker, Kubernetes, and AWS ECS/Fargate patterns. Triggers on: Dockerfile, docker-compose, kubernetes, k8s, helm, pod, deployment, service, ingress, container, image, ecs, fargate, task definition, ecs service, awsvpc, FARGATE_SPOT, ALB, ecs vs kubernetes.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_container-orchestration-3dfaf0b.zip · 19 KB
Part of 0xdarkmatter/claude-mods — 94 skills

Install

skills CLI npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/container-orchestration
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git git clone https://github.com/0xDarkMatter/claude-mods.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Container Orchestration

Facts verified as of 2026-07.

Docker and Kubernetes patterns for containerized applications.

Dockerfile Best Practices

# Use specific version, not :latest
FROM python:3.11-slim AS builder

# Set working directory
WORKDIR /app

# Copy dependency files first (better caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY src/ ./src/

# Production stage (multi-stage build)
FROM python:3.11-slim

WORKDIR /app

# Create non-root user
RUN useradd --create-home appuser
USER appuser

# Copy from builder
COPY --from=builder /app /app

# Set environment
ENV PYTHONUNBUFFERED=1

# Health check
HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:8000/health || exit 1

EXPOSE 8000
CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0"]

Dockerfile Rules

DO:
- Use specific base image versions
- Use multi-stage builds
- Run as non-root user
- Order commands by change frequency
- Use .dockerignore
- Add health checks

DON'T:
- Use :latest tag
- Run as root
- Copy unnecessary files
- Store secrets in image
- Install dev dependencies in production

Docker Compose

# docker-compose.yml
version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/app
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  db:
    image: postgres:15-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d app"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:

Kubernetes Basics

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
  labels:
    app: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: app
        image: myapp:1.0.0
        ports:
        - containerPort: 8000
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 10
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /ready
            port: 8000
          initialDelaySeconds: 5
          periodSeconds: 10
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: database-url

Service

apiVersion: v1
kind: Service
metadata:
  name: app-service
spec:
  selector:
    app: myapp
  ports:
  - port: 80
    targetPort: 8000
  type: ClusterIP

Ingress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: app-service
            port:
              number: 80

kubectl Quick Reference

Command Description
kubectl get pods List pods
kubectl logs <pod> View logs
kubectl exec -it <pod> -- sh Shell into pod
kubectl apply -f manifest.yaml Apply config
kubectl rollout restart deployment/app Restart deployment
kubectl rollout status deployment/app Check rollout
kubectl describe pod <pod> Debug pod
kubectl port-forward svc/app 8080:80 Local port forward

Additional Resources

  • ./references/dockerfile-patterns.md - Advanced Dockerfile techniques
  • ./references/k8s-manifests.md - Full Kubernetes manifest examples
  • ./references/helm-patterns.md - Helm chart structure and values
  • ./references/ecs-fargate.md - Amazon ECS on AWS Fargate (task definitions, services, awsvpc networking, IAM roles, secrets, scaling, ALB/NLB, ECS vs Kubernetes)

Scripts

  • ./scripts/build-push.sh - Build and push Docker image

Assets

  • ./assets/Dockerfile.template - Production Dockerfile template
  • ./assets/docker-compose.template.yml - Compose starter template
Files (claude-mods)
  • assets
    • docker-compose.template.yml 4.1 KB
      # Docker Compose Template
      # For local development and testing
      
      version: "3.9"
      
      services:
        # ==============================================================================
        # Application
        # ==============================================================================
        app:
          build:
            context: .
            dockerfile: Dockerfile
            # For development, use debug target
            # target: debug
          image: ${IMAGE_NAME:-myapp}:${IMAGE_TAG:-latest}
          container_name: myapp
          restart: unless-stopped
          ports:
            - "${APP_PORT:-8000}:8000"
          environment:
            - DATABASE_URL=postgres://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@db:5432/${DB_NAME:-myapp}
            - REDIS_URL=redis://redis:6379/0
            - LOG_LEVEL=${LOG_LEVEL:-info}
          depends_on:
            db:
              condition: service_healthy
            redis:
              condition: service_healthy
          healthcheck:
            test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
            interval: 30s
            timeout: 10s
            retries: 3
            start_period: 10s
          volumes:
            # Development: mount source code
            # - ./src:/app/src:ro
            - app_logs:/app/logs
          networks:
            - app-network
      
        # ==============================================================================
        # Database
        # ==============================================================================
        db:
          image: postgres:15-alpine
          container_name: myapp-db
          restart: unless-stopped
          environment:
            POSTGRES_USER: ${DB_USER:-postgres}
            POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
            POSTGRES_DB: ${DB_NAME:-myapp}
          ports:
            - "${DB_PORT:-5432}:5432"
          volumes:
            - postgres_data:/var/lib/postgresql/data
            # - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
          healthcheck:
            test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-myapp}"]
            interval: 10s
            timeout: 5s
            retries: 5
          networks:
            - app-network
      
        # ==============================================================================
        # Cache
        # ==============================================================================
        redis:
          image: redis:7-alpine
          container_name: myapp-redis
          restart: unless-stopped
          command: redis-server --appendonly yes
          ports:
            - "${REDIS_PORT:-6379}:6379"
          volumes:
            - redis_data:/data
          healthcheck:
            test: ["CMD", "redis-cli", "ping"]
            interval: 10s
            timeout: 5s
            retries: 5
          networks:
            - app-network
      
        # ==============================================================================
        # Optional: Worker (for background jobs)
        # ==============================================================================
        # worker:
        #   build:
        #     context: .
        #     dockerfile: Dockerfile
        #   container_name: myapp-worker
        #   restart: unless-stopped
        #   command: python -m celery -A src.worker worker --loglevel=info
        #   environment:
        #     - DATABASE_URL=postgres://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@db:5432/${DB_NAME:-myapp}
        #     - REDIS_URL=redis://redis:6379/0
        #   depends_on:
        #     - db
        #     - redis
        #   networks:
        #     - app-network
      
        # ==============================================================================
        # Optional: Nginx (reverse proxy)
        # ==============================================================================
        # nginx:
        #   image: nginx:alpine
        #   container_name: myapp-nginx
        #   restart: unless-stopped
        #   ports:
        #     - "80:80"
        #     - "443:443"
        #   volumes:
        #     - ./nginx.conf:/etc/nginx/nginx.conf:ro
        #     - ./certs:/etc/nginx/certs:ro
        #   depends_on:
        #     - app
        #   networks:
        #     - app-network
      
      # ==============================================================================
      # Volumes
      # ==============================================================================
      volumes:
        postgres_data:
          driver: local
        redis_data:
          driver: local
        app_logs:
          driver: local
      
      # ==============================================================================
      # Networks
      # ==============================================================================
      networks:
        app-network:
          driver: bridge
      
    • Dockerfile.template 1.9 KB · in bundle
  • references
    • dockerfile-patterns.md 6.3 KB
      # Advanced Dockerfile Patterns
      
      Production-ready Dockerfile techniques.
      
      ## Multi-Stage Builds
      
      ### Python Application
      
      ```dockerfile
      # Stage 1: Build dependencies
      FROM python:3.11-slim AS builder
      
      WORKDIR /app
      
      # Install build dependencies
      RUN apt-get update && apt-get install -y --no-install-recommends \
          build-essential \
          && rm -rf /var/lib/apt/lists/*
      
      # Create virtual environment
      RUN python -m venv /opt/venv
      ENV PATH="/opt/venv/bin:$PATH"
      
      # Install dependencies
      COPY requirements.txt .
      RUN pip install --no-cache-dir -r requirements.txt
      
      # Stage 2: Production image
      FROM python:3.11-slim
      
      WORKDIR /app
      
      # Copy virtual environment from builder
      COPY --from=builder /opt/venv /opt/venv
      ENV PATH="/opt/venv/bin:$PATH"
      
      # Create non-root user
      RUN useradd --create-home --shell /bin/bash appuser
      USER appuser
      
      # Copy application
      COPY --chown=appuser:appuser src/ ./src/
      
      EXPOSE 8000
      CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0"]
      ```
      
      ### Node.js Application
      
      ```dockerfile
      # Stage 1: Dependencies
      FROM node:20-alpine AS deps
      WORKDIR /app
      COPY package*.json ./
      RUN npm ci --only=production
      
      # Stage 2: Build
      FROM node:20-alpine AS builder
      WORKDIR /app
      COPY package*.json ./
      RUN npm ci
      COPY . .
      RUN npm run build
      
      # Stage 3: Production
      FROM node:20-alpine AS runner
      WORKDIR /app
      
      ENV NODE_ENV=production
      RUN addgroup --system --gid 1001 nodejs
      RUN adduser --system --uid 1001 nextjs
      
      COPY --from=deps /app/node_modules ./node_modules
      COPY --from=builder /app/dist ./dist
      COPY --from=builder /app/package.json ./
      
      USER nextjs
      EXPOSE 3000
      CMD ["node", "dist/index.js"]
      ```
      
      ### Go Application
      
      ```dockerfile
      # Stage 1: Build
      FROM golang:1.21-alpine AS builder
      
      WORKDIR /app
      
      # Cache dependencies
      COPY go.mod go.sum ./
      RUN go mod download
      
      # Build
      COPY . .
      RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server
      
      # Stage 2: Minimal runtime
      FROM scratch
      
      # Copy CA certificates for HTTPS
      COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
      
      # Copy binary
      COPY --from=builder /app/server /server
      
      EXPOSE 8080
      ENTRYPOINT ["/server"]
      ```
      
      ## Layer Optimization
      
      ### Order by Change Frequency
      
      ```dockerfile
      # Least frequently changed first
      FROM python:3.11-slim
      
      # System packages (rarely change)
      RUN apt-get update && apt-get install -y \
          libpq5 \
          && rm -rf /var/lib/apt/lists/*
      
      # Dependencies (change occasionally)
      COPY requirements.txt .
      RUN pip install --no-cache-dir -r requirements.txt
      
      # Application code (changes frequently)
      COPY src/ ./src/
      
      CMD ["python", "-m", "src.main"]
      ```
      
      ### Combine RUN Commands
      
      ```dockerfile
      # BAD - Multiple layers
      RUN apt-get update
      RUN apt-get install -y curl
      RUN apt-get install -y git
      RUN rm -rf /var/lib/apt/lists/*
      
      # GOOD - Single layer
      RUN apt-get update && apt-get install -y --no-install-recommends \
          curl \
          git \
          && rm -rf /var/lib/apt/lists/*
      ```
      
      ## Security Best Practices
      
      ### Non-Root User
      
      ```dockerfile
      # Create user with specific UID
      RUN groupadd --gid 1000 appgroup \
          && useradd --uid 1000 --gid appgroup --shell /bin/bash --create-home appuser
      
      # Switch to user
      USER appuser
      
      # Copy files with correct ownership
      COPY --chown=appuser:appgroup src/ ./src/
      ```
      
      ### Read-Only Root Filesystem
      
      ```dockerfile
      # Use with docker run --read-only
      FROM python:3.11-slim
      
      # Create writable directories
      RUN mkdir -p /tmp /var/log/app \
          && chown -R appuser:appuser /tmp /var/log/app
      
      USER appuser
      
      # Application writes only to /tmp and /var/log/app
      ```
      
      ### No Secrets in Image
      
      ```dockerfile
      # WRONG - Secret in build arg
      ARG API_KEY
      ENV API_KEY=${API_KEY}
      
      # CORRECT - Secret at runtime
      # Pass via environment variable or secret manager
      ENV API_KEY=""  # Set at runtime
      ```
      
      ### Minimal Base Image
      
      ```dockerfile
      # Full image: ~1GB
      FROM python:3.11
      
      # Slim image: ~150MB
      FROM python:3.11-slim
      
      # Alpine image: ~50MB (but musl libc issues)
      FROM python:3.11-alpine
      
      # Distroless: Minimal, no shell
      FROM gcr.io/distroless/python3-debian12
      ```
      
      ## Health Checks
      
      ```dockerfile
      # HTTP health check
      HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
          CMD curl -f http://localhost:8000/health || exit 1
      
      # Without curl (for minimal images)
      HEALTHCHECK --interval=30s --timeout=3s \
          CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
      
      # TCP health check
      HEALTHCHECK --interval=30s --timeout=3s \
          CMD nc -z localhost 8000 || exit 1
      ```
      
      ## Build Arguments
      
      ```dockerfile
      # Declare build args
      ARG PYTHON_VERSION=3.11
      ARG APP_ENV=production
      
      FROM python:${PYTHON_VERSION}-slim
      
      # Use in ENV
      ARG APP_ENV
      ENV APP_ENV=${APP_ENV}
      
      # Conditional logic
      RUN if [ "$APP_ENV" = "development" ]; then \
              pip install debugpy pytest; \
          fi
      ```
      
      ## Caching Strategies
      
      ### Mount Cache (BuildKit)
      
      ```dockerfile
      # syntax=docker/dockerfile:1.4
      
      # Cache pip downloads
      RUN --mount=type=cache,target=/root/.cache/pip \
          pip install -r requirements.txt
      
      # Cache apt packages
      RUN --mount=type=cache,target=/var/cache/apt \
          apt-get update && apt-get install -y curl
      ```
      
      ### Bind Mounts for Build
      
      ```dockerfile
      # syntax=docker/dockerfile:1.4
      
      # Mount source code without copying
      RUN --mount=type=bind,source=src,target=/app/src \
          python -m compileall /app/src
      ```
      
      ## Labels and Metadata
      
      ```dockerfile
      LABEL org.opencontainers.image.title="My App"
      LABEL org.opencontainers.image.description="Production application"
      LABEL org.opencontainers.image.version="1.0.0"
      LABEL org.opencontainers.image.vendor="Company"
      LABEL org.opencontainers.image.source="https://github.com/org/repo"
      ```
      
      ## .dockerignore
      
      ```
      # .dockerignore
      .git
      .gitignore
      .env
      .env.*
      *.md
      !README.md
      Dockerfile*
      docker-compose*
      .dockerignore
      
      # Python
      __pycache__
      *.pyc
      *.pyo
      .pytest_cache
      .coverage
      htmlcov
      .venv
      venv
      
      # Node
      node_modules
      npm-debug.log
      .npm
      
      # IDE
      .idea
      .vscode
      *.swp
      ```
      
      ## Debug Container
      
      ```dockerfile
      # Multi-stage with debug target
      FROM python:3.11-slim AS base
      WORKDIR /app
      COPY requirements.txt .
      RUN pip install -r requirements.txt
      COPY src/ ./src/
      
      # Debug stage
      FROM base AS debug
      RUN pip install debugpy
      CMD ["python", "-m", "debugpy", "--listen", "0.0.0.0:5678", "-m", "src.main"]
      
      # Production stage
      FROM base AS production
      USER appuser
      CMD ["python", "-m", "src.main"]
      ```
      
      Build specific target:
      ```bash
      docker build --target debug -t myapp:debug .
      docker build --target production -t myapp:latest .
      ```
      
    • ecs-fargate.md 6.7 KB
      # Amazon ECS on AWS Fargate
      
      The rest of this skill covers Docker and Kubernetes. ECS on Fargate is a distinct
      orchestrator: AWS-native, serverless containers — no nodes to patch, no kubelet, no
      control plane to run. Reach here for production-grade Fargate workloads.
      
      Verify specifics against the canonical sources (links per section); ECS features move
      across platform versions.
      
      ## ECS vs Kubernetes — when to pick Fargate
      
      | | ECS on Fargate | Kubernetes (EKS / self-managed) |
      |---|---|---|
      | Ops burden | None — no nodes, no control plane | You run/patch nodes (EKS manages the control plane) |
      | Portability | AWS-only | Portable across clouds |
      | Ecosystem | AWS-native (ALB, IAM, CloudWatch, Secrets Manager) | Huge CNCF ecosystem, Helm, operators |
      | Learning curve | Low | High |
      | Best for | AWS-committed teams wanting minimal infra ops | Multi-cloud, complex scheduling, existing k8s investment |
      
      Pick Fargate when you're AWS-committed and want to stop managing container hosts.
      Pick Kubernetes when you need portability, advanced scheduling, or already run k8s.
      Note **EKS can also run on Fargate** — that's k8s pods on serverless capacity, a
      different product from the ECS-on-Fargate covered here.
      
      ## Core building blocks
      
      - **Task definition** — the blueprint: container image(s), CPU/memory at the task
        level (Fargate requires valid CPU/memory pairs, e.g. 0.25 vCPU / 0.5 GB up to
        16 vCPU / 120 GB), `awsvpc` network mode (mandatory on Fargate), log config, the
        two IAM roles (below), and `secrets` mappings.
      - **Service** — keeps N task copies running, registers them with a load balancer,
        handles rolling or blue/green deploys, and integrates Service Auto Scaling and
        Service Connect / service discovery.
      - **Cluster** — logical grouping; with Fargate it's effectively just a namespace
        plus a capacity-provider strategy.
      - **Capacity providers** — `FARGATE` (on-demand) and `FARGATE_SPOT` (interruptible,
        up to ~70% cheaper). Mix them with a strategy (e.g. a base of FARGATE plus a
        weighted FARGATE_SPOT layer) to cut cost while protecting a reliable baseline.
      
      Architecting for AWS Fargate:
      https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html ·
      ECS Developer Guide:
      https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html
      
      ## Networking (awsvpc)
      
      Every Fargate task gets its own ENI with a private IP — security groups attach to the
      *task*, not a shared host.
      
      - Run tasks in **private subnets**; reach the internet via a NAT Gateway, or skip NAT
        with **VPC interface endpoints** (ECR, Secrets Manager, CloudWatch Logs, etc.) to
        cut NAT cost and keep traffic on the AWS backbone.
      - **Security groups**: the task SG is the source. Common gotcha — a task can't reach
        RDS even though SGs "look right": the RDS SG must allow inbound *from the task's SG*
        (reference the SG id, not a CIDR), and the task must be in a subnet with a route to
        RDS.
      - **Load balancing**: **ALB** for HTTP/HTTPS (path/host routing, TLS termination) —
        the default for web services; **NLB** for TCP/UDP, ultra-low latency, or a static
        IP. The service registers task ENIs into the target group as `ip` targets.
      - **Service Connect** (preferred) or ECS Service Discovery for service-to-service
        communication with built-in naming and health.
      
      ## IAM — two distinct roles
      
      | Role | Used by | Grants |
      |------|---------|--------|
      | **Task execution role** | the ECS agent, at launch | Pull the image from ECR, write logs, fetch `secrets` from Secrets Manager / SSM |
      | **Task role** | your application, at runtime | The app's own AWS permissions (S3, DynamoDB, SQS…) |
      
      Keep them separate and least-privilege. Don't grant the app's S3 access on the
      execution role, and don't put image-pull permission on the task role.
      
      ## Secrets
      
      Inject via the task definition `secrets` block from **Secrets Manager** or **SSM
      Parameter Store** — values land as env vars at start, never baked into the image or
      committed. The *execution role* needs read access to the secret (and to the KMS key
      if the secret is CMK-encrypted).
      
      ## Scaling & cost
      
      - **Service Auto Scaling** via Application Auto Scaling: target tracking (e.g. hold
        CPU at 60%, or scale on ALB requests-per-target), step scaling, or scheduled
        scaling for known peaks.
      - **Cost levers**: FARGATE_SPOT for fault-tolerant/stateless work, right-size task
        CPU/memory (Container Insights shows actual utilization), Compute Savings Plans for
        steady baseline load, VPC endpoints to drop NAT data charges.
      
      ## Observability & deploys
      
      - **Logging** — `awslogs` driver to CloudWatch is the simple default; **FireLens**
        (Fluent Bit sidecar) when you need to route/transform logs to a third party or
        multiple sinks.
      - **Metrics** — CloudWatch Container Insights for per-task CPU/memory/network;
        alarm on those.
      - **Debugging** — **ECS Exec** opens an interactive shell into a running task (no SSH,
        no public IP). Needs ECS Exec enabled on the service and the SSM permissions on the
        task role.
      - **Deploys** — rolling (built-in, `minimumHealthyPercent` / `maximumPercent`) or
        **blue/green** via CodeDeploy (shift traffic, auto-rollback on alarm). Always wire
        container **health checks** so a bad revision fails the deployment instead of
        serving errors.
      
      ## Deployment tooling
      
      | Tool | Best for |
      |------|----------|
      | **AWS Copilot** | Fastest path — opinionated, generates the VPC/ALB/service for you. Great for getting a service live and managing environments. |
      | **AWS CDK** | Real IaC with `ecs-patterns` L3 constructs (e.g. `ApplicationLoadBalancedFargateService`); programmable, testable. |
      | **CloudFormation** | Declarative, no extra runtime; verbose. |
      | **Terraform** | When the org standardizes on Terraform across clouds. |
      
      Copilot to start fast; CDK/Terraform when you need full control and review.
      
      ## Common failure modes
      
      | Symptom | Likely cause |
      |---------|--------------|
      | Task can't reach RDS (timeout) | RDS SG doesn't allow the task SG; or task subnet has no route |
      | `CannotPullContainerError` | Execution role lacks ECR pull, or no route to ECR (no NAT / no VPC endpoint) |
      | Secret injection fails at start | Execution role missing Secrets Manager / SSM (and KMS) read permission |
      | Tasks killed under load | Task CPU/memory under-provisioned — check Container Insights, right-size |
      | Spot tasks vanish | FARGATE_SPOT interruption — add a FARGATE base in the capacity strategy |
      
      ## Canonical references
      
      - AWS Fargate architecture: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html
      - ECS Developer Guide: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html
      - ECS Best Practices: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-best-practices.html
      - Getting started with Fargate: https://aws.amazon.com/fargate/getting-started/
      
    • helm-patterns.md 7.4 KB
      # Helm Chart Patterns
      
      Production Helm chart structure and patterns.
      
      ## Chart Structure
      
      ```
      myapp/
      ├── Chart.yaml
      ├── values.yaml
      ├── values-staging.yaml
      ├── values-production.yaml
      ├── templates/
      │   ├── _helpers.tpl
      │   ├── deployment.yaml
      │   ├── service.yaml
      │   ├── ingress.yaml
      │   ├── configmap.yaml
      │   ├── secret.yaml
      │   ├── hpa.yaml
      │   ├── pdb.yaml
      │   └── NOTES.txt
      └── charts/           # Dependencies
      ```
      
      ## Chart.yaml
      
      ```yaml
      apiVersion: v2
      name: myapp
      description: My Application Helm Chart
      type: application
      version: 1.0.0
      appVersion: "2.0.0"
      keywords:
        - web
        - api
      maintainers:
        - name: Team
          email: team@example.com
      dependencies:
        - name: postgresql
          version: "12.x.x"
          repository: https://charts.bitnami.com/bitnami
          condition: postgresql.enabled
      ```
      
      ## values.yaml
      
      ```yaml
      # Default values for myapp
      
      replicaCount: 3
      
      image:
        repository: myregistry/myapp
        pullPolicy: IfNotPresent
        tag: ""  # Defaults to appVersion
      
      imagePullSecrets: []
      nameOverride: ""
      fullnameOverride: ""
      
      serviceAccount:
        create: true
        annotations: {}
        name: ""
      
      podAnnotations: {}
      
      podSecurityContext:
        runAsNonRoot: true
        runAsUser: 1000
        fsGroup: 1000
      
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop:
            - ALL
      
      service:
        type: ClusterIP
        port: 80
      
      ingress:
        enabled: false
        className: nginx
        annotations: {}
        hosts:
          - host: app.example.com
            paths:
              - path: /
                pathType: Prefix
        tls: []
      
      resources:
        limits:
          cpu: 500m
          memory: 512Mi
        requests:
          cpu: 100m
          memory: 128Mi
      
      autoscaling:
        enabled: true
        minReplicas: 3
        maxReplicas: 10
        targetCPUUtilizationPercentage: 70
        targetMemoryUtilizationPercentage: 80
      
      pdb:
        enabled: true
        minAvailable: 2
      
      nodeSelector: {}
      tolerations: []
      affinity: {}
      
      # Application config
      config:
        logLevel: info
        cacheTtl: 3600
      
      # Secrets (use external secrets in production)
      secrets:
        databaseUrl: ""
        apiKey: ""
      
      # Database dependency
      postgresql:
        enabled: false
        auth:
          database: myapp
      ```
      
      ## Helper Template (_helpers.tpl)
      
      ```yaml
      {{/*
      Expand the name of the chart.
      */}}
      {{- define "myapp.name" -}}
      {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
      {{- end }}
      
      {{/*
      Create a default fully qualified app name.
      */}}
      {{- define "myapp.fullname" -}}
      {{- if .Values.fullnameOverride }}
      {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
      {{- else }}
      {{- $name := default .Chart.Name .Values.nameOverride }}
      {{- if contains $name .Release.Name }}
      {{- .Release.Name | trunc 63 | trimSuffix "-" }}
      {{- else }}
      {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
      {{- end }}
      {{- end }}
      {{- end }}
      
      {{/*
      Create chart name and version as used by the chart label.
      */}}
      {{- define "myapp.chart" -}}
      {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
      {{- end }}
      
      {{/*
      Common labels
      */}}
      {{- define "myapp.labels" -}}
      helm.sh/chart: {{ include "myapp.chart" . }}
      {{ include "myapp.selectorLabels" . }}
      {{- if .Chart.AppVersion }}
      app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
      {{- end }}
      app.kubernetes.io/managed-by: {{ .Release.Service }}
      {{- end }}
      
      {{/*
      Selector labels
      */}}
      {{- define "myapp.selectorLabels" -}}
      app.kubernetes.io/name: {{ include "myapp.name" . }}
      app.kubernetes.io/instance: {{ .Release.Name }}
      {{- end }}
      
      {{/*
      Create the name of the service account to use
      */}}
      {{- define "myapp.serviceAccountName" -}}
      {{- if .Values.serviceAccount.create }}
      {{- default (include "myapp.fullname" .) .Values.serviceAccount.name }}
      {{- else }}
      {{- default "default" .Values.serviceAccount.name }}
      {{- end }}
      {{- end }}
      ```
      
      ## Deployment Template
      
      ```yaml
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: {{ include "myapp.fullname" . }}
        labels:
          {{- include "myapp.labels" . | nindent 4 }}
      spec:
        {{- if not .Values.autoscaling.enabled }}
        replicas: {{ .Values.replicaCount }}
        {{- end }}
        selector:
          matchLabels:
            {{- include "myapp.selectorLabels" . | nindent 6 }}
        template:
          metadata:
            annotations:
              checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
              {{- with .Values.podAnnotations }}
              {{- toYaml . | nindent 8 }}
              {{- end }}
            labels:
              {{- include "myapp.selectorLabels" . | nindent 8 }}
          spec:
            {{- with .Values.imagePullSecrets }}
            imagePullSecrets:
              {{- toYaml . | nindent 8 }}
            {{- end }}
            serviceAccountName: {{ include "myapp.serviceAccountName" . }}
            securityContext:
              {{- toYaml .Values.podSecurityContext | nindent 8 }}
            containers:
              - name: {{ .Chart.Name }}
                securityContext:
                  {{- toYaml .Values.securityContext | nindent 12 }}
                image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
                imagePullPolicy: {{ .Values.image.pullPolicy }}
                ports:
                  - name: http
                    containerPort: 8000
                    protocol: TCP
                livenessProbe:
                  httpGet:
                    path: /health
                    port: http
                  initialDelaySeconds: 10
                  periodSeconds: 30
                readinessProbe:
                  httpGet:
                    path: /ready
                    port: http
                  initialDelaySeconds: 5
                  periodSeconds: 10
                resources:
                  {{- toYaml .Values.resources | nindent 12 }}
                envFrom:
                  - configMapRef:
                      name: {{ include "myapp.fullname" . }}
                  - secretRef:
                      name: {{ include "myapp.fullname" . }}
                volumeMounts:
                  - name: tmp
                    mountPath: /tmp
            volumes:
              - name: tmp
                emptyDir: {}
            {{- with .Values.nodeSelector }}
            nodeSelector:
              {{- toYaml . | nindent 8 }}
            {{- end }}
            {{- with .Values.affinity }}
            affinity:
              {{- toYaml . | nindent 8 }}
            {{- end }}
            {{- with .Values.tolerations }}
            tolerations:
              {{- toYaml . | nindent 8 }}
            {{- end }}
      ```
      
      ## Helm Commands
      
      ```bash
      # Install
      helm install myapp ./myapp -f values-production.yaml
      
      # Upgrade
      helm upgrade myapp ./myapp -f values-production.yaml
      
      # Dry run
      helm install myapp ./myapp --dry-run --debug
      
      # Template output
      helm template myapp ./myapp -f values-production.yaml
      
      # Rollback
      helm rollback myapp 1
      
      # History
      helm history myapp
      
      # Uninstall
      helm uninstall myapp
      ```
      
      ## Environment-Specific Values
      
      ### values-staging.yaml
      
      ```yaml
      replicaCount: 2
      
      ingress:
        enabled: true
        hosts:
          - host: staging.app.example.com
            paths:
              - path: /
                pathType: Prefix
        tls:
          - secretName: staging-tls
            hosts:
              - staging.app.example.com
      
      resources:
        limits:
          cpu: 250m
          memory: 256Mi
        requests:
          cpu: 50m
          memory: 64Mi
      
      autoscaling:
        enabled: false
      ```
      
      ### values-production.yaml
      
      ```yaml
      replicaCount: 3
      
      ingress:
        enabled: true
        annotations:
          nginx.ingress.kubernetes.io/ssl-redirect: "true"
        hosts:
          - host: app.example.com
            paths:
              - path: /
                pathType: Prefix
        tls:
          - secretName: production-tls
            hosts:
              - app.example.com
      
      resources:
        limits:
          cpu: 500m
          memory: 512Mi
        requests:
          cpu: 100m
          memory: 128Mi
      
      autoscaling:
        enabled: true
        minReplicas: 3
        maxReplicas: 20
      
      pdb:
        enabled: true
        minAvailable: 2
      ```
      
    • k8s-manifests.md 6.9 KB
      # Kubernetes Manifests
      
      Production Kubernetes configuration examples.
      
      ## Complete Application Stack
      
      ### Namespace
      
      ```yaml
      apiVersion: v1
      kind: Namespace
      metadata:
        name: myapp
        labels:
          app: myapp
      ```
      
      ### ConfigMap
      
      ```yaml
      apiVersion: v1
      kind: ConfigMap
      metadata:
        name: app-config
        namespace: myapp
      data:
        LOG_LEVEL: "info"
        CACHE_TTL: "3600"
        config.yaml: |
          server:
            port: 8000
            workers: 4
          database:
            pool_size: 10
      ```
      
      ### Secret
      
      ```yaml
      apiVersion: v1
      kind: Secret
      metadata:
        name: app-secrets
        namespace: myapp
      type: Opaque
      stringData:
        DATABASE_URL: postgres://user:pass@db:5432/app
        API_KEY: supersecretkey
      ---
      # External Secrets (for AWS Secrets Manager, etc.)
      apiVersion: external-secrets.io/v1beta1
      kind: ExternalSecret
      metadata:
        name: app-secrets
        namespace: myapp
      spec:
        refreshInterval: 1h
        secretStoreRef:
          name: aws-secrets-manager
          kind: SecretStore
        target:
          name: app-secrets
        data:
        - secretKey: DATABASE_URL
          remoteRef:
            key: myapp/database-url
      ```
      
      ### Deployment
      
      ```yaml
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: app
        namespace: myapp
        labels:
          app: myapp
          version: v1
      spec:
        replicas: 3
        selector:
          matchLabels:
            app: myapp
        strategy:
          type: RollingUpdate
          rollingUpdate:
            maxSurge: 1
            maxUnavailable: 0
        template:
          metadata:
            labels:
              app: myapp
              version: v1
            annotations:
              prometheus.io/scrape: "true"
              prometheus.io/port: "8000"
          spec:
            serviceAccountName: app-service-account
            securityContext:
              runAsNonRoot: true
              runAsUser: 1000
              fsGroup: 1000
            containers:
            - name: app
              image: myregistry/myapp:1.0.0
              imagePullPolicy: IfNotPresent
              ports:
              - name: http
                containerPort: 8000
                protocol: TCP
              env:
              - name: LOG_LEVEL
                valueFrom:
                  configMapKeyRef:
                    name: app-config
                    key: LOG_LEVEL
              - name: DATABASE_URL
                valueFrom:
                  secretKeyRef:
                    name: app-secrets
                    key: DATABASE_URL
              resources:
                requests:
                  memory: "128Mi"
                  cpu: "100m"
                limits:
                  memory: "512Mi"
                  cpu: "500m"
              livenessProbe:
                httpGet:
                  path: /health
                  port: http
                initialDelaySeconds: 10
                periodSeconds: 30
                timeoutSeconds: 5
                failureThreshold: 3
              readinessProbe:
                httpGet:
                  path: /ready
                  port: http
                initialDelaySeconds: 5
                periodSeconds: 10
                timeoutSeconds: 3
                failureThreshold: 3
              securityContext:
                allowPrivilegeEscalation: false
                readOnlyRootFilesystem: true
                capabilities:
                  drop:
                  - ALL
              volumeMounts:
              - name: tmp
                mountPath: /tmp
              - name: config
                mountPath: /app/config
                readOnly: true
            volumes:
            - name: tmp
              emptyDir: {}
            - name: config
              configMap:
                name: app-config
            affinity:
              podAntiAffinity:
                preferredDuringSchedulingIgnoredDuringExecution:
                - weight: 100
                  podAffinityTerm:
                    labelSelector:
                      matchLabels:
                        app: myapp
                    topologyKey: kubernetes.io/hostname
      ```
      
      ### Service
      
      ```yaml
      apiVersion: v1
      kind: Service
      metadata:
        name: app-service
        namespace: myapp
      spec:
        type: ClusterIP
        selector:
          app: myapp
        ports:
        - name: http
          port: 80
          targetPort: http
          protocol: TCP
      ```
      
      ### Ingress
      
      ```yaml
      apiVersion: networking.k8s.io/v1
      kind: Ingress
      metadata:
        name: app-ingress
        namespace: myapp
        annotations:
          nginx.ingress.kubernetes.io/ssl-redirect: "true"
          nginx.ingress.kubernetes.io/proxy-body-size: "10m"
          cert-manager.io/cluster-issuer: "letsencrypt-prod"
      spec:
        ingressClassName: nginx
        tls:
        - hosts:
          - app.example.com
          secretName: app-tls
        rules:
        - host: app.example.com
          http:
            paths:
            - path: /
              pathType: Prefix
              backend:
                service:
                  name: app-service
                  port:
                    number: 80
      ```
      
      ### HorizontalPodAutoscaler
      
      ```yaml
      apiVersion: autoscaling/v2
      kind: HorizontalPodAutoscaler
      metadata:
        name: app-hpa
        namespace: myapp
      spec:
        scaleTargetRef:
          apiVersion: apps/v1
          kind: Deployment
          name: app
        minReplicas: 3
        maxReplicas: 10
        metrics:
        - type: Resource
          resource:
            name: cpu
            target:
              type: Utilization
              averageUtilization: 70
        - type: Resource
          resource:
            name: memory
            target:
              type: Utilization
              averageUtilization: 80
        behavior:
          scaleDown:
            stabilizationWindowSeconds: 300
            policies:
            - type: Percent
              value: 10
              periodSeconds: 60
          scaleUp:
            stabilizationWindowSeconds: 0
            policies:
            - type: Percent
              value: 100
              periodSeconds: 15
      ```
      
      ### PodDisruptionBudget
      
      ```yaml
      apiVersion: policy/v1
      kind: PodDisruptionBudget
      metadata:
        name: app-pdb
        namespace: myapp
      spec:
        minAvailable: 2
        selector:
          matchLabels:
            app: myapp
      ```
      
      ### ServiceAccount and RBAC
      
      ```yaml
      apiVersion: v1
      kind: ServiceAccount
      metadata:
        name: app-service-account
        namespace: myapp
      ---
      apiVersion: rbac.authorization.k8s.io/v1
      kind: Role
      metadata:
        name: app-role
        namespace: myapp
      rules:
      - apiGroups: [""]
        resources: ["configmaps", "secrets"]
        verbs: ["get", "list"]
      ---
      apiVersion: rbac.authorization.k8s.io/v1
      kind: RoleBinding
      metadata:
        name: app-role-binding
        namespace: myapp
      subjects:
      - kind: ServiceAccount
        name: app-service-account
        namespace: myapp
      roleRef:
        kind: Role
        name: app-role
        apiGroup: rbac.authorization.k8s.io
      ```
      
      ### NetworkPolicy
      
      ```yaml
      apiVersion: networking.k8s.io/v1
      kind: NetworkPolicy
      metadata:
        name: app-network-policy
        namespace: myapp
      spec:
        podSelector:
          matchLabels:
            app: myapp
        policyTypes:
        - Ingress
        - Egress
        ingress:
        - from:
          - namespaceSelector:
              matchLabels:
                name: ingress-nginx
          ports:
          - protocol: TCP
            port: 8000
        egress:
        - to:
          - namespaceSelector:
              matchLabels:
                name: database
          ports:
          - protocol: TCP
            port: 5432
        - to:
          - namespaceSelector: {}
          ports:
          - protocol: UDP
            port: 53  # DNS
      ```
      
      ### CronJob
      
      ```yaml
      apiVersion: batch/v1
      kind: CronJob
      metadata:
        name: cleanup-job
        namespace: myapp
      spec:
        schedule: "0 2 * * *"  # 2 AM daily
        concurrencyPolicy: Forbid
        successfulJobsHistoryLimit: 3
        failedJobsHistoryLimit: 1
        jobTemplate:
          spec:
            template:
              spec:
                restartPolicy: OnFailure
                containers:
                - name: cleanup
                  image: myregistry/myapp:1.0.0
                  command: ["python", "-m", "src.jobs.cleanup"]
                  resources:
                    limits:
                      memory: "256Mi"
                      cpu: "200m"
      ```
      
  • scripts
    • build-push.sh 5.3 KB
      #!/usr/bin/env bash
      # Build and (optionally) push a Docker image with a predictable, agent-safe CLI.
      #
      # Usage:   build-push.sh [--tag TAG] [--registry REG] [--push]
      #                        [--dockerfile FILE] [--context DIR] [--dry-run] [--help]
      # Input:   argv only. Env overrides: IMAGE_NAME, IMAGE_TAG, DOCKER_REGISTRY.
      # Output:  stdout carries the resolved plan as plain "Key: Value" data lines
      #          (Image / Dockerfile / Context, [+ Pushed] after a push) — identical
      #          data to the pre-backfill behaviour, so downstream parsers are
      #          unaffected. Under --dry-run the planned `docker build` command is
      #          appended and nothing is executed.
      # Stderr:  progress banners ("=== Building ... ===") and diagnostics.
      # Exit:    0 ok, 2 usage (unknown flag or missing value), 5 missing-dep (docker
      #          not on PATH), 1 build or push failed.
      #
      # Examples:
      #   build-push.sh --tag v1.2.3 --registry ghcr.io/acme --push
      #   IMAGE_NAME=svc build-push.sh --dry-run --tag dev
      #   build-push.sh --dockerfile Dockerfile.prod --context ./app
      set -uo pipefail
      
      usage() {
          cat <<'EOF'
      Usage: build-push.sh [OPTIONS]
      
      Build and optionally push a Docker image.
      
      Options:
        -t, --tag TAG          Image tag (default: $IMAGE_TAG or "latest").
        -r, --registry REG     Registry prefix, e.g. ghcr.io/acme (default: $DOCKER_REGISTRY).
        -p, --push             Push the image after a successful build.
        -f, --dockerfile FILE  Dockerfile path (default: Dockerfile).
        -c, --context DIR      Build context directory (default: .).
        -n, --dry-run          Resolve and print the plan WITHOUT invoking docker.
        -h, --help             Show this help and exit.
      
      Environment:
        IMAGE_NAME        Image name (default: current directory basename).
        IMAGE_TAG         Default tag.
        DOCKER_REGISTRY   Default registry prefix.
      
      Exit codes:
        0  success
        2  usage error (unknown flag or missing value)
        5  docker is not installed (missing dependency)
        1  build or push failed
      
      Examples:
        build-push.sh --tag v1.2.3 --registry ghcr.io/acme --push
        IMAGE_NAME=svc build-push.sh --dry-run --tag dev
        build-push.sh --dockerfile Dockerfile.prod --context ./app
      EOF
      }
      
      # Defaults
      REGISTRY="${DOCKER_REGISTRY:-}"
      TAG="${IMAGE_TAG:-latest}"
      PUSH=false
      DOCKERFILE="Dockerfile"
      CONTEXT="."
      DRY_RUN=false
      
      # Parse arguments — runs with NO docker dependency so --help / validation never
      # require a running daemon.
      need_value() {
          echo "build-push.sh: $1 requires a value" >&2
          exit 2
      }
      while [[ $# -gt 0 ]]; do
          case $1 in
              --tag|-t)
                  [[ $# -ge 2 ]] || need_value "$1"; TAG="$2"; shift 2 ;;
              --registry|-r)
                  [[ $# -ge 2 ]] || need_value "$1"; REGISTRY="$2"; shift 2 ;;
              --push|-p)
                  PUSH=true; shift ;;
              --dockerfile|-f)
                  [[ $# -ge 2 ]] || need_value "$1"; DOCKERFILE="$2"; shift 2 ;;
              --context|-c)
                  [[ $# -ge 2 ]] || need_value "$1"; CONTEXT="$2"; shift 2 ;;
              --dry-run|-n)
                  DRY_RUN=true; shift ;;
              --help|-h)
                  usage; exit 0 ;;
              *)
                  echo "build-push.sh: unknown option: $1" >&2
                  echo "Run 'build-push.sh --help' for usage." >&2
                  exit 2 ;;
          esac
      done
      
      # Resolve image name (env override, else current directory) and full reference.
      IMAGE_NAME="${IMAGE_NAME:-$(basename "$(pwd)")}"
      if [[ -n "$REGISTRY" ]]; then
          FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
      else
          FULL_IMAGE="${IMAGE_NAME}:${TAG}"
      fi
      
      # Progress banners are chatter -> stderr; the resolved Image/Dockerfile/Context
      # are the data product -> stdout (unchanged from pre-backfill so anything that
      # keyed off "Image: ..." keeps working).
      banner() { printf '=== %s ===\n' "$*" >&2; }
      
      # Dry-run: validate args and print the plan, never touching docker.
      if [[ "$DRY_RUN" = true ]]; then
          banner "Dry run (no docker invoked)"
          printf 'Image: %s\n' "$FULL_IMAGE"
          printf 'Dockerfile: %s\n' "$DOCKERFILE"
          printf 'Context: %s\n' "$CONTEXT"
          printf 'Push: %s\n' "$PUSH"
          printf 'Plan: docker build -t %s -f %s %s' "$FULL_IMAGE" "$DOCKERFILE" "$CONTEXT"
          [[ "$PUSH" = true ]] && printf ' && docker push %s' "$FULL_IMAGE"
          printf '\n'
          exit 0
      fi
      
      # Real path: docker is required from here on.
      if ! command -v docker >/dev/null 2>&1; then
          echo "build-push.sh: docker is not installed (or not on PATH)." >&2
          echo "  Install Docker: https://docs.docker.com/get-docker/" >&2
          exit 5
      fi
      
      banner "Building Docker Image"
      printf 'Image: %s\n' "$FULL_IMAGE"
      printf 'Dockerfile: %s\n' "$DOCKERFILE"
      printf 'Context: %s\n' "$CONTEXT"
      printf '\n'
      
      # Build — a failed build is a runtime error (exit 1), distinct from usage (2).
      if ! docker build \
              -t "$FULL_IMAGE" \
              -f "$DOCKERFILE" \
              --build-arg BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
              --build-arg VCS_REF="$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')" \
              "$CONTEXT"; then
          echo "build-push.sh: docker build failed" >&2
          exit 1
      fi
      
      banner "Build Complete"
      printf 'Image: %s\n' "$FULL_IMAGE"
      
      # Push if requested.
      if [[ "$PUSH" = true ]]; then
          banner "Pushing Image"
          if ! docker push "$FULL_IMAGE"; then
              echo "build-push.sh: docker push failed" >&2
              exit 1
          fi
          printf 'Pushed: %s\n' "$FULL_IMAGE"
      fi
      
      # Show image info.
      banner "Image Info"
      docker images "$FULL_IMAGE" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}"
      
  • tests
    • run.sh 7.7 KB
      #!/usr/bin/env bash
      # Self-test for container-orchestration — fully offline: never runs a real
      # `docker build`/`docker push` and never calls a real registry.
      #
      # build-push.sh ships real side effects, so this suite exercises ONLY its safe
      # surfaces: the protocol contract (--help / EXAMPLES / exit codes), arg parsing
      # through the no-docker `--dry-run` path, stream separation (data on stdout,
      # chatter on stderr), and a pre/post proof that the happy-path DATA output is
      # unchanged by the protocol backfill (only the chatter banners and --help were
      # added/moved). A sentinel `docker` on PATH proves the help/parse/dry-run paths
      # never invoke docker at all.
      #
      # Usage:   bash tests/run.sh
      # Exit:    0 all pass, 1 one or more failures
      
      set -uo pipefail
      
      HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      SKILL="$(dirname "$HERE")"
      V="$SKILL/scripts/build-push.sh"
      
      # Pre-backfill git blob of build-push.sh — used for a live pre/post data diff
      # when the object is still reachable in the repo's history.
      OLD_BLOB="0d6b10cdef2901784b3360a2c61ba2ea144b6bc2"
      
      SB="$(mktemp -d)"; trap 'rm -rf "$SB"' EXIT
      PASS=0; FAIL=0
      ok() { PASS=$((PASS+1)); printf '  PASS  %s\n' "$1"; }
      no() { FAIL=$((FAIL+1)); printf '  FAIL  %s\n' "$1"; }
      expect_exit() { [[ "$2" == "$3" ]] && ok "$1 (exit $3)" || no "$1 (want $2 got $3)"; }
      expect_has()  { case "$3" in *"$2"*) ok "$1";; *) no "$1 (missing '$2')";; esac; }
      
      # Sentinel `docker`: records any invocation into $DOCKER_SENTINEL_LOG, then
      # exits nonzero WITHOUT performing a real build/push. Prepended to PATH so we
      # can prove the safe paths never touch docker (and the baseline run of the old
      # script likewise performs no real build).
      mkdir -p "$SB/bin"
      cat > "$SB/bin/docker" <<'EOF'
      #!/usr/bin/env bash
      # Test sentinel — never builds/pushes; just records the call.
      printf 'docker %s\n' "$*" >> "${DOCKER_SENTINEL_LOG:-/dev/null}"
      exit 1
      EOF
      chmod +x "$SB/bin/docker"
      NO_DOCKER_PATH="$SB/bin:$PATH"
      no_docker_calls() { [[ ! -s "$SB/docker.log" ]]; }   # true == nothing was logged
      
      echo "=== container-orchestration self-test ==="
      
      # ── syntax + contract header ─────────────────────────────────────────────────
      echo "-- contract --"
      bash -n "$V" 2>/dev/null && ok "bash -n build-push.sh" || no "bash -n build-push.sh"
      hdr="$(head -25 "$V")"
      expect_has "header has Usage"     "Usage:"   "$hdr"
      expect_has "header has Output"    "Output:"  "$hdr"
      expect_has "header has Stderr"    "Stderr:"  "$hdr"
      expect_has "header has Exit"      "Exit:"    "$hdr"
      expect_has "header has Examples"  "xamples"  "$hdr"
      
      # ── --help / -h ──────────────────────────────────────────────────────────────
      echo "-- help --"
      PATH="$NO_DOCKER_PATH" bash "$V" --help >/dev/null 2>&1; expect_exit "--help exits 0" 0 $?
      PATH="$NO_DOCKER_PATH" bash "$V" -h     >/dev/null 2>&1; expect_exit "-h exits 0"     0 $?
      out="$(PATH="$NO_DOCKER_PATH" bash "$V" --help 2>/dev/null)"
      expect_has "--help lists --tag"      "--tag"      "$out"
      expect_has "--help lists --registry" "--registry" "$out"
      expect_has "--help lists --push"     "--push"     "$out"
      expect_has "--help lists --dry-run"  "--dry-run"  "$out"
      expect_has "--help documents exit 2" "2"          "$out"
      expect_has "--help documents exit 5" "5"          "$out"
      rm -f "$SB/docker.log"
      PATH="$NO_DOCKER_PATH" DOCKER_SENTINEL_LOG="$SB/docker.log" bash "$V" --help >/dev/null 2>&1
      no_docker_calls && ok "--help invokes no docker" || no "--help invoked docker"
      
      # ── usage errors (exit 2) ────────────────────────────────────────────────────
      echo "-- usage errors --"
      PATH="$NO_DOCKER_PATH" bash "$V" --bogus >/dev/null 2>&1; expect_exit "unknown flag -> 2" 2 $?
      PATH="$NO_DOCKER_PATH" bash "$V" --tag   >/dev/null 2>&1; expect_exit "missing --tag value -> 2" 2 $?
      PATH="$NO_DOCKER_PATH" bash "$V" --registry >/dev/null 2>&1; expect_exit "missing --registry value -> 2" 2 $?
      rm -f "$SB/docker.log"
      PATH="$NO_DOCKER_PATH" DOCKER_SENTINEL_LOG="$SB/docker.log" bash "$V" --bogus >/dev/null 2>&1
      no_docker_calls && ok "arg-parse error invokes no docker" || no "arg-parse error invoked docker"
      
      # ── arg parsing + resolution via --dry-run (no docker) ───────────────────────
      echo "-- dry-run resolution --"
      rm -f "$SB/docker.log"
      out="$(PATH="$NO_DOCKER_PATH" DOCKER_SENTINEL_LOG="$SB/docker.log" \
             IMAGE_NAME=myapp bash "$V" --dry-run --tag v1 --registry ghcr.io/acme 2>/dev/null)"; rc=$?
      expect_exit "dry-run exits 0" 0 "$rc"
      expect_has "resolves full image"    "Image: ghcr.io/acme/myapp:v1" "$out"
      expect_has "default dockerfile"     "Dockerfile: Dockerfile"      "$out"
      expect_has "default context"        "Context: ."                  "$out"
      expect_has "push flag shown false"  "Push: false"                 "$out"
      no_docker_calls && ok "dry-run invokes no docker" || no "dry-run invoked docker"
      
      out="$(PATH="$NO_DOCKER_PATH" IMAGE_NAME=svc bash "$V" --dry-run --push --tag dev 2>/dev/null)"
      expect_has "no registry -> bare image" "Image: svc:dev"      "$out"
      expect_has "push flag shown true"      "Push: true"          "$out"
      expect_has "plan includes build"       "docker build -t svc:dev -f Dockerfile ." "$out"
      expect_has "plan includes push"        "docker push svc:dev" "$out"
      
      out="$(PATH="$NO_DOCKER_PATH" IMAGE_NAME=app bash "$V" --dry-run \
             --dockerfile Dockerfile.prod --context ./builds/app 2>/dev/null)"
      expect_has "--dockerfile honoured" "Dockerfile: Dockerfile.prod" "$out"
      expect_has "--context honoured"    "Context: ./builds/app"       "$out"
      
      # ── stream separation: banners on stderr, data on stdout ─────────────────────
      echo "-- stream separation --"
      err="$(PATH="$NO_DOCKER_PATH" IMAGE_NAME=x bash "$V" --dry-run 2>&1 1>/dev/null)"
      expect_has "banner on stderr" "===" "$err"
      dat="$(PATH="$NO_DOCKER_PATH" IMAGE_NAME=x bash "$V" --dry-run 2>/dev/null)"
      case "$dat" in
          *"==="*) no "stdout leaked a banner";;
          *)       ok "stdout carries only data lines";;
      esac
      
      # ── happy-path DATA preserved pre/post (only chatter/help moved) ─────────────
      echo "-- data preservation (pre/post) --"
      # Captured baseline = the pre-backfill happy-path stdout data lines. These must
      # stay byte-identical so downstream parsers are unaffected.
      BASELINE=$'Image: ghcr.io/acme/myapp:v1\nDockerfile: Dockerfile\nContext: .'
      new_data="$(PATH="$NO_DOCKER_PATH" IMAGE_NAME=myapp bash "$V" --dry-run \
                  --tag v1 --registry ghcr.io/acme 2>/dev/null \
                  | grep -E '^(Image|Dockerfile|Context): ')"
      [[ "$new_data" == "$BASELINE" ]] \
          && ok "data lines match captured baseline" \
          || { no "data lines match captured baseline"; printf '  got:\n%s\n' "$new_data" >&2; }
      
      # Live pre/post diff against the actual pre-backfill blob, when reachable.
      if OLD_SRC="$(git -C "$SKILL/../../.." cat-file -p "$OLD_BLOB" 2>/dev/null)"; then
          printf '%s' "$OLD_SRC" > "$SB/old-build-push.sh"
          old_data="$(cd "$SB" && PATH="$NO_DOCKER_PATH" DOCKER_SENTINEL_LOG="$SB/old-docker.log" \
                      IMAGE_NAME=myapp bash "$SB/old-build-push.sh" --tag v1 --registry ghcr.io/acme 2>/dev/null \
                      | grep -E '^(Image|Dockerfile|Context): ')"
          if [[ -n "$old_data" && "$old_data" == "$new_data" ]]; then
              ok "pre/post data identical (live diff vs old blob)"
          else
              no "pre/post data identical (live diff vs old blob)"
          fi
      else
          echo "  SKIP  live pre/post diff (old blob $OLD_BLOB not reachable)"
      fi
      
      echo ""
      echo "=== $PASS passed, $FAIL failed ==="
      [[ "$FAIL" -eq 0 ]] || exit 1
      exit 0
      
  • SKILL.md 5.3 KB
    ---
    name: container-orchestration
    description: "Docker, Kubernetes, and AWS ECS/Fargate patterns. Triggers on: Dockerfile, docker-compose, kubernetes, k8s, helm, pod, deployment, service, ingress, container, image, ecs, fargate, task definition, ecs service, awsvpc, FARGATE_SPOT, ALB, ecs vs kubernetes."
    when_to_use: "Use when running containers at scale — Dockerfile/compose authoring plus Kubernetes or ECS/Fargate orchestration — e.g. 'write a multi-stage Dockerfile', 'author a Helm chart or k8s Deployment+Service', 'set up an ECS Fargate task definition', 'ECS vs Kubernetes'. Reach for docker-ops for Docker-only build/compose work."
    license: MIT
    compatibility: "Docker 20+, Kubernetes 1.25+, Helm 3+"
    allowed-tools: "Read Write Bash"
    metadata:
      author: claude-mods
    ---
    
    # Container Orchestration
    
    > Facts verified as of 2026-07.
    
    Docker and Kubernetes patterns for containerized applications.
    
    ## Dockerfile Best Practices
    
    ```dockerfile
    # Use specific version, not :latest
    FROM python:3.11-slim AS builder
    
    # Set working directory
    WORKDIR /app
    
    # Copy dependency files first (better caching)
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    
    # Copy application code
    COPY src/ ./src/
    
    # Production stage (multi-stage build)
    FROM python:3.11-slim
    
    WORKDIR /app
    
    # Create non-root user
    RUN useradd --create-home appuser
    USER appuser
    
    # Copy from builder
    COPY --from=builder /app /app
    
    # Set environment
    ENV PYTHONUNBUFFERED=1
    
    # Health check
    HEALTHCHECK --interval=30s --timeout=3s \
      CMD curl -f http://localhost:8000/health || exit 1
    
    EXPOSE 8000
    CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0"]
    ```
    
    ### Dockerfile Rules
    ```
    DO:
    - Use specific base image versions
    - Use multi-stage builds
    - Run as non-root user
    - Order commands by change frequency
    - Use .dockerignore
    - Add health checks
    
    DON'T:
    - Use :latest tag
    - Run as root
    - Copy unnecessary files
    - Store secrets in image
    - Install dev dependencies in production
    ```
    
    ## Docker Compose
    
    ```yaml
    # docker-compose.yml
    version: "3.9"
    
    services:
      app:
        build:
          context: .
          dockerfile: Dockerfile
        ports:
          - "8000:8000"
        environment:
          - DATABASE_URL=postgres://user:pass@db:5432/app
        depends_on:
          db:
            condition: service_healthy
        healthcheck:
          test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
          interval: 30s
          timeout: 10s
          retries: 3
    
      db:
        image: postgres:15-alpine
        volumes:
          - postgres_data:/var/lib/postgresql/data
        environment:
          POSTGRES_USER: user
          POSTGRES_PASSWORD: pass
          POSTGRES_DB: app
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U user -d app"]
          interval: 10s
          timeout: 5s
          retries: 5
    
    volumes:
      postgres_data:
    ```
    
    ## Kubernetes Basics
    
    ### Deployment
    
    ```yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: app
      labels:
        app: myapp
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: myapp
      template:
        metadata:
          labels:
            app: myapp
        spec:
          containers:
          - name: app
            image: myapp:1.0.0
            ports:
            - containerPort: 8000
            resources:
              requests:
                memory: "128Mi"
                cpu: "100m"
              limits:
                memory: "256Mi"
                cpu: "500m"
            livenessProbe:
              httpGet:
                path: /health
                port: 8000
              initialDelaySeconds: 10
              periodSeconds: 30
            readinessProbe:
              httpGet:
                path: /ready
                port: 8000
              initialDelaySeconds: 5
              periodSeconds: 10
            env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: database-url
    ```
    
    ### Service
    
    ```yaml
    apiVersion: v1
    kind: Service
    metadata:
      name: app-service
    spec:
      selector:
        app: myapp
      ports:
      - port: 80
        targetPort: 8000
      type: ClusterIP
    ```
    
    ### Ingress
    
    ```yaml
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: app-ingress
      annotations:
        nginx.ingress.kubernetes.io/rewrite-target: /
    spec:
      ingressClassName: nginx
      rules:
      - host: app.example.com
        http:
          paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: app-service
                port:
                  number: 80
    ```
    
    ## kubectl Quick Reference
    
    | Command | Description |
    |---------|-------------|
    | `kubectl get pods` | List pods |
    | `kubectl logs <pod>` | View logs |
    | `kubectl exec -it <pod> -- sh` | Shell into pod |
    | `kubectl apply -f manifest.yaml` | Apply config |
    | `kubectl rollout restart deployment/app` | Restart deployment |
    | `kubectl rollout status deployment/app` | Check rollout |
    | `kubectl describe pod <pod>` | Debug pod |
    | `kubectl port-forward svc/app 8080:80` | Local port forward |
    
    ## Additional Resources
    
    - `./references/dockerfile-patterns.md` - Advanced Dockerfile techniques
    - `./references/k8s-manifests.md` - Full Kubernetes manifest examples
    - `./references/helm-patterns.md` - Helm chart structure and values
    - `./references/ecs-fargate.md` - Amazon ECS on AWS Fargate (task definitions, services, awsvpc networking, IAM roles, secrets, scaling, ALB/NLB, ECS vs Kubernetes)
    
    ## Scripts
    
    - `./scripts/build-push.sh` - Build and push Docker image
    
    ## Assets
    
    - `./assets/Dockerfile.template` - Production Dockerfile template
    - `./assets/docker-compose.template.yml` - Compose starter template
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related