Fazer o PM2 sobreviver a um reinício do servidor

O servidor reiniciou e o site não voltou. Voltou uma hora depois, quando alguém lançou um deploy. Parece um problema de deploy e não é: esse deploy limitou-se a correr pm2 start outra vez à mão.

Para que a aplicação arranque sozinha são precisas duas coisas distintas, e cada uma sem a outra não serve de nada.

As duas metades

pm2 save escreve a lista de processos atual em ~/.pm2/dump.pm2. O PM2 relança o que estiver nesse ficheiro e mais nada. Um script de deploy que faz pm2 delete e pm2 start mas nunca guarda deixa o dump desatualizado, ou vazio se nunca chegou a ser escrito.

pm2 startup instala uma unidade systemd que corre pm2 resurrect no arranque, e é ela que lê esse dump.

Uma sem a outra dá-te um arranque que não levanta nada, ou uma lista guardada que ninguém lê.

A armadilha

Correr pm2 startup com um utilizador sem privilégios não instala absolutamente nada. O comando deteta o systemd, calcula o comando que instalaria a unidade, imprime-o e termina. A saída é longa, aparece a verde e acaba com uma linha a dizer que podes copiar o comando. Parece sucesso. Não aconteceu nada.

Portanto: corre-o como root e confirma com o systemd, não com o que o PM2 imprimiu.

A ordem correta

A aplicação tem de estar a correr antes de guardares, senão guardas uma lista vazia.

# como <user> — confirma que a aplicação está a correr
pm2 status
# como root — instala a unidade para esse utilizador
env PATH=$PATH:/usr/bin pm2 startup systemd -u <user> --hp /home/<user>

O prefixo env PATH=$PATH:/usr/bin não é enfeite. O PATH do root normalmente não tem Node, e a unidade guarda o caminho com que foi gerada. Sem esse prefixo a unidade existe, mas no arranque não encontra o node.

# outra vez como <user> — escreve o dump
pm2 save

Verifica cada metade em separado

Falham em separado, por isso verificam-se em separado.

systemctl is-enabled pm2-<user>

Tem de responder enabled. Qualquer outra coisa, seja disabled ou Failed to get unit file state, quer dizer que a unidade nunca foi instalada, diga o que disser o PM2.

grep -o '"name":"[^"]*"' /home/<user>/.pm2/dump.pm2

Aí tem de aparecer <app>. Se o ficheiro não existe, ou o grep não imprime nada, o pm2 save nunca correu com a aplicação em linha.

Testa sem reiniciar

Não é preciso reiniciar uma máquina em produção para saber se isto funciona.

# como <user>
pm2 kill
# como root
systemctl start pm2-<user>
# como <user>
pm2 status
curl -sf http://127.0.0.1:3000/health

Se <app> voltou e está em linha, as duas metades funcionam. É exatamente o que o arranque faz.

Nunca corras pm2 save depois de pm2 kill. Com o daemon morto a lista de processos está vazia, e ao guardar escreves esse vazio no dump, desfazendo precisamente aquilo que estavas a testar. Se o fizeres por reflexo, levanta a aplicação outra vez e repete o pm2 save.

Põe o pm2 save no script de deploy

O dump desatualiza-se assim que um deploy mexe no processo. Se o teu script apaga e volta a arrancar, tem de guardar a seguir:

pm2 delete <app> || true
pm2 start ecosystem.config.cjs --only <app>
pm2 save

O || true conta na primeira execução, quando não há nada para apagar e o pm2 delete sai com código diferente de zero, o que mata o script inteiro com set -e.

Sem essa última linha tudo o que está acima continua a funcionar até ao primeiro deploy que mude o nome da aplicação, altere o número de instâncias ou troque o ponto de entrada. A partir daí o dump descreve um processo que já não existe, e o reinício seguinte ressuscita esse.

Mais uma coisa sobre a ordem de arranque

A unidade gerada leva After=network.target. Não está ordenada depois da tua base de dados. Num arranque a frio a aplicação pode subir antes de o MariaDB ou o PostgreSQL aceitarem ligações, falhar na primeira consulta e sair.

Normalmente resolve-se sozinho: o PM2 reinicia o processo, à segunda ou terceira tentativa a base de dados já está pronta, e o site sobe. A pista é um contador de reinícios acima de zero num processo que de resto está saudável, na coluna do pm2 status.

Se não recuperar, então ordena a unidade de forma explícita:

sudo systemctl edit pm2-<user>
[Unit]
After=network.target mariadb.service

Acrescenta isso só se já o viste falhar. Pôr uma unidade atrás de um serviço lento, ou que vive noutra máquina, troca um arranque que tenta outra vez por um que fica pendurado.

Analisa o teu site · Mais artigos · Sobre o AgentReady