Augusto Dalmas
notas de estudo
← voltar aos artigos
System Design  ·  2 jul 2026  ·  7 min de leitura

GitHub Actions: três jeitos diferentes de fazer deploy numa VPS

Três aplicações, três workflows de deploy diferentes — e a escolha de onde rodar o build (no runner ou na VPS) muda tudo sobre o que cada um faz.

Fui automatizando deploy conforme os projetos foram pedindo, e acabei com três workflows de GitHub Actions que resolvem o mesmo problema — colocar código em produção numa VPS — de três jeitos diferentes. Nenhum foi planejado como “a forma certa” desde o início; cada um nasceu do tipo de aplicação que estava sendo deployada.

Landing page: copiar arquivo estático

O caso mais simples. Uma landing page sem processo de build — HTML, CSS e assets prontos no repositório. O workflow só precisa levar esses arquivos pra VPS:

./.github/workflows/deploy.yml

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Deploy via SCP
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}
          source: "index.html,404.html,sitemap.xml,robots.txt,public"
          target: "/home/deploy/apps/landing-page"

appleboy/scp-action faz o checkout, pega os arquivos listados em source e copia via SCP pro target na VPS. Não tem build, não tem npm install, não tem nada rodando no servidor depois — é literalmente copiar arquivo de um lugar pro outro. Faz sentido porque não existe nada pra compilar: o HTML que está no repositório é o HTML que vai servir em produção.

API: git pull e rebuild direto na VPS

Pra API do Dauth o workflow já é outra categoria de coisa. Em vez de mandar arquivos, ele manda comandos — a VPS é quem faz o trabalho:

./.github/workflows/deploy.yml

name: Deploy VPS

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Deploy na VPS
        uses: appleboy/ssh-action@v1.0.3

        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}

          script: |
            cd /home/deploy/apps/api

            git pull origin main

            docker-compose down

            docker-compose up -d --build

Aqui o appleboy/ssh-action abre uma sessão SSH e executa o script diretamente na VPS. Não existe checkout no runner do GitHub Actions — o repositório já está clonado na VPS, e o workflow só dá um git pull nele. O docker-compose down seguido de up -d --build derruba os containers, reconstrói a imagem com o código novo e sobe tudo de novo.

A diferença central pro caso da landing page: aqui é a VPS que builda, não o runner do GitHub. Faz sentido porque a imagem Docker da API precisa existir no ambiente onde ela vai rodar — não tem por que gerar essa imagem em outro lugar e depois transportar ela inteira pela rede.

Front React: build no runner, só o dist viaja

O terceiro caso mistura os dois anteriores. O React precisa de build (diferente da landing), mas eu não quero rodar npm run build na VPS (diferente da API):

./.github/workflows/deploy.yml

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci
      - run: npm run build

      - name: Deploy via SCP
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}
          source: "dist/"
          target: "/home/deploy/apps/front/dist"
          strip_components: 1

O npm ci e o npm run build rodam no runner do GitHub Actions — não na VPS. Só depois que o dist/ já existe, pronto e buildado, é que ele viaja via SCP pro servidor. A VPS nunca vê o código-fonte do React, nunca instala uma node_modules, nunca roda um builder: ela só recebe HTML/JS/CSS já compilados e serve isso (via Nginx, no meu caso).

O strip_components: 1 existe porque sem ele o SCP recriaria a pasta dist/ dentro do target, resultando em .../dist/dist/index.html. Com strip_components: 1, o conteúdo de dentro de dist/ vai direto pro target.

Por que a mesma decisão não serve pros três

O padrão que emergiu, sem eu ter desenhado de propósito, foi: builda onde o resultado do build é mais barato de mover.

  • Landing: não tem build, então não tem escolha — só copia.
  • API: a imagem Docker é pesada e específica do ambiente Linux/container da VPS. Buildar lá evita ter que empacotar e transportar uma imagem inteira pela rede a cada deploy.
  • React: o dist/ de uma SPA é leve (poucos KBs a poucos MBs), e o processo de build (npm ci + Vite/Webpack) é bem mais rápido e confiável rodando no runner do GitHub, com cache de dependências (cache: 'npm'), do que dependendo dos recursos da VPS pra fazer a mesma coisa.

Ou seja, a pergunta que decide onde buildar não é “qual é mais simples”, é “o que é mais barato de mover: o código-fonte ou o artefato final?”.

Limitações que ficaram evidentes

  • Nenhum dos três workflows tem rollback automático. Se o docker-compose up -d --build da API subir uma versão quebrada, o jeito de reverter é manual — outro git pull pra um commit anterior e rebuild.
  • Não existe healthcheck pós-deploy em nenhum dos três. O workflow termina “com sucesso” assim que o docker-compose up ou o scp retornam sem erro, mas isso não garante que a aplicação realmente subiu saudável.
  • Zero downtime não é garantido na API: entre o docker-compose down e o up -d --build terminar de subir, a API fica fora do ar. Pra um serviço com usuários ativos, isso é uma janela de indisponibilidade real a cada deploy.
  • Os três dependem da mesma chave SSH (VPS_SSH_KEY) com acesso amplo à VPS. Não há segregação de permissões por projeto — qualquer um desses workflows, se comprometido, tem o mesmo nível de acesso ao servidor.

Próximo passo

O ponto mais urgente de resolver é o downtime da API: trocar o docker-compose down + up --build por um rebuild que sobe o container novo antes de derrubar o antigo (ou migrar pra alguma estratégia de blue-green mais séria). Depois disso, valeria adicionar um step de healthcheck no fim de cada workflow — um curl simples num endpoint de status já preveniria a maioria dos “deploy que passou mas a aplicação não subiu”.

#github actions #ci-cd #deploy #devops #vps
próximo artigo →
Cache-aside em Node.js: o que muda de verdade entre 1000 requisições sem e com cache