Alternar tema

Estratégias de deploy com GitHub Actions: de VPS a plataformas de nuvem

Easton editorial illustration: developer problem-solving desk

Introdução

Às três da manhã, eu observava a tela de logs do GitHub Actions enquanto linhas de erro em vermelho subiam sem parar. “Host key verification failed”. De novo, um problema com SSH.

Era a quinta falha de deploy. Tudo passava nos testes locais, mas quebrava depois do push para o GitHub. Naquele momento, fiquei com muita vontade de xingar — e também percebi uma coisa: escolher uma estratégia de deploy era bem mais complexo do que eu imaginava.

Seja em uma VPS administrada por você, seja em uma plataforma gerenciada como Vercel ou Cloudflare Pages, toda opção tem suas armadilhas. Uma escolha ruim só aumenta o número de madrugadas perdidas em diagnóstico.

Vamos comparar algumas estratégias de deploy com GitHub Actions para encontrar a mais adequada ao seu projeto.


Deploy em VPS via SSH: tradicional, mas confiável

Para ser sincero, no início eu resistia bastante ao deploy em VPS. Parecia trabalhoso demais: chaves SSH, known_hosts, parâmetros do rsync… havia muita coisa para configurar.

Depois de enfrentar alguns problemas, porém, percebi que essa abordagem “tradicional” era justamente a que oferecia mais controle.

Configuração da chave SSH: não coloque a chave no código

A dúvida mais comum é onde guardar a chave SSH.

Quem está começando muitas vezes coloca a chave privada diretamente no arquivo de workflow. É um erro grave. O lugar correto é o GitHub Secrets.

Abra Settings → Secrets → Actions no repositório e adicione SSH_PRIVATE_KEY. Depois, use o secret no workflow desta forma:

- name: Setup SSH
  uses: webfactory/[email protected]
  with:
    ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

Essa action inicia o ssh-agent e carrega sua chave automaticamente, sem complicação.

known_hosts: evite o erro “Host key verification failed”

Na primeira conexão SSH com um servidor, o cliente pergunta se você confia naquele host. Um ambiente de CI não consegue responder a essa pergunta interativamente, por isso é necessário adicionar antecipadamente a impressão digital do servidor ao known_hosts.

Há duas formas de fazer isso:

Opção 1: adicionar automaticamente com uma action

- name: Add server to known hosts
  uses: webfactory/[email protected]
  with:
    ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
    known-hosts: ${{ secrets.SSH_KNOWN_HOSTS }}

Você pode obter o conteúdo de SSH_KNOWN_HOSTS assim:

ssh-keyscan -H your-server.com >> known_hosts.txt
# Copie o conteúdo do arquivo para o GitHub Secrets

Opção 2: configurar manualmente

- name: Add server to known hosts
  run: |
    mkdir -p ~/.ssh
    ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts

A primeira opção deixa a configuração mais organizada; a segunda é útil para um diagnóstico rápido.

rsync ou scp?

Para transferir os arquivos do deploy, uso rsync. Os motivos são simples:

  • Transfere apenas os arquivos alterados, economizando tempo
  • Permite excluir diretórios específicos, como node_modules
  • Oferece sincronização incremental

Um comando rsync típico:

- name: Deploy to server
  run: |
    rsync -avz --delete \
      --exclude 'node_modules' \
      --exclude '.git' \
      ./dist/ ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}:/var/www/html/

O parâmetro --delete remove do diretório de destino os arquivos que não existem na origem. Use-o com cuidado: um caminho incorreto pode apagar arquivos que deveriam ser preservados.

Comando após o deploy: reiniciar o serviço

Para um site estático, basta concluir a transferência. Uma aplicação Node.js, por outro lado, ainda precisa ter o serviço reiniciado.

Gosto de usar o PM2 para gerenciar processos Node. Depois do deploy, executo:

- name: Restart application
  run: |
    ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} \
      "cd /var/www/app && pm2 restart all"

Ou, de forma mais segura, reinicio apenas uma aplicação específica:

- name: Restart application
  run: |
    ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} \
      "pm2 restart my-app --update-env"

O --update-env recarrega as variáveis de ambiente e é útil quando a configuração mudou.


Deploy em plataformas de nuvem: a conveniência dos serviços gerenciados

O problema do deploy em VPS é que você precisa administrar o servidor: patches de segurança, renovação de certificados SSL, regras de firewall… a lista de pequenas tarefas é longa.

Uma plataforma gerenciada elimina boa parte desse trabalho. Você envia o código, e ela faz o build e o deploy automaticamente. Assim, pode se concentrar no código.

Vercel: uma ótima escolha para projetos frontend

O suporte da Vercel a projetos frontend é quase perfeito. Next.js, Astro, React: deploy com um clique e praticamente nenhuma configuração.

