Deploy de aplicações Node.js com PM2 e pnpm
· Atualizado em · 5 min de leitura
- node
- pm2
- pnpm
- deploy
O PM2 é conhecido por manter processos Node.js de pé, mas ele também traz uma pequena ferramenta de deploy: da sua máquina, um comando conecta no servidor por SSH, puxa o último commit, instala as dependências e recarrega a aplicação. Sem pipeline de CI, sem containers, só git e SSH.
Este artigo foi publicado em 2020 e reescrito em outubro de 2026 para o PM2 7, o pnpm e o Node.js 22. A maior mudança: o Node agora lê arquivos .env sozinho, então o dotenv saiu. Os passos de setup, deploy, reload, rollback e startup foram executados de verdade com uma aplicação de teste; os outputs vêm dessa execução, com o host e os caminhos trocados pelos do exemplo.
O que você precisa
- Um servidor Linux acessível por SSH com chave, com um usuário comum (aqui,
deploy). - Node.js 22 ou mais novo no servidor, e git.
- Acesso de leitura do servidor ao seu repositório, normalmente por uma deploy key.
No servidor, instale o pnpm e o PM2 globalmente:
npm install --global pnpm pm2
O PM2 roda um daemon que é dono do processo da sua aplicação, então no servidor ele deve ser global. Isso vai importar mais à frente, quando ele for configurado para subir junto com o sistema.
1. Adicione o PM2 ao projeto
Na sua máquina, o PM2 só dispara o deploy, então basta uma dependência de desenvolvimento:
pnpm add --save-dev pm2
Você vai rodá-lo pelo pnpm exec, sem instalar nada globalmente.
2. O arquivo ecosystem
O PM2 lê as configurações de um arquivo ecosystem. Chame ele de ecosystem.config.cjs: o pnpm init cria projetos com "type": "module", e neles um arquivo .js usando module.exports falha ao carregar. O PM2 encontra o nome .cjs sozinho.
// O destino do deploy fica no seu .env local, então nunca vai para o git
const { existsSync } = require('node:fs');
if (existsSync('.env')) process.loadEnvFile();
module.exports = {
apps: [
{
name: 'api',
script: './src/server.js',
node_args: '--env-file=.env',
env_production: {
NODE_ENV: 'production',
},
},
],
deploy: {
production: {
user: process.env.DEPLOY_USER,
host: process.env.DEPLOY_HOST,
ref: 'origin/main',
repo: '[email protected]:username/repo.git',
path: process.env.DEPLOY_PATH,
'post-setup': 'cp .env.example ../shared/.env',
'post-deploy': [
'ln -sfn ../shared/.env .env',
'pnpm install --frozen-lockfile',
'pm2 startOrReload ecosystem.config.cjs --env production',
].join(' && '),
},
},
};
O que cada parte faz:
process.loadEnvFile()vem no próprio Node e substitui odotenv. Ele lê o endereço do servidor do seu.envlocal.node_args: '--env-file=.env'faz o Node carregar o.envdo servidor quando a aplicação sobe, então a aplicação também não precisa de biblioteca de env.post-setuproda uma vez, depois do primeiro clone, e copia o arquivo de exemplo parashared/, a pasta que sobrevive a todo deploy.post-deployroda depois de cada deploy: liga o.envcompartilhado ao código, instala exatamente o que está no lockfile e sobe a aplicação na primeira vez, ou recarrega nas seguintes.
3. Arquivos de ambiente
São dois .env com o mesmo nome, em máquinas diferentes. Deixe os dois fora do git:
node_modules
.env
O seu .env local diz para onde fazer o deploy:
DEPLOY_USER=deploy
DEPLOY_HOST=203.0.113.10
DEPLOY_PATH=/home/deploy/www/example.com
O .env do servidor guarda as configurações da aplicação. Commite um .env.example como modelo:
PORT=3000
APP_ENV=production
4. Prepare o servidor
O setup cria a estrutura de pastas no servidor e clona o repositório. Rode uma vez:
pnpm exec pm2 deploy production setup
--> Deploying to production environment
--> on host 203.0.113.10
○ hook pre-setup
○ running setup
○ cloning [email protected]:username/repo.git
○ full fetch
Cloning into '/home/deploy/www/example.com/source'...
done.
○ executing post-setup `cp .env.example ../shared/.env`
○ setup complete
--> Success
Agora o servidor tem:
/home/deploy/www/example.com
├── current -> source
├── shared
│ ├── .env
│ ├── logs
│ └── pids
└── source
source guarda o código, current aponta para ele, e shared guarda o que precisa sobreviver a um deploy. Preencha os valores reais no shared/.env:
ssh [email protected]
nano ~/www/example.com/shared/.env
5. Faça o deploy
pnpm exec pm2 deploy production
--> Deploying to production environment
--> on host 203.0.113.10
○ deploying origin/main
○ executing pre-deploy-local
○ hook pre-deploy
○ fetching updates
○ full fetch
○ resetting HEAD to origin/main
HEAD is now at aef89b2 first version
○ executing post-deploy `ln -sfn ../shared/.env .env && pnpm install --frozen-lockfile && pm2 startOrReload ecosystem.config.cjs --env production`
Lockfile is up to date, resolution step is skipped
Packages: +77
Done in 451ms using pnpm v11.24.0
[PM2][WARN] Applications api not running, starting...
[PM2] App [api] launched (1 instances)
○ successfully deployed origin/main
--> Success
No primeiro deploy a aplicação ainda não estava rodando, então o PM2 subiu ela. Do próximo deploy em diante, o mesmo comando recarrega:
[PM2] Applying action reloadProcessId on app [api](ids: [ 0 ])
[PM2] [api](0) ✓
6. Sobreviva a um reboot
Até aqui a aplicação só vive enquanto o daemon do PM2 estiver de pé. No servidor, salve a lista de processos e registre o PM2 no sistema de inicialização:
pm2 save
pm2 startup
[PM2] Saving current process list...
[PM2] Successfully saved in /home/deploy/.pm2/dump.pm2
[PM2] Init System found: systemd
[PM2] To setup the Startup Script, copy/paste the following command:
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u deploy --hp /home/deploy
O pm2 startup não instala nada: ele imprime o comando para rodar com sudo. Repare que ele aponta para o caminho do binário do PM2. É por isso que o PM2 é global no servidor: uma cópia do node_modules do projeto apontaria para dentro de uma pasta que muda a cada atualização.
No dia a dia
| Tarefa | Comando |
|---|---|
| Publicar o último commit | pnpm exec pm2 deploy production |
| Voltar um deploy | pnpm exec pm2 deploy production revert 1 |
| Listar deploys anteriores | pnpm exec pm2 deploy production list |
| Rodar um comando no servidor | pnpm exec pm2 deploy production exec "pm2 logs api --lines 20 --nostream" |
O deploy sempre publica o que está em origin/main, então dê push antes. Um rollback faz checkout do commit anterior e roda o post-deploy de novo, reinstalando as dependências e recarregando a aplicação.
Quando o deploy não encontra o node ou o pnpm
O PM2 roda os hooks numa sessão SSH não interativa. Se o Node do servidor veio do nvm, essa sessão normalmente não carrega ele: o ~/.bashrc padrão do Ubuntu para cedo em shells não interativos, antes das linhas que o nvm adiciona no final. O deploy então falha com pnpm: command not found. Mova as linhas do nvm para antes desse return, ou instale o Node pelos pacotes da sua distribuição.