EADDRINUSE :::3000 — порт занят, что делать
Запускаешь npm run dev, а вместо знакомого ready on http://localhost:3000 получаешь:
Error: listen EADDRINUSE: address already in use :::3000Порт 3000 уже кем-то занят. Чаще всего это твой же предыдущий процесс Next или Vite, который не закрылся как следует. Реже — другой инструмент, который тоже любит этот порт. У меня в практике это бывает раз в неделю на каждом проекте.
Покажу, как быстро понять, кто держит порт, и как от этого избавиться. И заодно — как сделать, чтобы это меньше мешало.
Кто занимает порт
На macOS и Linux
Команда lsof отдаёт список процессов, слушающих указанный порт:
lsof -i :3000Вывод примерно такой:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 23415 me 23u IPv6 0x... 0t0 TCP *:3000 (LISTEN)Видим: процесс node с PID 23415 держит порт.
Можно ещё через ss:
ss -ltnp | grep :3000Эта команда работает на современных Linux. На macOS её нет, там lsof плюс netstat.
На Windows
netstat -ano | findstr :3000В последней колонке будет PID. Дальше:
tasklist /FI "PID eq 23415"Убиваем процесс
На macOS/Linux:
kill 23415Сначала пробую SIGTERM (это и есть kill без флагов). Большинство Node-процессов корректно завершатся, дописав логи. Если процесс не реагирует:
kill -9 23415Это SIGKILL — без шансов на корректное завершение. Использую только если обычный kill не помог секунд за 5.
Один лайнер, чтобы не возиться:
lsof -ti :3000 | xargs kill -9На Windows:
taskkill /PID 23415 /FЧто обычно держит 3000-й порт
Зомби-процесс от вчерашнего dev-сервера
Самая частая ситуация. Терминал закрылся, ноутбук уснул, на Mac ты ушёл в режим энергосбережения, и Next не успел корректно отвалиться. Если в lsof видишь node или npm — это он.
Несколько проектов одновременно
Открыл два терминала, в каждом — свой Next-проект. Первый занял 3000, второй ругается. Сам Next в новых версиях (13+) умеет автоматически выбирать следующий свободный порт и сообщает об этом в консоли. Если у тебя нет автоподбора — добавляй в package.json:
{
"scripts": {
"dev": "next dev -p 3001"
}
}Системные сервисы и docker-контейнеры
Бывает, что 3000 занимает совсем не Node. Например:
- контейнер с Grafana или другим инструментом, который ты забыл остановить (
docker ps); - локально установленный сервис, который кто-то поставил в systemd.
Если в lsof видишь com.docker.backend или подобное — иди в Docker. Останови контейнер:
docker ps
docker stop <container_id>WSL и Hyper-V на Windows
На Windows иногда порт «занят», хотя ничего не запущено. Виноват резервированный диапазон Hyper-V — Windows бронирует под виртуализацию случайные порты после ребута. Помогает:
net stop winnat
net start winnatЭто перезапускает службу NAT и обычно освобождает порт.
Как уменьшить количество таких ситуаций
Не убивать терминал, а останавливать процесс
Привыкаешь нажимать Ctrl+C в терминале с dev-сервером, прежде чем закрывать. Звучит банально, но половина зависших Node-процессов именно отсюда.
Использовать переменную PORT
Я в каждом проекте задаю порт через .env:
PORT=3030Next и Vite оба читают эту переменную и стартуют на ней. Удобно, когда у тебя 4 проекта и ты помнишь — этот всегда на 3030, тот на 3040.
Скрипт «убить и запустить»
Если работаю в команде, где половине людей всё ещё лень убивать процессы вручную, добавляю в package.json:
{
"scripts": {
"dev": "next dev",
"dev:fresh": "kill-port 3000 && next dev"
}
}Пакет kill-port ставится в devDependencies, работает и на Windows, и на Mac. На своих проектах хватает dev, для тех, кто часто ловит EADDRINUSE — есть dev:fresh.
Если EADDRINUSE прилетает в проде
Это уже другая история. На сервере под systemd или pm2 порт занимает предыдущая копия твоего сервиса. Симптомы — после деплоя сервис не поднимается, в логе EADDRINUSE.
Лечится правильным управлением жизненным циклом:
- в systemd-юните
ExecStopиKillMode=mixed, чтобы старый процесс дожидался завершения перед стартом нового; - в pm2 —
pm2 reloadвместоpm2 startпри апдейте; - в Docker-сетапах — нормальный graceful shutdown в самом приложении (по
SIGTERMзакрывать сервер).
Если в коде ты делаешь process.on('SIGTERM', () => server.close(...)), шанс схлопотать EADDRINUSE на деплое сильно снижается.
Маленький чек-лист
lsof -i :3000илиss -ltnp | grep :3000— кто держит порт.kill PID— корректно убить.kill -9 PID— если совсем висит.- Поменять порт через
PORT=...или-p 3001. - На Windows —
net stop winnat && net start winnat, если порт «занят», но процесса нет.
EADDRINUSE — почти никогда не баг твоего кода. Это либо хвост старого dev-сервера, либо чужой процесс на нужном порту. Минута расследования — и снова можно работать.