Next.js binds its dev server to TCP port 3000 unless you tell it otherwise — and on a machine that also runs a local API, a Docker container, or a half-dead previous next dev, that default is exactly where collisions happen. There are three idiomatic ways to move it: an environment variable, a CLI flag, or a custom server wrapper. This post covers all three, plus how to diagnose the crash that sends most people looking.
-
The crash:
listen EADDRINUSE: address already in use :::3000— port 3000 is already bound by another process. -
Three ways out:
PORT=4000 next dev(inline shell variable),next dev -p 4000(CLI flag), or a customserver.jswrapper. -
Proof it worked:
npm run devstarts without the EADDRINUSE error and prints the new port number.
listen EADDRINUSE: address already in use :::3000
When you run the default development command, the terminal stops with a stack trace that looks like this:
> next dev
Error: listen EADDRINUSE: address already in use :::3000
at Server.setupListenHandle [as _listen2] (node:net:1315:19)
at listenInCluster (node:net:1385:12)
at Server.listen (node:net:1475:7)
at Server.listen (node_modules/next/dist/server/next-server.js:1234:15)
at Object.<anonymous> (node_modules/next/dist/bin/next.js:45:23)
at Module._compile (node:internal/modules/cjs/loader:1126:14)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1180:10)
It happens right after you execute npm run dev (or yarn dev) on a fresh clone of a Next.js project. The behavior is identical on macOS, Linux, and Windows because the underlying Node.js net module reports the same error code across platforms.
EADDRINUSE means the bind call failed because something else already owns that socket. Common culprits are:
- A local API server (Express, Fastify, Supabase Edge Functions) running on the same port.
- A Docker container exposing port 3000.
- A previous instance of
next devthat didn’t shut down cleanly.
Because the error bubbles up during the server startup phase, the process exits before any page rendering occurs, leaving you with the stack trace above and no hot‑reload.
How Next.js resolves the port
Next.js resolves the port from the -p/--port flag first, then the PORT environment variable, then falls back to a hard-coded 3000 default. There is no port option in next.config.js, which is why a config or .env file has no effect:
# Override the default 3000 — via the CLI flag…
next dev -p 4000
# …or the PORT environment variable
PORT=4000 next dev
That resolution order dictates which of the three methods below wins if you combine them: the -p flag beats the environment variable, which beats the default.
Option 1 — inline PORT environment variable
Pass PORT directly on the command line — before the next dev command. Next.js's HTTP server starts before .env* files are loaded, so setting PORT inside .env.local has no effect on the listening port.
macOS / Linux (bash/zsh):
PORT=4000 next dev
Windows PowerShell:
$env:PORT=4000; next dev
Windows CMD:
set PORT=4000 && next dev
You can also export the variable at the OS level (e.g. in ~/.zshrc or via System Properties → Environment Variables on Windows) so every project in your shell inherits it without touching package.json. For most single‑page apps, this inline variable is the least invasive change.
Option 2 — the -p flag in package.json
Modify the dev script in package.json to pass the -p flag:
{
"scripts": {
"dev": "next dev -p 4000",
"build": "next build",
"start": "next start"
}
}
The -p (or --port) flag is parsed by the Next.js binary and overrides any environment variable.
Option 3 — a custom server.js wrapper
If you need to run additional middleware (e.g., Supabase auth) before handing control to Next.js, create server.js:
// server.js
const { createServer } = require('http');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
const PORT = parseInt(process.env.PORT, 10) || 4000;
app.prepare().then(() => {
createServer((req, res) => {
// Example: inject a header for every request
res.setHeader('X-Custom-Header', 'iloveblogs');
handle(req, res);
}).listen(PORT, (err) => {
if (err) throw err;
console.log(`> Custom server listening on http://localhost:${PORT}`);
});
});
Then change the dev script:
{
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "next start"
}
}
All three approaches tell the Next.js runtime to bind to 4000 (or any port you choose) instead of the colliding 3000. Pick the one that fits your workflow, add the snippet, and restart the dev server with npm run dev.
Confirming the new port
Run the development command you just configured:
npm run dev
You should see output similar to:
> my-next-app@0.1.0 dev /path/to/my-next-app
> next dev -p 4000
▲ Next.js 15.x.x
- Local: http://localhost:4000
- Network: http://192.168.x.x:4000
Notice that the error stack trace is gone and the URL reflects the port you set. Open http://localhost:4000 in a browser; the app should load exactly as before.
If you still encounter EADDRINUSE, double‑check that the new port isn’t also taken. You can scan for listening ports with:
lsof -iTCP -sTCP:LISTEN -P | grep 4000
If the command returns a line, stop that process (kill -9 <PID>) or choose a different port.
When the replacement port is taken too
Sometimes the port you pick is also occupied (e.g., you chose 4000 but a local Supabase emulator runs there). In that case, repeat the verification step with a different number, such as 5000. The same code changes apply; only the numeric value changes.
Docker: container port vs host mapping
When you containerize the app, the host‑side port mapping (docker run -p 3000:3000) may conflict with the container’s internal port. The fix is to expose a different internal port in Dockerfile or docker-compose.yml and keep the Next.js configuration aligned:
# docker-compose.yml
services:
web:
build: .
ports:
- "5000:4000" # host:container
environment:
- PORT=4000
Now the container listens on 4000 internally while the host maps it to 5000.
Keeping ports from colliding across the team
The root invariant is that only one process can bind to a given TCP port on a host at a time. Because Next.js defaults to a well‑known development port, the likelihood of a clash grows as you add more local services (Supabase, Redis, mock APIs). To stay ahead:
- Add a
PORTentry to.env.exampleso every teammate knows which variable to set. - Include a pre‑flight script in
package.jsonthat checks port availability:
// scripts/check-port.js
const net = require('net');
const port = parseInt(process.env.PORT, 10) || 3000;
const server = net.createServer();
server.once('error', err => {
if (err.code === 'EADDRINUSE') {
console.error(`Port ${port} is already in use. Choose another port or stop the conflicting process.`);
process.exit(1);
}
});
server.once('listening', () => {
server.close();
});
server.listen(port);
Then run it before next dev:
{
"scripts": {
"predev": "node scripts/check-port.js",
"dev": "npm run predev && next dev"
}
}
- Document the chosen port in your onboarding guide and reference it when you write about deployment. For example, see my article on Deploying Next.js + Supabase to Production where I lock the production port to
8080and keep the dev port configurable.
By making the port explicit and checking it early, you eliminate the surprise EADDRINUSE crash.
Related
- AI Integration for Next.js + Supabase Applications
- Deploying Next.js + Supabase to Production
- Next.js 15 Minimum Node.js Version 18.18.0
- How to Structure Your Next.js App Router Project for Scale
- How to set the next/image component to 100% height
Originally published at https://www.iloveblogs.blog
Top comments (0)