Se o projeto precisa de uma API no backend, porém, é importante conhecer os limites. As Serverless Functions da Vercel têm um tempo máximo de execução: 10 segundos no plano gratuito e 60 segundos no Pro. Se a função ultrapassar esse limite, ocorre timeout.

Para sites totalmente estáticos ou APIs simples, a Vercel atende muito bem. Serviços de backend complexos ainda exigem outra infraestrutura.

Configuração do GitHub Actions para fazer deploy na Vercel:

name: Deploy to Vercel

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Vercel CLI
        run: npm i -g vercel@latest

      - name: Pull Vercel Environment Information
        run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}

      - name: Build Project Artifacts
        run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}

      - name: Deploy Project Artifacts to Vercel
        run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}

Gere o VERCEL_TOKEN no painel da Vercel e salve-o no GitHub Secrets.

Cloudflare Pages: uma franquia gratuita generosa

A franquia gratuita do Cloudflare Pages é bem mais generosa que a da Vercel. A largura de banda é ilimitada, e há 500 builds por mês — mais que suficiente para projetos pessoais.

Além disso, a CDN global da Cloudflare é realmente rápida. Nos meus próprios testes, a velocidade de acesso na Ásia foi mais estável que a da Vercel.

Configuração de deploy:

name: Deploy to Cloudflare Pages

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build
        run: npm run build

      - name: Deploy
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: my-project
          directory: dist

A Cloudflare tem outra vantagem: a franquia gratuita do armazenamento R2 também é ampla. Você pode colocar os recursos estáticos no R2 e combiná-lo com a CDN do Pages para reduzir bastante o tempo de carregamento.

Netlify: uma opção madura e estável

Uso menos a Netlify que as duas plataformas anteriores, mas ela é uma opção consolidada, com um ecossistema maduro.

A configuração de deploy é semelhante:

- name: Deploy to Netlify
  uses: netlify/actions/cli@master
  with:
    args: deploy --prod
  env:
    NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
    NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

O recurso de processamento de formulários da Netlify é bastante útil: os envios são tratados automaticamente, o que funciona bem em landing pages simples.

Limitações das plataformas gerenciadas

Ainda assim, plataformas gerenciadas não resolvem tudo.

Algumas limitações comuns:

  1. Ambiente de build limitado: memória e CPU têm limites, e projetos grandes podem falhar durante o build
  2. Pouca personalização: quer alterar a configuração do nginx? Não é possível
  3. Dependência da plataforma: se a empresa encerrar o serviço ou mudar suas políticas, será necessário migrar
  4. Problemas de acesso na China: algumas plataformas são instáveis no país, embora a Cloudflare tenha melhorado

Se o projeto exige controle total, a VPS continua sendo a melhor opção.


Estratégia híbrida: flexibilidade e controle no mesmo projeto

Muitos projetos não são “totalmente estáticos” nem “apenas backend”. O frontend pode usar Next.js enquanto o backend acessa um banco de dados e executa tarefas agendadas.

Nesse caso, um deploy híbrido pode ser a melhor solução.

Páginas estáticas em uma plataforma gerenciada e API em uma VPS

Uma arquitetura típica:

  • Páginas estáticas (HTML/CSS/JS) no Cloudflare Pages ou na Vercel
  • Serviço de API em Node.js na sua própria VPS
  • Banco de dados também na VPS, ou em um serviço gerenciado como Supabase ou PlanetScale

Essa combinação aproveita os pontos fortes de cada opção:

  • O frontend ganha aceleração por CDN e HTTPS automático
  • O backend fica sob seu controle total, sem as limitações da plataforma gerenciada
  • A latência de acesso ao banco de dados é baixa, pois a API e o banco estão na mesma máquina

Deploy em várias etapas com GitHub Actions

Um único workflow pode fazer o deploy nos dois ambientes:

name: Hybrid Deploy

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      artifact-path: ./dist
    steps:
      - uses: actions/checkout@v4
      - name: Install dependencies
        run: npm ci
      - name: Build
        run: npm run build
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist

  deploy-frontend:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist
      - name: Deploy to Cloudflare Pages
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: my-frontend
          directory: dist

  deploy-backend:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup SSH
        uses: webfactory/[email protected]
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
      - name: Deploy API to VPS
        run: |
          rsync -avz --delete \
            --exclude 'node_modules' \
            ./api/ ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}:/var/www/api/
      - name: Restart API service
        run: |
          ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} \
            "cd /var/www/api && npm install && pm2 restart api"

Esse workflow tem três jobs:

  1. build: compila o projeto e gera os arquivos estáticos
  2. deploy-frontend: envia os arquivos estáticos para o Cloudflare Pages
  3. deploy-backend: envia a API para a VPS e reinicia o serviço

