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

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:
- Ambiente de build limitado: memória e CPU têm limites, e projetos grandes podem falhar durante o build
- Pouca personalização: quer alterar a configuração do nginx? Não é possível
- Dependência da plataforma: se a empresa encerrar o serviço ou mudar suas políticas, será necessário migrar
- 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:
build: compila o projeto e gera os arquivos estáticosdeploy-frontend: envia os arquivos estáticos para o Cloudflare Pagesdeploy-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 secret | Descrição | Como obter |
|---|---|---|
SSH_PRIVATE_KEY | Conteúdo da chave privada SSH | Gere localmente e coloque a chave pública no servidor |
SERVER_HOST | IP ou domínio do servidor | Informações da sua VPS |
SERVER_USER | Nome do usuário SSH | Geralmente root ou ubuntu |
DEPLOY_PATH | Caminho de destino do deploy | Por exemplo, /var/www/html |
NOTIFICATION_WEBHOOK | Endereço para notificações de deploy | Webhook 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:
- 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
- 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
- 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
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
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
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
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?
• 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?
Qual plataforma é mais adequada para projetos pessoais: Vercel, Cloudflare Pages ou Netlify?
Para sites totalmente estáticos, prefira o Cloudflare Pages.
Quais são as vantagens de uma arquitetura de deploy híbrida?
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?
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?
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
Guia completo GitHub Actions
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
GitHub Actions Matrix: testes paralelos em várias versões na prática
Tutorial prático de GitHub Actions Matrix, com sintaxe básica, filtros exclude/include, otimização de fail-fast e controle de recursos com max-parallel, além de um pipeline completo para testes paralelos em várias versões.
Parte 4 de 6
Próximo
Como gerenciar secrets no GitHub Actions: do risco de vazamento à implantação sem chaves com OIDC
Aprenda a gerenciar secrets no GitHub Actions: escolha entre três níveis, aplique 8 regras de segurança, configure OIDC sem chaves e reduza riscos na cadeia de suprimentos.
Parte 6 de 6



Comentários
Entre com GitHub para comentar