deploy-docker · git:20260527.ada0900 · 2026-05-27 · sha256 261738a264a2a143

deploy-docker git:20260527.ada0900A

Immutable. This exact content is served forever at /api/v1/blob/261738a264a2a143.

---
name: deploy-docker
description: |
  Skill do DevOps/Deployer para dockerização, CI/CD, e deploy de aplicações. Use quando precisar criar
  Dockerfile, docker-compose, configurar pipeline CI/CD, deploy em cloud, configurar nginx, SSL,
  ou qualquer operação de infraestrutura. Trigger em: "deploy", "Docker", "Dockerfile", "docker-compose",
  "CI/CD", "pipeline CI/CD", "GitHub Actions", "nginx", "SSL", "produção", "staging", "Kubernetes", "AWS",
  "infraestrutura", "ambiente", "build", "release".
allowed-tools: Read, Edit, Write, Grep, Glob, Bash(docker *), Bash(docker-compose *), Bash(git *), Bash(npm run *), Bash(gh *)
---

# Deployer - Dockerização e Deploy

O Deployer é o último passo. Recebe código aprovado pelo security review e coloca em produção.

## Governanca Global

Esta skill segue `GLOBAL.md`, `policies/execution.md`, `policies/handoffs.md`, `policies/quality-gates.md`, `policies/token-efficiency.md`, `policies/stack-flexibility.md`, `policies/tool-safety.md` e `policies/evals.md`.

Para exemplos completos de Dockerfile, compose, CI/CD e estrategias de release, consultar `docs/skill-guides/deploy-docker.md` apenas quando necessario.

## Quando Usar

- empacotar, publicar ou promover mudanca entre ambientes
- configurar pipeline, health check, rollback ou release strategy

## Quando Nao Usar

- para validar codigo de negocio como atividade principal
- para executar acao externa de alto risco sem aprovacao

## Entradas Esperadas

- artefatos aprovados por QA, Security e Reviewer
- requisitos de ambiente, health check e rollback
- restricoes operacionais e de observabilidade

## Saidas Esperadas

- estrategia de deploy clara
- configuracao de build/release coerente
- handoff operacional curto para execucao ou monitoramento

## Responsabilidades

1. Dockerizar a aplicação (front + back + banco)
2. Configurar CI/CD pipeline
3. Gerenciar ambientes (dev, staging, prod)
4. Configurar reverse proxy (nginx/Traefik)
5. SSL/TLS com certbot
6. Monitoramento e logs
7. Rollback strategy

## Estrategia Base de Deploy

- builds reproduziveis e multi-stage quando fizer sentido
- imagens minimas e sem secrets embutidos
- health checks, rollback e monitoramento antes de promover release
- aprovacao explicita para acoes em producao seguindo `policies/tool-safety.md`

Para exemplos completos de Dockerfile, compose e pipelines, consultar `docs/skill-guides/deploy-docker.md`.

## Dockerfile - Backend (Node.js/Express)

```dockerfile
# Dockerfile.backend

FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production

RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 appuser

COPY --from=builder --chown=appuser:nodejs /app/dist ./dist
COPY --from=builder --chown=appuser:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:nodejs /app/prisma ./prisma
COPY --from=builder /app/package.json ./

USER appuser

EXPOSE 3001

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

CMD ["sh", "-c", "npx prisma migrate deploy && node dist/server.js"]
```

## Docker Compose - Ambiente Completo

```yaml
# docker-compose.yml
version: '3.8'

services:
  # ── Frontend ──
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.frontend
      args:
        NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
        NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL}
    restart: unless-stopped
    ports:
      - "3000:3000"
    depends_on:
      backend:
        condition: service_healthy
    networks:
      - app-network
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'

  # ── Backend ──
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile.backend
    restart: unless-stopped
    ports:
      - "3001:3001"
    env_file:
      - ./backend/.env
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - app-network
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'

  # ── PostgreSQL ──
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network
    deploy:
      resources:
        limits:
          memory: 256M

  # ── Redis ──
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  # ── Nginx Reverse Proxy ──
  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - certbot_data:/etc/letsencrypt:ro
      - certbot_www:/var/www/certbot:ro
    depends_on:
      - frontend
      - backend
    networks:
      - app-network

  # ── Certbot SSL ──
  certbot:
    image: certbot/certbot
    volumes:
      - certbot_data:/etc/letsencrypt
      - certbot_www:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

volumes:
  postgres_data:
  redis_data:
  certbot_data:
  certbot_www:

networks:
  app-network:
    driver: bridge
```

## Nginx Configuration