O needs: build garante que os jobs de deploy só sejam executados depois da conclusão do build. upload-artifact e download-artifact transferem os artefatos entre os jobs.

Separação das variáveis de ambiente

Um dos desafios do deploy híbrido é que o frontend e o backend usam variáveis de ambiente diferentes.

O frontend precisa saber o endereço da API; o backend precisa conhecer a senha do banco de dados.

Minha solução é esta:

# Job do frontend
- name: Set frontend env
  run: |
    echo "API_URL=https://api.mydomain.com" >> $GITHUB_ENV

# Job do backend
- name: Deploy with env
  run: |
    ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} \
      "cd /var/www/api && pm2 restart api --update-env DATABASE_URL=${{ secrets.DATABASE_URL }}"

Informações sensíveis, como senha do banco de dados e token de API, devem sempre ficar no GitHub Secrets. Informações não confidenciais, como o endereço da API, podem ser escritas no workflow.


Exemplo prático de configuração

A seguir está um workflow completo de deploy em VPS que reúne todos os pontos anteriores.

Arquivo de workflow completo

name: Deploy to VPS

on:
  push:
    branches: [main]
  workflow_dispatch:  # Aciona o deploy manualmente

env:
  NODE_VERSION: '20'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist
          retention-days: 1

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download build artifact
        uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist

      - name: Setup SSH
        uses: webfactory/[email protected]
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Add server to known hosts
        run: |
          mkdir -p ~/.ssh
          ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts

      - name: Deploy files
        run: |
          rsync -avz --delete \
            --exclude '.htaccess' \
            ./dist/ ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:${{ secrets.DEPLOY_PATH }}

      - name: Verify deployment
        run: |
          ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} \
            "ls -la ${{ secrets.DEPLOY_PATH }}"

      - name: Send deployment notification
        if: always()
        run: |
          curl -X POST "${{ secrets.NOTIFICATION_WEBHOOK }}" \
            -H "Content-Type: application/json" \
            -d '{"text": "Deployment completed: ${GITHUB_SHA}"}'

Secrets que precisam ser configurados

Nome do secretDescriçãoComo obter
SSH_PRIVATE_KEYConteúdo da chave privada SSHGere localmente e coloque a chave pública no servidor
SERVER_HOSTIP ou domínio do servidorInformações da sua VPS
SERVER_USERNome do usuário SSHGeralmente root ou ubuntu
DEPLOY_PATHCaminho de destino do deployPor exemplo, /var/www/html
NOTIFICATION_WEBHOOKEndereço para notificações de deployWebhook do Slack ou Telegram

Solução de problemas comuns

Quando um deploy falha, é fácil se perder nos logs: há informações demais.

Eu verifico as etapas nesta ordem:

  1. Problemas de conexão SSH: confira as etapas “Setup SSH” e “Add server to known hosts”
    • Se houver falha, verifique o formato da chave e o conteúdo de known_hosts
  2. Problemas na transferência com rsync: confira a etapa “Deploy files”
    • Se houver falha, verifique se o caminho existe e se as permissões estão corretas
  3. Problemas ao reiniciar o serviço: confira a etapa “Verify deployment”
    • Se houver falha, verifique se há arquivos no caminho de destino

Uma dica é adicionar uma saída de diagnóstico após a etapa que falhou.

- name: Debug SSH connection
  if: failure()
  run: |
    echo "SSH config:"
    cat ~/.ssh/config || echo "No config file"
    echo "Known hosts:"
    cat ~/.ssh/known_hosts || echo "No known_hosts file"
    ssh -v ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} echo "Connection test"

O ssh -v gera logs detalhados e ajuda a identificar onde está o problema.


Conclusão

Depois de tudo isso, a ideia central cabe em uma frase: não existe uma solução de deploy perfeita, apenas a mais adequada para cada projeto.

Recomendações para escolher:

  • Site totalmente estático, como blog ou documentação: Cloudflare Pages ou Vercel, pela simplicidade
  • Frontend com uma API simples: uma plataforma gerenciada é suficiente; não complique com uma VPS
  • Backend complexo com banco de dados: VPS ou servidor em nuvem, pois o controle é importante
  • Arquitetura híbrida: frontend gerenciado e backend em VPS, aproveitando os pontos fortes de cada opção

Independentemente da escolha, a configuração do GitHub Actions segue quase sempre o mesmo padrão: build → transferência → reinicialização. Ao separar bem essas três etapas, o diagnóstico fica muito mais claro.

E mais uma coisa: não entre em pânico quando o deploy falhar. Analise os logs por etapas e descubra primeiro se o problema está na conexão SSH ou na execução de um comando. Adicione uma etapa de diagnóstico e a causa logo ficará visível.

Na próxima falha de deploy às três da manhã, espero que você encontre o motivo mais rápido.

Configurar o deploy em VPS com GitHub Actions

