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:

bash
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:

bash
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.

javascript
// 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 o dotenv. Ele lê o endereço do servidor do seu .env local.
  • node_args: '--env-file=.env' faz o Node carregar o .env do servidor quando a aplicação sobe, então a aplicação também não precisa de biblioteca de env.
  • post-setup roda uma vez, depois do primeiro clone, e copia o arquivo de exemplo para shared/, a pasta que sobrevive a todo deploy.
  • post-deploy roda depois de cada deploy: liga o .env compartilhado 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:

texto
node_modules
.env

O seu .env local diz para onde fazer o deploy:

texto
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:

texto
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:

bash
pnpm exec pm2 deploy production setup
output
--> 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:

texto
/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:

bash
ssh [email protected]
nano ~/www/example.com/shared/.env

5. Faça o deploy

bash
pnpm exec pm2 deploy production
output
--> 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:

output
[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:

bash
pm2 save
pm2 startup
output
[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

TarefaComando
Publicar o último commitpnpm exec pm2 deploy production
Voltar um deploypnpm exec pm2 deploy production revert 1
Listar deploys anteriorespnpm exec pm2 deploy production list
Rodar um comando no servidorpnpm 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.

Fontes