ci: replace portainer webhook with ssh deploy (portainer free)
This commit is contained in:
parent
dcd18a07e6
commit
b0eb12c17d
2 changed files with 113 additions and 68 deletions
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue