Pular para o conteúdo

Automatizar Deploy Contínuo Node.js com Webhooks e PM2 no Linux

Por Equipe Técnica AviraHost · 17 min de leitura · Atualizado em · nodejs, pm2, webhooks, deploy-continuo, linux, devops, AviraHost · 0

Para automatizar o deploy contínuo de Node.js com webhooks e PM2 no Linux, configure um listener HTTP que recebe gatilhos do GitHub, executa pull, instala dependências e recarrega o cluster PM2 sem interromper requisições ativas. Os passos abaixo partem de um servidor com Node.js, PM2 e Nginx já instalados.

  1. Crie um usuário de sistema dedicado e clone o repositório da aplicação no servidor Linux.
  2. Configure o ecossistema do PM2 em modo cluster para gerenciar os processos Node.js.
  3. Escreva um script shell de automação para executar pull, build e reload sequencial.
  4. Implemente um servidor receptor de webhooks com validação de assinatura HMAC SHA-256.
  5. Configure o Nginx como proxy reverso para encaminhar requisições externas de forma segura ao listener de webhook.

Pré-requisitos

  • Servidor Linux executando Ubuntu 24.04 LTS com acesso SSH via chave pública.
  • Node.js 22 LTS ou 24 LTS instalado globalmente via repositório oficial NodeSource.
  • Gerenciador de processos PM2 instalado via npm (npm install -g pm2).
  • Nginx 1.28+ instalado e ativo para terminação TLS e proxy reverso.
  • Repositório no GitHub com permissão de administrador para cadastrar webhooks (o GitLab envia o segredo no cabeçalho X-Gitlab-Token e exige outra verificação, fora deste guia).
  • Usuário não privilegiado criado no Linux com permissões restritas ao diretório do projeto.

Configuração do usuário do sistema e estrutura da aplicação

O gerenciamento seguro de processos Node.js exige isolamento de privilégios para evitar que acessos indevidos comprometam o sistema operacional. Crie um usuário de serviço sem privilégios administrativos chamado deployer para hospedar o código-fonte e gerenciar a execução da aplicação.

sudo adduser --disabled-password --gecos "" deployer
sudo mkdir -p /var/www/minha-aplicacao
sudo chown -R deployer:deployer /var/www/minha-aplicacao

Alterne para o novo usuário e gere um par de chaves SSH ed25519 para autenticar com o repositório Git sem armazenar senhas em texto puro:

sudo -u deployer -i
ssh-keygen -t ed25519 -C "deployer@servidor" -f ~/.ssh/id_ed25519 -N ""
cat ~/.ssh/id_ed25519.pub

Copie a chave pública exibida e cadastre-a como Deploy Key de somente leitura no GitHub. Em seguida, teste o acesso e clone o repositório no diretório de destino:

ssh -T [email protected]
cd /var/www/minha-aplicacao
git clone [email protected]:seu-usuario/seu-repositorio.git current
cd current
npm ci
npm run build --if-present
npm prune --omit=dev

O npm ci instala exatamente as versões do package-lock.json — inclusive as dependências de desenvolvimento, que o build costuma exigir (TypeScript, bundlers) — e para com erro se o arquivo estiver desatualizado. O npm run build --if-present só compila se o projeto tiver o script build, e o npm prune --omit=dev remove as dependências de desenvolvimento depois disso.

Gerenciamento da aplicação com PM2 em modo cluster

A orquestração de microsserviços e APIs exige reinicialização coordenada para absorver novas versões de código sem derrubar conexões ativas. Para entender as nuances de provisionamento básico, consulte o artigo Entenda o Deploy de Node.js no Debian 12 com PM2, que detalha o comportamento do daemon em distros baseadas em Debian.

Crie um arquivo de configuração estruturado chamado ecosystem.config.cjs dentro da raiz do projeto para definir variáveis de ambiente, portas e número de instâncias:

