DEV Community

Cover image for How to Change the Port in Next.js (and Fix EADDRINUSE)
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on Originally published at iloveblogs.blog

How to Change the Port in Next.js (and Fix EADDRINUSE)

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 custom server.js wrapper.
  • Proof it worked: npm run dev starts 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)
Enter fullscreen mode Exit fullscreen mode

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:

  1. A local API server (Express, Fastify, Supabase Edge Functions) running on the same port.
  2. A Docker container exposing port 3000.
  3. A previous instance of next dev that 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Windows PowerShell:

$env:PORT=4000; next dev
Enter fullscreen mode Exit fullscreen mode

Windows CMD:

set PORT=4000 && next dev
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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}`);
  });
});
Enter fullscreen mode Exit fullscreen mode

Then change the dev script:

{
  "scripts": {
    "dev": "node server.js",
    "build": "next build",
    "start": "next start"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 PORT entry to .env.example so every teammate knows which variable to set.
  • Include a pre‑flight script in package.json that 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);
Enter fullscreen mode Exit fullscreen mode

Then run it before next dev:

{
  "scripts": {
    "predev": "node scripts/check-port.js",
    "dev": "npm run predev && next dev"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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 8080 and keep the dev port configurable.

By making the port explicit and checking it early, you eliminate the surprise EADDRINUSE crash.

Related


Originally published at https://www.iloveblogs.blog

Top comments (0)