From b0eb12c17d2ea7aca608a831d56d61953d601864 Mon Sep 17 00:00:00 2001 From: MatheusAlves96 Date: Tue, 21 Apr 2026 00:09:30 -0300 Subject: [PATCH] ci: replace portainer webhook with ssh deploy (portainer free) --- .forgejo/workflows/README.md | 49 ++++++++----- .forgejo/workflows/deploy.yml | 132 +++++++++++++++++++++------------- 2 files changed, 113 insertions(+), 68 deletions(-) diff --git a/.forgejo/workflows/README.md b/.forgejo/workflows/README.md index d48e3b1..6f0167f 100644 --- a/.forgejo/workflows/README.md +++ b/.forgejo/workflows/README.md @@ -3,50 +3,56 @@ ## Fluxo ``` -push main → Build images → Push registry → Deploy Portainer → Health checks HTTPS +push main → Build images → Push registry → SSH no servidor → docker compose up → Health checks HTTPS ``` ## Configurar no Forgejo (Settings → Secrets & Variables) -### Secrets (valores sensíveis) +### Secrets | Secret | Descrição | |--------|-----------| | `REGISTRY_USER` | Usuário do registry (ex: `gitadmin`) | | `REGISTRY_PASSWORD` | Senha ou token do registry | -| `PORTAINER_WEBHOOK_URL` | URL do webhook do stack no Portainer | +| `SSH_PRIVATE_KEY` | Chave privada SSH para acessar o servidor | | `POSTGRES_DB` | Nome do banco de dados | | `POSTGRES_USER` | Usuário do PostgreSQL | | `POSTGRES_PASSWORD` | Senha do PostgreSQL | | `SECRET_KEY` | Flask SECRET_KEY | | `JWT_SECRET_KEY` | Chave JWT (mín. 32 chars) | -### Variables (valores não-sensíveis) +### Variables | Variable | Exemplo | |----------|---------| | `REGISTRY` | `git.matheussouza.com.br/gitadmin` | | `DOMAIN` | `imobiliaria.matheussouza.com.br` | +| `SSH_HOST` | IP ou hostname do servidor | +| `SSH_USER` | Usuário SSH (ex: `root` ou `deploy`) | +| `SSH_PORT` | Porta SSH (padrão: `22`) | -## Configurar no Portainer +## Gerar chave SSH para o deploy -1. Crie um **Stack** com o arquivo `docker-compose.prod.yml` -2. Ative o **webhook** do stack (Stack → Webhooks → Enable) -3. Copie a URL do webhook → cole em `PORTAINER_WEBHOOK_URL` -4. Certifique-se que a rede `traefik-public` existe: - ``` +No seu computador: +```bash +ssh-keygen -t ed25519 -C "forgejo-deploy" -f ~/.ssh/forgejo_deploy -N "" +``` + +- Conteúdo de `~/.ssh/forgejo_deploy` → cole em `SSH_PRIVATE_KEY` (secret) +- Conteúdo de `~/.ssh/forgejo_deploy.pub` → adicione em `~/.ssh/authorized_keys` no servidor + +## Pré-requisitos no servidor + +1. Docker + Docker Compose instalados +2. Rede Traefik criada: + ```bash docker network create traefik-public ``` +3. Traefik rodando com entrypoints `web` (80), `websecure` (443) e certresolver `letsencrypt` +4. Usuário SSH com permissão para rodar `docker` -## Traefik — pré-requisitos +## Traefik — configuração mínima -O Traefik deve estar rodando com: -- Entrypoint `web` (porta 80) -- Entrypoint `websecure` (porta 443) -- CertResolver `letsencrypt` configurado -- Conectado à rede `traefik-public` - -Exemplo mínimo de configuração do Traefik: ```yaml -# traefik.yml +# /opt/traefik/traefik.yml entryPoints: web: address: ":80" @@ -60,4 +66,9 @@ certificatesResolvers: storage: /letsencrypt/acme.json httpChallenge: entryPoint: web + +providers: + docker: + exposedByDefault: false + network: traefik-public ``` diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index d358c12..150e769 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: CI/CD → Portainer +name: CI/CD → Deploy via SSH on: push: @@ -6,7 +6,7 @@ on: - main env: - REGISTRY: ${{ vars.REGISTRY }} # ex: git.matheussouza.com.br/gitadmin + REGISTRY: ${{ vars.REGISTRY }} BACKEND_IMAGE: saas-imobiliaria-backend FRONTEND_IMAGE: saas-imobiliaria-frontend @@ -36,7 +36,6 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - # ── Backend ────────────────────────────────────────────────────────────── - name: Build & push backend uses: docker/build-push-action@v5 with: @@ -49,7 +48,6 @@ jobs: cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:cache cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:cache,mode=max - # ── Frontend ───────────────────────────────────────────────────────────── - name: Build & push frontend uses: docker/build-push-action@v5 with: @@ -68,10 +66,10 @@ jobs: image_tag: ${{ steps.tag.outputs.IMAGE_TAG }} # ──────────────────────────────────────────────────────────────────────────── - # 2. DEPLOY VIA PORTAINER WEBHOOK + # 2. DEPLOY VIA SSH # ──────────────────────────────────────────────────────────────────────────── deploy: - name: Deploy to Portainer + name: Deploy via SSH runs-on: ubuntu-latest needs: build @@ -79,27 +77,66 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Trigger Portainer stack deploy + - name: Setup SSH key run: | - curl -s -o /dev/null -w "%{http_code}" \ - --fail \ - -X POST "${{ secrets.PORTAINER_WEBHOOK_URL }}" \ - -H "Content-Type: application/json" \ - -d '{ - "env": [ - {"name": "IMAGE_TAG", "value": "${{ needs.build.outputs.image_tag }}"}, - {"name": "REGISTRY", "value": "${{ vars.REGISTRY }}"}, - {"name": "DOMAIN", "value": "${{ vars.DOMAIN }}"}, - {"name": "POSTGRES_DB", "value": "${{ secrets.POSTGRES_DB }}"}, - {"name": "POSTGRES_USER", "value": "${{ secrets.POSTGRES_USER }}"}, - {"name": "POSTGRES_PASSWORD", "value": "${{ secrets.POSTGRES_PASSWORD }}"}, - {"name": "SECRET_KEY", "value": "${{ secrets.SECRET_KEY }}"}, - {"name": "JWT_SECRET_KEY", "value": "${{ secrets.JWT_SECRET_KEY }}"} - ] - }' + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -p ${{ vars.SSH_PORT || '22' }} ${{ vars.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null + + - name: Copy docker-compose.prod.yml to server + run: | + scp -i ~/.ssh/deploy_key \ + -P ${{ vars.SSH_PORT || '22' }} \ + docker-compose.prod.yml \ + ${{ vars.SSH_USER }}@${{ vars.SSH_HOST }}:/opt/saas-imobiliaria/docker-compose.prod.yml + + - name: Deploy on server + run: | + ssh -i ~/.ssh/deploy_key \ + -p ${{ vars.SSH_PORT || '22' }} \ + ${{ vars.SSH_USER }}@${{ vars.SSH_HOST }} \ + bash -s << 'ENDSSH' + + set -e + + DEPLOY_DIR="/opt/saas-imobiliaria" + mkdir -p "$DEPLOY_DIR" + cd "$DEPLOY_DIR" + + # Write .env for docker compose + cat > .env << EOF + IMAGE_TAG=${{ needs.build.outputs.image_tag }} + REGISTRY=${{ vars.REGISTRY }} + DOMAIN=${{ vars.DOMAIN }} + POSTGRES_DB=${{ secrets.POSTGRES_DB }} + POSTGRES_USER=${{ secrets.POSTGRES_USER }} + POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }} + SECRET_KEY=${{ secrets.SECRET_KEY }} + JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }} + EOF + + # Log in to registry on the server + echo "${{ secrets.REGISTRY_PASSWORD }}" | \ + docker login ${{ vars.REGISTRY }} \ + -u ${{ secrets.REGISTRY_USER }} \ + --password-stdin + + # Pull new images + docker compose -f docker-compose.prod.yml pull + + # Rolling restart (zero-downtime: db stays up) + docker compose -f docker-compose.prod.yml up -d --remove-orphans + + # Clean up old images + docker image prune -f + + echo "Deploy concluído: ${{ needs.build.outputs.image_tag }}" + + ENDSSH # ──────────────────────────────────────────────────────────────────────────── - # 3. HEALTH CHECK (valida HTTPS + endpoints críticos) + # 3. HEALTH CHECK — valida HTTPS + endpoints críticos # ──────────────────────────────────────────────────────────────────────────── healthcheck: name: Validate HTTPS & Endpoints @@ -113,48 +150,45 @@ jobs: - name: Check frontend HTTPS run: | STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ - --max-time 15 \ - "https://${{ vars.DOMAIN }}") - echo "Frontend status: $STATUS" - [ "$STATUS" = "200" ] || (echo "Frontend HTTPS failed ($STATUS)" && exit 1) + --max-time 15 "https://${{ vars.DOMAIN }}") + echo "Frontend: $STATUS" + [ "$STATUS" = "200" ] || (echo "❌ Frontend HTTPS falhou ($STATUS)" && exit 1) - name: Check HTTP → HTTPS redirect run: | LOCATION=$(curl -s -o /dev/null -w "%{redirect_url}" \ - --max-time 10 \ - "http://${{ vars.DOMAIN }}") - echo "Redirect to: $LOCATION" - echo "$LOCATION" | grep -q "https://" || (echo "HTTP redirect missing" && exit 1) + --max-time 10 "http://${{ vars.DOMAIN }}") + echo "Redirect: $LOCATION" + echo "$LOCATION" | grep -q "https://" || (echo "❌ Redirect HTTP→HTTPS ausente" && exit 1) - name: Check backend /health run: | STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ - --max-time 15 \ - "https://${{ vars.DOMAIN }}/api/health") - echo "Backend health status: $STATUS" - [ "$STATUS" = "200" ] || (echo "Backend health check failed ($STATUS)" && exit 1) + --max-time 15 "https://${{ vars.DOMAIN }}/api/health") + echo "Backend health: $STATUS" + [ "$STATUS" = "200" ] || (echo "❌ Backend health check falhou ($STATUS)" && exit 1) - name: Check TLS certificate validity run: | - EXPIRY=$(echo | openssl s_client -connect ${{ vars.DOMAIN }}:443 -servername ${{ vars.DOMAIN }} 2>/dev/null \ + EXPIRY=$(echo | openssl s_client \ + -connect ${{ vars.DOMAIN }}:443 \ + -servername ${{ vars.DOMAIN }} 2>/dev/null \ | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2) - echo "Certificate expires: $EXPIRY" - # Fail if cert expires in less than 7 days - EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$EXPIRY" +%s) + echo "Certificado expira: $EXPIRY" + EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s) NOW_EPOCH=$(date +%s) DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 )) - echo "Days until expiry: $DAYS_LEFT" - [ "$DAYS_LEFT" -gt 7 ] || (echo "Certificate expires in $DAYS_LEFT days!" && exit 1) + echo "Dias restantes: $DAYS_LEFT" + [ "$DAYS_LEFT" -gt 7 ] || (echo "❌ Certificado expira em $DAYS_LEFT dias!" && exit 1) - name: Check API properties endpoint run: | STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ - --max-time 15 \ - "https://${{ vars.DOMAIN }}/api/properties?limit=1") - echo "Properties API status: $STATUS" - [ "$STATUS" = "200" ] || (echo "Properties API failed ($STATUS)" && exit 1) + --max-time 15 "https://${{ vars.DOMAIN }}/api/properties?limit=1") + echo "Properties API: $STATUS" + [ "$STATUS" = "200" ] || (echo "❌ Properties API falhou ($STATUS)" && exit 1) - - name: Deployment validated successfully + - name: All checks passed run: | - echo "✅ All checks passed!" - echo " → https://${{ vars.DOMAIN }} is live and healthy" + echo "✅ Deploy validado com sucesso!" + echo " → https://${{ vars.DOMAIN }} está no ar"