GitOps na VPS: deploy automático ao push

    GitOps é a prática de usar o Git como fonte única de verdade para o estado da infraestrutura — qualquer mudança na aplicação é feita via commit, e a infraestrutura se sincroniza automaticamente. Na VPS, a implementação mais simples usa Watchtower (atualização automática de imagens Docker) ou um webhook que dispara o deploy no servidor sem que o CI precise de acesso SSH.

    Watchtower: atualização automática de containers

    Watchtower monitora o registry e atualiza containers automaticamente quando uma nova imagem é publicada:

    yaml
    # docker-compose.yml — adicionar Watchtower
    services:
      app:
        image: ghcr.io/usuario/minha-api:main
        restart: always
        labels:
          - "com.centurylinklabs.watchtower.enable=true"   # monitorar este container
    
      watchtower:
        image: containrrr/watchtower
        restart: always
        volumes:
          - /var/run/docker.sock:/var/run/docker.sock
          - ~/.docker/config.json:/config.json:ro    # credenciais do registry
        environment:
          WATCHTOWER_POLL_INTERVAL: 60          # verificar a cada 60 segundos
          WATCHTOWER_CLEANUP: "true"            # remover imagens antigas
          WATCHTOWER_LABEL_ENABLE: "true"       # monitorar apenas containers com label
          WATCHTOWER_NOTIFICATIONS: "slack"
          WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL: ${SLACK_WEBHOOK}
          WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER: "watchtower-vps-prod"
    Dica
    Watchtower com WATCHTOWER_LABEL_ENABLE: true só atualiza containers com a label com.centurylinklabs.watchtower.enable=true — evita atualizar containers de infraestrutura (banco, Nginx) que devem ser versionados manualmente.

    Webhook de deploy: CI notifica a VPS

    Alternativa ao Watchtower: a VPS expõe um endpoint que o GitHub Actions chama após publicar a imagem:

    python
    # Na VPS — servidor de webhook simples com Python
    # /opt/webhook/webhook.py
    
    from http.server import HTTPServer, BaseHTTPRequestHandler
    import subprocess
    import hmac
    import hashlib
    import os
    
    SECRET = os.environ["WEBHOOK_SECRET"].encode()
    
    class WebhookHandler(BaseHTTPRequestHandler):
        def do_POST(self):
            if self.path != "/deploy":
                self.send_response(404)
                self.end_headers()
                return
    
            # Verificar assinatura HMAC
            content_length = int(self.headers.get("Content-Length", 0))
            body = self.rfile.read(content_length)
            signature = self.headers.get("X-Hub-Signature-256", "")
            expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
    
            if not hmac.compare_digest(signature, expected):
                self.send_response(403)
                self.end_headers()
                return
    
            # Executar deploy
            subprocess.Popen(["/opt/scripts/deploy.sh"])
            self.send_response(200)
            self.end_headers()
            self.wfile.write(b"Deploy iniciado")
    
        def log_message(self, *args):
            pass  # silenciar logs padrão
    
    if __name__ == "__main__":
        server = HTTPServer(("127.0.0.1", 9000), WebhookHandler)
        server.serve_forever()

    Chamar webhook a partir do GitHub Actions

    Após o build e push, o workflow notifica a VPS para puxar a nova imagem:

    yaml
    # .github/workflows/deploy.yml
          - name: Notificar VPS para deploy
            run: |
              PAYLOAD='{"ref":"main","sha":"${{ github.sha }}"}'
              SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "${{ secrets.WEBHOOK_SECRET }}" | awk '{print "sha256="$2}')
    
              curl -f -X POST https://seudominio.com.br/webhook/deploy \
                -H "Content-Type: application/json" \
                -H "X-Hub-Signature-256: $SIGNATURE" \
                -d "$PAYLOAD"
    
    # O Nginx roteia /webhook/ para localhost:9000 (webhook server)
    # nginx.conf:
    # location /webhook/ {
    #     allow all;
    #     proxy_pass http://127.0.0.1:9000;
    # }

    deploy.sh chamado pelo webhook

    Script de deploy na VPS que puxa a nova imagem e reinicia o container:

    bash
    #!/bin/bash
    # /opt/scripts/deploy.sh
    set -euo pipefail
    LOG="/var/log/deploy.log"
    exec >> "$LOG" 2>&1
    
    echo "=== Deploy: $(date) ==="
    
    # Autenticar no registry (se imagem privada)
    echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$GITHUB_USER" --password-stdin 2>/dev/null
    
    cd /opt/minha-api
    
    # Puxar nova imagem
    docker compose pull app
    
    # Reiniciar apenas o container app
    docker compose up -d --no-deps app
    
    # Health check
    sleep 5
    if curl -sf http://localhost:3000/health > /dev/null; then
        echo "Deploy bem-sucedido"
    else
        echo "Health check falhou — rollback"
        docker compose up -d --no-deps --scale app=0 app
        exit 1
    fi

    Comparação: Push vs Pull GitOps

    Duas abordagens de GitOps com tradeoffs distintos:

    bash
    # Push GitOps (mais comum em VPS):
    # CI/CD faz SSH na VPS e executa o deploy diretamente
    # + Simples de implementar
    # + Feedback imediato no pipeline
    # - CI precisa de credenciais SSH da VPS
    # - Gargalo de segurança se CI for comprometido
    
    # Pull GitOps (padrão em Kubernetes com ArgoCD/Flux):
    # A VPS monitora o repositório/registry e atualiza sozinha
    # + Sem credenciais de deploy no CI
    # + VPS nunca expõe acesso de entrada para deploy
    # - Lag de alguns segundos/minutos até sincronizar
    # - Mais complexo de debugar
    
    # Para VPS simples: Push GitOps (GitHub Actions via SSH)
    # Para múltiplas VPS ou containers: Pull GitOps (Watchtower)
    # Para Kubernetes: ArgoCD ou Flux (Pull GitOps nativo)

    $ runstack deploy --plan starter

    Não quer configurar manualmente?

    Não quer configurar manualmente? Implante o VPS em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.

    Perguntas frequentes