save the pm2 list after starting or stopping a server

Without it, pm2 resurrects the list as it was the last time somebody happened to
run `pm2 save` — so a machine that reboots comes back without the site, and with
nothing to indicate anything was meant to be there. The systemd unit is enabled
on this host, so the resurrect definitely happens; the only question was what it
would restore.

After stopping too, not only after starting. The dump has to match reality in
both directions: a server stopped here and left in the dump comes back on its
own at the next reboot, which is the same bug pointing the other way.

A failed save does not fail the action — the process is already started or
stopped by then, and reporting "could not start" about something that did start
is worse than a line saying the list was not persisted. It is in the console
either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 14:16:46 +01:00
co-authored by Claude Opus 5
parent 0a95647b8e
commit 752c06d24a
+28 -2
View File
@@ -128,6 +128,27 @@ const run = async (
}
};
/**
* Persist pm2's process list, so a reboot brings back what is actually running.
*
* Without this, pm2 resurrects the list as it was the last time somebody saved — which, after starting
* a server here, means the machine comes back up without the site and with no indication that anything
* was meant to be there. The systemd unit is enabled on this host, so the resurrect definitely happens;
* the only question is what it restores.
*
* Run after every start AND every stop, because the dump has to match reality in both directions: a
* server stopped here and left in the dump comes back on its own at the next reboot, which is the same
* bug pointing the other way.
*
* A failure to save does not fail the action. The process is already started or stopped by then, and
* reporting "could not start" about something that did start is worse than a line saying the list was
* not persisted.
*/
async function persist(): Promise<string> {
const saved = await run([PM2, 'save'], 30_000);
return saved.ok ? 'pm2 list saved' : `pm2 save failed: ${saved.output}`;
}
export function checkPort(value: unknown, field: string): number {
const port = Number(value);
if (!Number.isInteger(port) || port < 1024 || port > 65535) {
@@ -270,14 +291,19 @@ export async function startServer(which: ServerName): Promise<{ ok: boolean; out
return {
ok: started.ok,
output: `$ pm2 start ${PROCESS[which]} (${which}, port ${port})\n${started.output}`,
output: [`$ pm2 start ${PROCESS[which]} (${which}, port ${port})`, started.output, await persist()]
.filter(Boolean)
.join('\n'),
};
}
/** Stop one. `delete`, not `stop`, so a stopped entry does not linger in the list looking like a fault. */
export async function stopServer(which: ServerName): Promise<{ ok: boolean; output: string }> {
const result = await run([PM2, 'delete', PROCESS[which]], 30_000);
return { ok: result.ok, output: `$ pm2 delete ${PROCESS[which]}\n${result.output}` };
return {
ok: result.ok,
output: [`$ pm2 delete ${PROCESS[which]}`, result.output, await persist()].filter(Boolean).join('\n'),
};
}
/**