From dev-team-kit-fv
Dockerizes applications, configures CI/CD pipelines, manages environments (dev/staging/prod), sets up nginx reverse proxy with SSL, and implements monitoring, health checks, and rollback strategies.
How this skill is triggered — by the user, by Claude, or both
Slash command
/dev-team-kit-fv:07-deploy-dockerThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
O Deployer é o último passo. Recebe código aprovado pelo security review e coloca em produção.
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.
Tratar infraestrutura como código: o estado de servidores, redes e recursos cloud vive em arquivos versionados, revisáveis e reproduzíveis — nunca em passos manuais ou conhecimento tribal. Os princípios abaixo são atemporais (destilados de "DevOps na prática", Casa do Código); o ferramental moderno (Terraform, OpenTofu, Ansible, Pulumi) substitui o ferramental datado da literatura original (Puppet, Vagrant, Chef).
ssl-init.sh — IaC é a mesma ideia no nível de servidor/infra inteira).ssh e mudou à mão). IaC detecta e corrige drift reaplicando o estado declarado. Mudança em produção que não passa pelo código é dívida — o próximo apply a reverte ou o plan a denuncia.| Conceito do livro | Ferramenta datada | Equivalente moderno |
|---|---|---|
| Provisionar VM / recurso cloud | Vagrant + VirtualBox | Terraform / OpenTofu / Pulumi |
| Configuration management (estado do SO/serviços) | Puppet, Chef | Ansible (agentless), cloud-init |
| Manifesto / recurso declarativo | .pp (Puppet) | .tf (HCL), playbook YAML |
| Reuso comunitário | Puppet Forge | Terraform Registry, Ansible Galaxy |
| Idempotência | core do Puppet | core de Terraform (plan/apply) e Ansible |
Regra de bolso atual: Terraform/OpenTofu provisiona (cria VM, rede, bucket, cluster); Ansible configura (instala pacote, ajusta arquivo, sobe serviço). Em ambiente totalmente container/Kubernetes, boa parte do config management migra pro Dockerfile + manifesto K8s + GitOps (Argo CD / Flux) — o princípio declarativo é o mesmo, o apply muda de dono.
Em resumo: IaC constrói o palco (07), a 24 troca o ator em cena, a 20 vigia o espetáculo. Drift de infra é da 07; degradação em runtime é da 20; deploy da versão errada é da 24.
terraform import)plan/--check pra detectar antes de reaplicarPara um único servidor descartável com docker-compose, IaC pesado é overkill — o compose já é declarativo no nível de containers. IaC entra quando a infra abaixo dos containers (VM, rede, DNS, certificado, cluster) também precisa ser versionada e recriável.
# 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.
npx claudepluginhub felvieira/claude-skills-fv --plugin dev-team-kit-fvProvides deployment strategies (rolling, blue-green, canary), multi-stage Dockerfiles for Node.js, health checks, rollback plans, and production checklists for web apps.
Guides deployment workflows including CI/CD pipeline patterns, Docker containerization, health checks, rollback strategies, and production readiness checklists for web applications.
Dockerizes applications, sets up CI/CD with GitHub Actions, and deploys to AWS (Lambda, ECS) using Terraform/SAM with health checks and rollback strategies.