module.exports = {
  apps: [{
    name: "api-producao",
    script: "./src/server.js",
    cwd: "/var/www/minha-aplicacao/current",
    instances: "max",
    exec_mode: "cluster",
    autorestart: true,
    watch: false,
    max_memory_restart: "500M",
    env: {
      NODE_ENV: "production",
      PORT: 3000
    }
  }]
};

Inicie a aplicação utilizando o PM2 e configure o serviço para inicializar junto com o sistema operacional:

pm2 start ecosystem.config.cjs
pm2 save

Para persistir o processo no systemd do Ubuntu 24.04 LTS, gere o comando de inicialização adequado executando pm2 startup systemd -u deployer --hp /home/deployer e execute a linha recomendada pelo terminal utilizando privilégios sudo. Verifique a tabela de status do cluster:

pm2 status
┌────┬────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┐
│ id │ name           │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │
├────┼────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┤
│ 0  │ api-producao   │ default     │ 1.0.0   │ cluster │ 28412    │ 12s    │ 0    │ online    │ 0%       │ 42.1mb   │
│ 1  │ api-producao   │ default     │ 1.0.0   │ cluster │ 28413    │ 12s    │ 0    │ online    │ 0%       │ 41.8mb   │
└────┴────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┘

Criação do script de automação do deploy

A execução manual de tarefas de compilação gera riscos operacionais e inconsistências entre ambientes de execução. O script em bash centraliza a validação de branches, instalação de dependências, builds de TypeScript ou assets e o recarregamento do cluster via comando pm2 reload.

Crie o arquivo /var/www/minha-aplicacao/deploy.sh com o seguinte conteúdo:

#!/usr/bin/env bash
set -e

APP_DIR="/var/www/minha-aplicacao/current"
BRANCH="main"

echo "[$(date +'%Y-%m-%d %H:%M:%S')] Iniciando deploy da branch $BRANCH..."

cd "$APP_DIR"

# Atualiza referências e reseta mudanças acidentais locais
git fetch origin "$BRANCH"
git checkout "$BRANCH"
git reset --hard "origin/$BRANCH"

# Dependências completas: o build costuma precisar das de desenvolvimento
npm ci

# Build, se o package.json tiver o script "build"
npm run build --if-present

# Depois do build, só as dependências de produção
npm prune --omit=dev

# Recarregamento sequencial sem downtime
echo "Recarregando instâncias no PM2..."
pm2 reload ecosystem.config.cjs --update-env

echo "[$(date +'%Y-%m-%d %H:%M:%S')] Deploy finalizado com sucesso."

Defina as permissões corretas para que apenas o usuário deployer possa ler e executar o script:

chmod 750 /var/www/minha-aplicacao/deploy.sh

Faça um teste de execução para garantir que o script roda sem erros do início ao fim:

/var/www/minha-aplicacao/deploy.sh

Implementação do listener de webhook seguro com Node.js

A recepção de gatilhos HTTP exige validação criptográfica para impedir que agentes externos executem scripts arbitrários no sistema. A especificação do GitHub envia um cabeçalho X-Hub-Signature-256 que contém o hash HMAC SHA-256 do corpo da requisição gerado a partir de um segredo compartilhado. Se você planeja desenhar pipelines para múltiplos ambientes, veja nosso guia: como usar webhooks para automatizar deploys.

Crie um diretório isolado para o listener e inicie um projeto Node.js simples nativo:

mkdir -p /home/deployer/webhook-listener
cd /home/deployer/webhook-listener
npm init -y

Crie o arquivo webhook.js utilizando a biblioteca nativa crypto e http sem dependências externas pesadas:

const http = require('http');
const crypto = require('crypto');
const { execFile } = require('child_process');

const PORT = 8080;
const SECRET = process.env.WEBHOOK_SECRET;
if (!SECRET) {
  console.error('Defina a variável WEBHOOK_SECRET antes de iniciar o listener.');
  process.exit(1);
}
const SCRIPT_PATH = '/var/www/minha-aplicacao/deploy.sh';