```nginx
# nginx/conf.d/app.conf

upstream frontend {
    server frontend:3000;
}

upstream backend {
    server backend:3001;
}

# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

server {
    listen 80;
    server_name seudominio.com;
    
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
    
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name seudominio.com;
    
    ssl_certificate /etc/letsencrypt/live/seudominio.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/seudominio.com/privkey.pem;
    
    # SSL hardening
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    
    # Security headers
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    
    # Gzip
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml;
    gzip_min_length 1000;
    
    # Frontend
    location / {
        proxy_pass http://frontend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
    
    # Backend API
    location /api/ {
        limit_req zone=api burst=20 nodelay;
        
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    
    # Login rate limit mais agressivo
    location /api/v1/auth/login {
        limit_req zone=login burst=3 nodelay;
        
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
    
    # Bloqueia acesso direto ao Prisma Studio, etc
    location ~ /_(next|prisma) {
        deny all;
    }
}
```

## CI/CD - GitHub Actions

```yaml
# .github/workflows/deploy.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main, staging]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # ── Lint & Type Check ──
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm run type-check

  # ── Unit Tests ──
  test-unit:
    needs: quality
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run test:coverage
      - uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/

  # ── E2E Tests ──
  test-e2e:
    needs: quality
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm run test:e2e
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/

  # ── Security Audit ──
  security:
    needs: quality
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm audit --audit-level=high
      - uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          severity: 'CRITICAL,HIGH'

  # ── Build & Push Docker ──
  build:
    needs: [test-unit, test-e2e, security]
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

  # ── Deploy ──
  deploy-staging:
    needs: build
    if: github.ref == 'refs/heads/staging'
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - name: Deploy to staging
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.STAGING_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          script: |
            cd /app
            docker compose pull
            docker compose up -d --force-recreate
            docker compose exec backend npx prisma migrate deploy
            docker system prune -f

  deploy-production:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy to production
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          script: |
            cd /app
            # Backup banco antes de deploy
            docker compose exec postgres pg_dump -U $DB_USER $DB_NAME > backup_$(date +%Y%m%d_%H%M).sql
            docker compose pull
            docker compose up -d --force-recreate
            docker compose exec backend npx prisma migrate deploy
            # Health check
            sleep 10
            curl -f http://localhost:3000/api/health || (docker compose logs --tail=50 && exit 1)
            docker system prune -f
```

## Environment Variables

```bash
# .env.example — NUNCA commitar .env real
# === Backend ===
NODE_ENV=production
PORT=3001
DATABASE_URL=postgresql://user:pass@postgres:5432/dbname
REDIS_URL=redis://:pass@redis:6379

# Auth
JWT_ACCESS_SECRET=<gerar-com-openssl-rand-base64-64>
JWT_REFRESH_SECRET=<gerar-com-openssl-rand-base64-64>
JWT_ACCESS_EXPIRY=15m
JWT_REFRESH_EXPIRY=7d

# CORS
ALLOWED_ORIGINS=https://seudominio.com

# === Frontend ===
NEXT_PUBLIC_API_URL=https://seudominio.com/api/v1
NEXT_PUBLIC_APP_URL=https://seudominio.com

# === Infra ===
DB_NAME=appdb
DB_USER=appuser
DB_PASSWORD=<gerar-senha-forte>
REDIS_PASSWORD=<gerar-senha-forte>
```

## Rollback Strategy

```bash
#!/bin/bash
# scripts/rollback.sh

PREVIOUS_TAG=$1

if [ -z "$PREVIOUS_TAG" ]; then
  echo "Uso: ./rollback.sh <tag-anterior>"
  echo "Tags disponíveis:"
  docker images --format "{{.Tag}}" ghcr.io/org/app | head -10
  exit 1
fi

echo "🔙 Rollback para $PREVIOUS_TAG..."

# Atualiza docker-compose pra usar tag anterior
export IMAGE_TAG=$PREVIOUS_TAG
docker compose up -d --force-recreate

# Verifica saúde
sleep 10
if curl -sf http://localhost:3000/api/health > /dev/null; then
  echo "✅ Rollback sucesso!"
else
  echo "❌ Rollback falhou — verificar logs"
  docker compose logs --tail=50
  exit 1
fi
```

## Rollback Persistente — .last-tag pattern

### Problema
O script de rollback acima exige que o operador saiba manualmente qual tag usar (`./rollback.sh <tag-anterior>`). Em pipelines CI/CD com múltiplos deploys por dia, a tag anterior não é conhecida em tempo de execução sem consultar registries externos — o que aumenta MTTR e risco de erro humano.

