Next.js

How to Change the Port in Next.js (dev and start)

Change the default Next.js port when it collides with another process, and know why next dev sometimes moves to 3001 on its own while next start crashes.

Mahdi Ben RhoumaMahdi Ben Rhouma
Updated
7 min read

Next.js binds its 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 forgotten previous server, that default is where collisions happen. This post covers the two built-in ways to move the port (the -p flag and the PORT variable), a custom server, and what the EADDRINUSE crash actually looks like. Every output below was reproduced on Next.js 16.3.8 on 2 October 2026.

listen EADDRINUSE: address already in use :::3000#

With port 3000 already taken, next start, or next dev started with PORT=3000 or -p 3000, prints this and exits:

text
⨯ Failed to start server
Error: listen EADDRINUSE: address already in use :::3000
    at <unknown> (Error: listen EADDRINUSE: address already in use :::3000)
    at new Promise (<anonymous>) {
  code: 'EADDRINUSE',
  errno: -4091,
  syscall: 'listen',
  address: '::',
  port: 3000
}

That run was on Windows. The code is EADDRINUSE on every platform; the numeric errno is the operating system's own number, so it differs on Linux and macOS.

EADDRINUSE means the bind call failed because something else already owns that port. Common culprits are:

  1. A local API server (Express, Fastify, Supabase Edge Functions) running on the same port.
  2. A Docker container publishing port 3000 on the host.
  3. A previous next dev or next start that did not shut down cleanly.

Why plain next dev often does not crash#

With no -p flag and no PORT variable, next dev does not stop on a busy port. In the same reproduction, a second project started with plain next dev while 3000 was taken printed:

text
⚠ Port 3000 is in use by process 17688, using available port 3001 instead.
▲ Next.js 16.3.8 (Turbopack)
- Local:         http://localhost:3001

Two consequences. First, if your app suddenly answers on 3001, something else holds 3000. Second, the moment you set the port explicitly, this fallback is off: -p 3000 and PORT=3000 both produced the crash above. Starting a second next dev in the same project folder is refused outright, with ⨯ Another next dev server is already running.

How Next.js picks the port#

The Next.js CLI reference documents -p/--port for both next dev and next start, the PORT environment variable, and the 3000 default. It also states that PORT cannot be set in .env, because the HTTP server boots before any code is initialised. There is no port option in next.config.js. In the reproduction:

  • PORT=4100 in .env.local was ignored: the server started on 3000 while listing .env.local under Environments.
  • PORT=5000 next dev -p 4000 started on 4000: the flag wins over the variable.

Option 1 — the -p flag in package.json#

This is the most reliable option, because it lives with the project and works the same in every shell:

json
{
  "scripts": {
    "dev": "next dev -p 4000",
    "build": "next build",
    "start": "next start -p 4000"
  }
}

next start takes the same flag, so set it there too if production should not use 3000. npm start serves an existing production build and does not create one, so run npm run build first, or the server stops with the missing .next/BUILD_ID error.

Option 2 — PORT in the shell#

Set PORT in the shell that launches the command, not in a .env file.

macOS / Linux (bash/zsh):

bash
PORT=4000 npm run dev

Windows PowerShell:

powershell
$env:PORT=4000; npm run dev

On a team with mixed operating systems, prefer Option 1: the inline PORT=4000 command syntax does not work in PowerShell or CMD.

Option 3 — a custom server.js#

A custom server binds the port itself, in its listen() call; the next() constructor does not open any port. Listen for 'error' on the server: the callback passed to listen() is only a 'listening' handler and never receives an error, per the Node.js server.listen() docs.

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(() => {
  const server = createServer((req, res) => handle(req, res));
  server.on('error', (err) => {
    if (err.code === 'EADDRINUSE') console.error(`Port ${port} is already in use.`);
    else console.error(err);
    process.exit(1);
  });
  server.listen(port, () => {
    console.log(`> Custom server listening on http://localhost:${port}`);
  });
});

Then point the dev script at it:

json
{
  "scripts": {
    "dev": "node server.js",
    "build": "next build",
    "start": "NODE_ENV=production node server.js"
  }
}

The start line uses the inline-variable syntax, so on Windows use cross-env or set NODE_ENV in the environment instead. A custom server also gives up some Next.js optimisations, which the custom server docs list; use it only when you need it for something else.

Confirming the new port#

Run the command you configured. With next dev -p 4000, Next.js 16.3.8 printed:

text
▲ Next.js 16.3.8 (Turbopack)
- Local:         http://localhost:4000

If you still get EADDRINUSE, the new port is taken as well. Find the process that holds it.

macOS / Linux:

bash
lsof -iTCP:4000 -sTCP:LISTEN -P
kill <PID>

Windows (PowerShell or CMD):

powershell
netstat -ano | findstr :4000
taskkill /PID <PID> /F

The last column of the netstat line is the process ID. Stop the process only if you know what it is; otherwise pick another port.

Docker: only the host side can collide#

Inside a container the app has its own network namespace, so the container's port 3000 never clashes with a process on your machine. What can clash is the host side of the mapping: docker run -p 3000:3000 fails if something on the host already listens on 3000. The Docker published ports docs describe the host:container order. Change the host number and leave the app alone:

yaml
# docker-compose.yml
services:
  web:
    build: .
    ports:
      - "5000:3000"   # host 5000 → container 3000

The app keeps listening on 3000 inside the container and is reachable on http://localhost:5000.

Keeping ports from colliding across the team#

Only one process can listen on a given TCP port on a host. As local services pile up (Supabase, Redis, mock APIs), make the port explicit in the dev script, as in Option 1, so everyone runs the same one. Remember that an explicit port also turns off the automatic move to 3001, so a clash fails loudly instead of silently changing the URL.

If you want an earlier, clearer message, add a check script and name it predev. npm runs a pre<name> script automatically before <name> (npm scripts docs), so the dev script must not call it again:

js
// scripts/check-port.js
const net = require('net');
const port = 4000;
 
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);
json
{
  "scripts": {
    "predev": "node scripts/check-port.js",
    "dev": "next dev -p 4000"
  }
}

In the reproduction, a dev script written as npm run predev && next dev ran predev twice. Once the app runs, deployment brings its own settings. The Next.js + Supabase production guide covers the Vercel setup, environment variables and migrations.

Further reading: Can't Import SVG in Next.js: Fix for Turbopack and Webpack, Why global-error.tsx Doesn't Fire in Next.js Dev Mode, Fix: FetchError request to localhost:1337 ECONNREFUSED ::1, Fix barrel_optimize Build Warnings with MUI in Next.js. Also see Failed to find font override values: next/font/google fix.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.