function verifySignature(payload, signature) {
  if (typeof signature !== 'string') return false;
  const expected = Buffer.from('sha256=' + crypto.createHmac('sha256', SECRET).update(payload).digest('hex'));
  const received = Buffer.from(signature);
  // timingSafeEqual lança exceção quando os tamanhos diferem: compare o tamanho antes
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const server = http.createServer((req, res) => {
  if (req.method === 'POST' && req.url === '/payload') {
    let body = [];

    req.on('data', chunk => body.push(chunk));
    req.on('end', () => {
      const rawBody = Buffer.concat(body);
      const signature = req.headers['x-hub-signature-256'];

      if (!verifySignature(rawBody, signature)) {
        res.writeHead(403, { 'Content-Type': 'application/json' });
        return res.end(JSON.stringify({ error: 'Assinatura inválida.' }));
      }

      let payload;
      try {
        payload = JSON.parse(rawBody.toString());
      } catch (e) {
        res.writeHead(400, { 'Content-Type': 'application/json' });
        return res.end(JSON.stringify({ error: 'Payload JSON malformado.' }));
      }

      // Filtra apenas eventos de push na branch main
      if (req.headers['x-github-event'] === 'push' && payload.ref === 'refs/heads/main') {
        res.writeHead(202, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ status: 'Deploy iniciado com sucesso.' }));

        console.log('Disparando script de deploy...');
        execFile(SCRIPT_PATH, (error, stdout, stderr) => {
          if (error) {
            console.error('Falha na execução do deploy:', error);
            return;
          }
          console.log('Deploy stdout:', stdout);
          if (stderr) console.error('Deploy stderr:', stderr);
        });
      } else {
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ message: 'Evento ignorado (não é push na main).' }));
      }
    });
  } else {
    res.writeHead(404, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: 'Rota não encontrada.' }));
  }
});

server.listen(PORT, '127.0.0.1', () => {
  console.log(`Listener de webhook ativo em http://127.0.0.1:${PORT}`);
});

Inicie o receptor de webhook com PM2 para garantir sua resiliência em caso de falhas transitórias. Primeiro, gere um segredo forte:

openssl rand -hex 32

Cadastre esse valor no campo Secret do webhook no GitHub e passe o mesmo valor ao listener pelo ambiente do comando — a opção --env do PM2 escolhe um bloco do arquivo ecosystem, não define variáveis. O read evita que o segredo fique no histórico do shell, e o pm2 save grava a variável junto com o processo para o reinício automático:

read -rsp 'Segredo do webhook: ' WEBHOOK_SECRET; echo
export WEBHOOK_SECRET
pm2 start webhook.js --name webhook-receiver
pm2 save

Configuração do Nginx e proxy reverso para o webhook

A infraestrutura de rede externa nunca deve conversar diretamente com listeners internos sem criptografia TLS e filtragem básica de rotas. O Nginx atua como terminador SSL, protegendo a rota do webhook e roteando requisições do seu domínio para a porta local 8080.

Os comandos a seguir exigem sudo: saia da sessão do deployer com exit e continue com o seu usuário administrativo. Crie um arquivo de configuração no Nginx para isolar a recepção das requisições:

sudo nano /etc/nginx/sites-available/webhook.conf

Insira a definição do servidor, direcionando a rota para o listener interno:

server {
    listen 80;
    server_name deploy.seudominio.com.br;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name deploy.seudominio.com.br;

    ssl_certificate /etc/letsencrypt/live/deploy.seudominio.com.br/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/deploy.seudominio.com.br/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    client_max_body_size 5M;

    location /payload {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Hub-Signature-256 $http_x_hub_signature_256;
        proxy_set_header X-GitHub-Event $http_x_github_event;
    }

    location / {
        return 404;
    }
}

