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.
- Crie um usuário de sistema dedicado e clone o repositório da aplicação no servidor Linux.
- Configure o ecossistema do PM2 em modo cluster para gerenciar os processos Node.js.
- Escreva um script shell de automação para executar pull, build e reload sequencial.
- Implemente um servidor receptor de webhooks com validação de assinatura HMAC SHA-256.
- 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-Tokene 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 reloadem 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
- guia: como usar webhooks para automatizar deploys
- Entenda o Deploy de Node.js no Debian 12 com PM2
- Guia Node-RED no CentOS 9 para webhooks do Mercado Pago
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.