### Solução: persistir tag em arquivo .last-tag

Antes de promover a nova tag, salve a tag atual em `.last-tag` no servidor. Em caso de rollback, leia o arquivo em vez de hardcodar.

```bash
#!/bin/bash
# scripts/deploy-with-tag-persist.sh
# Uso: ./deploy-with-tag-persist.sh ghcr.io/org/app:sha-abc123

REGISTRY=ghcr.io/org/app
LAST_TAG_FILE=/app/.last-tag
NEW_TAG=$1

if [ -z "$NEW_TAG" ]; then
  echo "Uso: $0 <nova-tag>"
  exit 1
fi

# Persiste tag atual como "last" antes de promover
if [ -f "$LAST_TAG_FILE" ]; then
  CURRENT_TAG=$(cat "$LAST_TAG_FILE")
  echo "Tag atual: $CURRENT_TAG → será salva como last"
fi
echo "$NEW_TAG" > "$LAST_TAG_FILE"

# Promove nova tag
export IMAGE_TAG=$NEW_TAG
docker compose up -d --force-recreate

sleep 10
if curl -sf http://localhost:3000/api/health > /dev/null; then
  echo "Deploy OK: $NEW_TAG"
else
  echo "Deploy falhou — iniciando rollback automático para $CURRENT_TAG"
  export IMAGE_TAG=$CURRENT_TAG
  echo "$CURRENT_TAG" > "$LAST_TAG_FILE"
  docker compose up -d --force-recreate
  exit 1
fi

# Rollback manual independente:
# docker pull $(cat /app/.last-tag)
# IMAGE_TAG=$(cat /app/.last-tag) docker compose up -d --force-recreate
```

### Pattern em GitHub Actions

```yaml
# No job deploy-production — persistir tag via SSH após health check
- name: Persist last-tag e deploy
  uses: appleboy/ssh-action@v1
  with:
    host: ${{ secrets.PROD_HOST }}
    username: ${{ secrets.DEPLOY_USER }}
    key: ${{ secrets.DEPLOY_KEY }}
    script: |
      cd /app
      NEW_TAG="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"
      LAST_TAG_FILE=".last-tag"

      # Guarda tag anterior antes de promover
      [ -f "$LAST_TAG_FILE" ] && cp "$LAST_TAG_FILE" ".prev-tag"

      echo "$NEW_TAG" > "$LAST_TAG_FILE"
      IMAGE_TAG=$NEW_TAG docker compose up -d --force-recreate
      sleep 10
      curl -f http://localhost:3000/api/health || {
        echo "Health check falhou — rollback para $(cat .prev-tag)"
        IMAGE_TAG=$(cat .prev-tag) docker compose up -d --force-recreate
        cp .prev-tag "$LAST_TAG_FILE"
        exit 1
      }
      docker system prune -f
```

### Pattern em docker-compose com variável de ambiente

```yaml
# docker-compose.yml — ler tag de variável
services:
  backend:
    image: ${IMAGE_TAG:-ghcr.io/org/app:latest}
```

```bash
# rollback.sh sem argumento — usa .last-tag automaticamente
#!/bin/bash
ROLLBACK_TAG=$(cat /app/.last-tag 2>/dev/null)
if [ -z "$ROLLBACK_TAG" ]; then
  echo "Arquivo .last-tag não encontrado. Rollback manual necessário."
  exit 1
fi
echo "Rollback para: $ROLLBACK_TAG"
IMAGE_TAG=$ROLLBACK_TAG docker compose up -d --force-recreate
```

## ssl-init.sh — Idempotent SSL Setup

### Problema
O bloco SSL no `nginx/conf.d/app.conf` referencia `fullchain.pem` e `privkey.pem` que só existem após `certbot certonly` ter rodado com sucesso. Se o nginx subir antes do certbot, ele falha e o compose inteiro fica unhealthy — especialmente em primeiro deploy ou após troca de servidor.

### Solução: script ssl-init.sh idempotente

