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.