skills/07-deploy-docker/SKILL.md
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".
npx skillsauth add felvieira/claude-skills-fv deploy-dockerInstall 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.
O Deployer é o último passo. Recebe código aprovado pelo security review e coloca em produção.
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.
policies/tool-safety.mdPara exemplos completos de Dockerfile, compose e pipelines, consultar docs/skill-guides/deploy-docker.md.
# 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.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/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;
}
}
# .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
# .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>
#!/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
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.
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.
#!/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
# 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
# docker-compose.yml — ler tag de variável
services:
backend:
image: ${IMAGE_TAG:-ghcr.io/org/app:latest}
# 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
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.
#!/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:-"[email protected]"}
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."
Opção 1 — rodar antes de subir nginx (recomendado para primeiro deploy):
# No script de deploy ou no docker-compose entrypoint do serviço nginx
entrypoint: >
/bin/sh -c "
/scripts/ssl-init.sh seudominio.com [email protected] &&
nginx -g 'daemon off;'
"
Opção 2 — nginx sobe com config HTTP-only primeiro, ssl-init promove para HTTPS:
# 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 [email protected] &&
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:
# /etc/cron.d/ssl-renew
0 3 * * * root /app/scripts/ssl-init.sh seudominio.com [email protected] >> /var/log/ssl-renew.log 2>&1
export function isFeatureEnabled(feature: string): boolean {
const flags = JSON.parse(process.env.FEATURE_FLAGS || '{}');
return flags[feature] === true;
}
☐ 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 deve priorizar clareza. Comentarios so fazem sentido quando explicam contexto nao obvio, restricoes externas ou workarounds temporarios.
Seguir policies/handoffs.md e, quando util, templates/handoff.md.
testing
Skill do Product Owner para especificação de features. Use quando precisar definir requisitos de negócio, escrever user stories, critérios de aceitação, priorização de backlog, ou qualquer documento de especificação de produto. Inclui fundamento de negócio para discovery: validação de hipótese, problema vs. necessidade, MVP, modelo de monetização e métricas pirata (AARRR) como input da spec. Trigger em: "nova feature", "especificação", "user story", "requisito", "backlog", "PO", "definir escopo", "critério de aceitação", "MVP", "roadmap", "validação de hipótese", "discovery", "monetização", "pricing", "product-market fit", "métricas AARRR".
development
Skill compositora que pega texto/assunto e gera post de blog HTML completo no repo {blog_repo_path} ({github_user_repo_url}), com imagens (via skill 17 fal.ai ou skill 42 Playwright screenshot), commit+push automático, retorna URL pública via GitHub Pages. Trigger em: "post no blog", "publicar post", "escrever post", "blog post", "publish blog", "gera post", "criar post", "novo post no meu blog".
tools
Audita o peso de contexto carregado na sessão — CLAUDE.md, agents, MCP descriptions, rules ativas, skills invocadas e histórico acumulado. Estima tokens por componente, reporta headroom disponível e emite alertas de overflow. Distinto do cost-tracker (skill 30) que rastreia tokens gastos em completions runtime. Trigger em: "contexto inchado", "context overflow", "quanto contexto estou usando", "peso do contexto", "context budget", "tokens carregados", "sessao lenta", "respostas degradadas", "headroom de contexto", "custo fixo de contexto", "overhead de rules", "overhead dos agents", "impacto do MCP no contexto", "espaco no context window", "quanto cabe no context window"
development
Coleta e organiza informacao tecnica multi-fonte antes de escrever docs, PRDs, ADRs ou artigos. Busca em: docs oficiais, GitHub (repos + issues), Stack Overflow, papers e blogs de referencia. Ranqueia fontes por autoridade (oficial 40% + recencia 30% + profundidade 20% + comunidade 10%). Output: memory/research/<slug>.md pronto para alimentar skill 10 (documenter), skill 01 (po-feature-spec), skill 26 (prompt-engineer) ou skill 41 (blog-publisher). Trigger em: "pesquisa tecnica", "levanta informacao", "coleta docs", "busca referencias", "preciso de fontes", "research antes de escrever", "levanta o que existe sobre", "benchmark de solucoes", "o que existe sobre X", "quero entender o estado da arte", "compara abordagens", "levanta referencias", "faz um research de", "coleta fontes sobre", "pesquisa sobre", "quero saber o que existe de", "monta um dossie tecnico", "background tecnico", "due diligence tecnica", "levantamento de alternativas".