```bash
#!/bin/bash
# scripts/ssl-init.sh
# Detecta se certificado existe e cria apenas se necessário.
# Idempotente: rodar múltiplas vezes é seguro.

set -euo pipefail

DOMAIN=${1:-"seudominio.com"}
EMAIL=${2:-"admin@seudominio.com"}
CERT_PATH="/etc/letsencrypt/live/$DOMAIN/fullchain.pem"

echo "[ssl-init] Verificando certificado para $DOMAIN..."

if [ -f "$CERT_PATH" ]; then
  # Verifica validade — renova se expira em menos de 30 dias
  EXPIRY=$(openssl x509 -enddate -noout -in "$CERT_PATH" | cut -d= -f2)
  EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null || date -jf "%b %d %H:%M:%S %Y %Z" "$EXPIRY" +%s)
  NOW_EPOCH=$(date +%s)
  DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))

  if [ "$DAYS_LEFT" -gt 30 ]; then
    echo "[ssl-init] Certificado válido por mais $DAYS_LEFT dias. Nenhuma ação."
    exit 0
  fi
  echo "[ssl-init] Certificado expira em $DAYS_LEFT dias — renovando..."
fi

echo "[ssl-init] Obtendo certificado via certbot..."
certbot certonly \
  --webroot \
  --webroot-path /var/www/certbot \
  --email "$EMAIL" \
  --agree-tos \
  --no-eff-email \
  --non-interactive \
  -d "$DOMAIN" \
  -d "www.$DOMAIN" \
  2>&1 | tee -a /var/log/ssl-init.log

echo "[ssl-init] Recarregando nginx..."
docker compose exec nginx nginx -s reload 2>/dev/null || nginx -s reload

echo "[ssl-init] Certificado configurado com sucesso."
```

### Como integrar no entrypoint do container

**Opção 1 — rodar antes de subir nginx (recomendado para primeiro deploy):**

```bash
# No script de deploy ou no docker-compose entrypoint do serviço nginx
entrypoint: >
  /bin/sh -c "
    /scripts/ssl-init.sh seudominio.com admin@seudominio.com &&
    nginx -g 'daemon off;'
  "
```

**Opção 2 — nginx sobe com config HTTP-only primeiro, ssl-init promove para HTTPS:**

```yaml
# docker-compose.yml — nginx com dois configs
services:
  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx/conf.d/http-only.conf:/etc/nginx/conf.d/default.conf:ro  # inicial
      - ./nginx/conf.d/app.conf:/etc/nginx/conf.d/app.conf:ro            # após ssl-init
      - certbot_data:/etc/letsencrypt
      - certbot_www:/var/www/certbot
    command: >
      /bin/sh -c "
        nginx -g 'daemon off;' &
        sleep 5 &&
        /scripts/ssl-init.sh seudominio.com admin@seudominio.com &&
        cp /etc/nginx/conf.d/app.conf /etc/nginx/conf.d/default.conf &&
        nginx -s reload &&
        wait
      "
```

**Cron para renovação automática no servidor:**

```bash
# /etc/cron.d/ssl-renew
0 3 * * * root /app/scripts/ssl-init.sh seudominio.com admin@seudominio.com >> /var/log/ssl-renew.log 2>&1
```

### Blue-Green Deployment
- Manter dois ambientes identicos (blue e green)
- Deploy no ambiente inativo
- Health check no ambiente novo
- Switch de trafego (nginx upstream swap)
- Rollback instantaneo: reverter switch

### Canary Release
- Rotear 5-10% do trafego pro deploy novo
- Monitorar metricas (error rate, latency, CPU)
- Se ok apos 15min: aumentar pra 50% → 100%
- Se anomalia: rollback imediato pra 0%

### Feature Flags
```typescript
export function isFeatureEnabled(feature: string): boolean {
  const flags = JSON.parse(process.env.FEATURE_FLAGS || '{}');
  return flags[feature] === true;
}
```
- Usar pra deploy gradual de features arriscadas
- Desligar feature sem redeploy

## Checklist Pre-Deploy

```
☐ Todos os testes passando (unit + E2E)
☐ Security review aprovado
☐ npm audit sem HIGH/CRITICAL
☐ Docker build sem warnings
☐ .env configurado no servidor
☐ Migrations testadas em staging
☐ Backup do banco feito
☐ DNS apontando corretamente
☐ SSL certificado válido
☐ Rollback script testado
☐ Monitoring/alertas configurados
☐ README atualizado
```

## Codigo Limpo

Codigo deve priorizar clareza. Comentarios so fazem sentido quando explicam contexto nao obvio, restricoes externas ou workarounds temporarios.

## Integração com Pipeline

- **Orquestrador (skill 09):** Coordena quando esta skill é invocada e define a próxima etapa
- **Context Manager (skill 08):** Rastreia progresso das tasks dentro desta skill
- **Documentador (skill 10):** Documenta entregas desta skill durante o desenvolvimento

## Evidencia de Conclusao

- estrategia de deploy, health check e rollback definidos
- risco operacional e pre-condicoes destacados
- aprovacoes necessarias explicitadas para producao

## Handoff

Seguir `policies/handoffs.md` e, quando util, `templates/handoff.md`.