Ative o site criando o link simbólico e verifique a integridade sintática das configurações antes de reiniciar o serviço:

sudo ln -s /etc/nginx/sites-available/webhook.conf /etc/nginx/sites-enabled/
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Recarregue o Nginx:

sudo systemctl reload nginx

Para concluir, acerte as regras no firewall local utilizando o UFW para permitir apenas tráfego seguro nas portas padrão:

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status

Problemas comuns e como resolver

Sintoma: O webhook retorna status 403 Forbidden após o disparo

Causa: Incompatibilidade entre o token configurado no painel do GitHub e o parâmetro WEBHOOK_SECRET definido nas variáveis de ambiente do listener, gerando discrepância na assinatura criptográfica HMAC.
Solução: como usuário deployer, exporte WEBHOOK_SECRET com o valor exato do campo "Secret" do webhook no GitHub e execute pm2 restart webhook-receiver --update-env, seguido de pm2 save.

Sintoma: Falha no git pull com mensagem "Permission denied (publickey)"

Causa: O listener acionou o script sob um usuário sem permissão para ler o arquivo ~/.ssh/id_ed25519 ou sem a chave SSH autorizada nas configurações do repositório remoto.
Solução: Execute ls -la ~/.ssh e confirme permissões 600 para a chave privada e 700 para o diretório .ssh. Certifique-se de que o PM2 do webhook está rodando sob o usuário deployer e não sob o root.

Sintoma: Processos não atualizam o código após o comando pm2 reload

Causa: O arquivo de ecossistema aponta para arquivos compilados antigos ou o diretório de trabalho (cwd) está desalinhado do diretório onde o git pull descarrega os arquivos novos.
Solução: confirme que o deploy.sh roda o build (npm run build --if-present) antes do reload, que o cwd do ecosystem.config.cjs aponta para o diretório atualizado pelo Git e que o reload usa --update-env.

Perguntas frequentes

O que acontece se o script de webhook falhar durante o deploy?

Com set -e, o deploy.sh para no primeiro erro e o pm2 reload não é executado: as instâncias em execução continuam com a versão anterior, sem indisponibilidade. Atenção: o git reset já trocou os arquivos em disco, então um reinício do processo carregaria o código novo — corrija o problema e rode o deploy de novo.

Como o PM2 garante zero downtime durante a atualização da aplicação?

Ao utilizar o comando pm2 reload em conjunto com o modo cluster, o PM2 reinicia os processos em etapas sequenciais. Ele aguarda uma nova instância subir e responder requisições antes de desligar a instância antiga, eliminando quedas.

É seguro expor a porta do listener de webhook diretamente na internet?

Não é recomendado expor a porta diretamente. O ideal é colocar o serviço atrás de um proxy reverso com Nginx protegido por SSL e validar a assinatura HMAC que o GitHub envia no cabeçalho X-Hub-Signature-256 para autenticar a requisição.

Qual usuário do sistema Linux deve executar o PM2 e o script de deploy?

O recomendado é usar um usuário não privilegiado exclusivo para a aplicação com permissão de leitura e escrita apenas no diretório do projeto. Evite utilizar o usuário root para rodar processos Node.js e PM2 por questões de segurança no servidor.

Conclusão

  • Mantenha o listener de webhook e o ecossistema de processos isolados sob usuários de serviço dedicados no Linux.
  • Adote sempre o pm2 reload em modo cluster com validação estrita de erros em shell script (set -e).
  • Proteja a rota do webhook utilizando terminação SSL com Nginx e conferência criptográfica obrigatória de payload com HMAC SHA-256.

Leia também

Precisa de ajuda com deploy em VPS Linux?

Conte com a infraestrutura estável e balanceada da AviraHost para hospedar seus serviços Node.js com alta conectividade, latência reduzida e total isolamento para suas esteiras de entrega contínua.

Conheça nossos planos de Servidor VPS


Esta resposta foi útil?