Processo completo para configurar o deploy em uma VPS via SSH usando GitHub Actions

⏱️ Estimated time: 30 min

  1. 1

    Step 1: Gerar o par de chaves SSH

    Gere localmente uma chave SSH exclusiva para o deploy:

    • ssh-keygen -t ed25519 -C "deploy@github" -f deploy_key
    • Adicione a chave pública (deploy_key.pub) ao arquivo ~/.ssh/authorized_keys do servidor
    • Salve o conteúdo da chave privada (deploy_key) no GitHub Secrets como SSH_PRIVATE_KEY
  2. 2

    Step 2: Configurar o GitHub Secrets

    Em Settings → Secrets → Actions no repositório, adicione:

    • SSH_PRIVATE_KEY: conteúdo completo da chave privada
    • SERVER_HOST: IP ou domínio do servidor
    • SERVER_USER: usuário SSH (como root ou ubuntu)
    • DEPLOY_PATH: caminho de destino do deploy
  3. 3

    Step 3: Criar o arquivo de workflow

    Crie a configuração de deploy em .github/workflows/deploy.yml:

    • Adicione a etapa de configuração da chave SSH (webfactory/ssh-agent-action)
    • Configure known_hosts para evitar falhas de verificação do host
    • Use rsync para transferir os artefatos do build
    • Execute o comando de reinicialização do serviço após o deploy
  4. 4

    Step 4: Testar o processo de deploy

    Envie o código para acionar o deploy automático ou inicie-o manualmente:

    • Acompanhe os logs de cada etapa
    • Se o SSH falhar, verifique o formato da chave e o known_hosts
    • Se o rsync falhar, verifique o caminho e as permissões
    • Adicione etapas de diagnóstico para investigar o problema

FAQ

Como resolver o erro 'Host key verification failed' durante o deploy com GitHub Actions?
Isso acontece porque a primeira conexão SSH com o servidor não encontra a configuração de known_hosts. Há duas soluções:

• Opção 1: use ssh-keyscan para obter a impressão digital do servidor e salve-a no secret SSH_KNOWN_HOSTS
• Opção 2: execute manualmente ssh-keyscan -H $SERVER_IP >> ~/.ssh/known_hosts no workflow

A primeira opção é recomendada por ser mais limpa e segura.
Onde devo guardar a chave SSH? Posso colocá-la diretamente no arquivo de workflow?
De forma alguma. A chave privada deve ficar no GitHub Secrets, e o workflow deve referenciá-la com `${{ secrets.SSH_PRIVATE_KEY }}`. Colocar a chave privada diretamente no código faz com que ela seja enviada ao repositório e fique visível para terceiros, o que representa um risco grave de segurança.
Qual plataforma é mais adequada para projetos pessoais: Vercel, Cloudflare Pages ou Netlify?
O Cloudflare Pages oferece a franquia gratuita mais generosa, com largura de banda ilimitada e 500 builds por mês, além de acesso mais estável na Ásia. A Vercel oferece a melhor experiência para projetos Next.js, mas as Serverless Functions do plano gratuito têm limite de 10 segundos. A Netlify tem um ecossistema maduro e um recurso prático de processamento de formulários.

Para sites totalmente estáticos, prefira o Cloudflare Pages.
Quais são as vantagens de uma arquitetura de deploy híbrida?
O frontend fica em uma plataforma gerenciada, com CDN e HTTPS automático, enquanto o backend roda em uma VPS, com controle total e sem as limitações da plataforma. Essa opção é indicada para projetos que precisam de banco de dados, tarefas agendadas e outros serviços de backend mais complexos.

Um workflow com vários jobs no GitHub Actions pode fazer o deploy nos dois ambientes.
Há logs demais quando o deploy falha. Como localizar o problema rapidamente?
Analise os logs por etapas, nesta ordem:

1. Problema de conexão SSH → verifique as etapas Setup SSH e known_hosts
2. Problema de transferência com rsync → verifique se o caminho existe e se as permissões estão corretas
3. Problema ao reiniciar o serviço → verifique a lista de arquivos no caminho de destino

Adicionar uma saída de diagnóstico após a etapa que falhou, como os logs detalhados de ssh -v, costuma revelar o problema rapidamente.
Qual é o risco do parâmetro --delete do rsync?
O --delete remove do diretório de destino os arquivos que não existem no diretório de origem, mantendo os dois lados totalmente sincronizados. Porém, se o caminho estiver configurado incorretamente, ele pode apagar arquivos que deveriam ser preservados.

No primeiro deploy, não use --delete. Ative-o somente depois de confirmar que tudo está correto. Outra opção é usar --delete-excluded para remover apenas arquivos excluídos.

12 min de leitura · Publicado em: 7 abr 2026 · Atualizado em: 4 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog