Ошибка Error: listen EADDRINUSE: address already in use :::3000 при перезапуске через pm2, systemd или compose. Приложение то запускается, то нет, а иногда уходит в цикл перезапусков.
Что было в обсуждении. Приложение запускалось через bash-обёртку, которая вызывала node с транспилятором. При pm2 restart pm2 посылал сигнал завершения обёртке, но сам node-процесс сигнал не получал, порт оставался занят, и новый экземпляр падал на listen. Сопровождающие указали на три вещи: не оборачивать процесс в скрипт (или использовать exec в конце скрипта, чтобы node заменил bash и получал сигналы напрямую), увеличить время ожидания завершения (kill_timeout), и обрабатывать SIGTERM в приложении, закрывая сервер и соединения.
Минимальная корректная остановка в приложении.
const server = app.listen(PORT);
process.on('SIGTERM', () => {
server.close(() => process.exit(0));
setTimeout(() => process.exit(1), 10000).unref();
});
server.close() перестаёт принимать новые соединения и завершает процесс после закрытия текущих; таймер на всякий случай не даёт зависнуть навсегда.
Со стороны менеджера процессов.
- pm2:
kill_timeoutв конфигурации больше, чем нужно приложению на закрытие;wait_readyиlisten_timeout, если хотите, чтобы pm2 ждал готовности нового экземпляра. - systemd:
KillSignal=SIGTERM,TimeoutStopSec=15,Restart=on-failure,RestartSec=2. Слишком маленькийRestartSecдаёт как раз цикл «порт ещё занят, снова упал». - Docker/compose: контейнер получает SIGTERM, но если процесс запущен через shell-форму
CMD node app.jsвнутриsh -c, сигнал до node не дойдёт; используйте exec-формуCMD ["node", "app.js"].
Кто держит порт прямо сейчас. ss -ltnp | grep :3000 или lsof -i :3000 покажут процесс; pm2 list покажет забытый экземпляр под другим именем.
Статус. Причина названа в обсуждении сопровождающими и совпадает с документацией; подтверждение автора не показано.
Сигналы и чистый перезапуск описаны в документации pm2.