Local dev port planner
List the services you run locally, one per line, and get a port plan with no clashes. The planner flags two services on one port, ports below 1024, ports in the operating system’s ephemeral ranges, and names that still need a port. It keeps the ports you chose, moves the lower-priority duplicate to the nearest free port above it, and prints a table with .env, Procfile, and Compose snippets. It runs in your browser.
Worked example
# Local stack for one project
nextjs
vite
api:3000
redis
worker
proxy 80
metrics:5432
debug:50000Local dev port plan
7 of 8 services planned: 6 kept, 1 moved. 1 not planned. No two planned services share a port.
Plan
Line Service Requested Planned Result Notes
---- ------- -------------- ------- ------------ --------------------------------------------------------------
2 nextjs 3000 (default) 3001 moved moved from 3000 because "api" (line 4) asked for it explicitly
3 vite 5173 (default) 5173 kept
4 api 3000 3000 kept
5 redis 6379 (default) 6379 kept
6 worker none none needs a port add a port to include it in the plan
7 proxy 80 80 kept below 1024, may need elevated privileges
8 metrics 5432 5432 kept
9 debug 50000 50000 kept in the IANA dynamic range; in the Linux ephemeral range
A moved service needs its new port set in the tool's own configuration, for example with its port flag or an environment variable.
Findings
- warning (lines 2 and 4): Port 3000 is wanted by "nextjs" (line 2, tool default) and "api" (line 4, explicit).
- error (line 6): "worker" is not in the known tools table, so it needs a port. Write worker:3000 or worker 3000.
- warning (line 7): "proxy" uses port 80, which is below 1024. On many Unix-like systems a process needs elevated privileges to listen on a port in that range.
- info (line 8): "metrics" uses port 5432, which is also the default port of PostgreSQL. This only matters if you also run that tool on its default port.
- warning (line 9): "debug" uses port 50000, which is in the IANA dynamic range (49152 to 65535) and the default Linux ephemeral range (32768 to 60999). The operating system may give this port to an outgoing connection before your service starts.
.env
NEXTJS_PORT=3001
VITE_PORT=5173
API_PORT=3000
REDIS_PORT=6379
PROXY_PORT=80
METRICS_PORT=5432
DEBUG_PORT=50000
Procfile skeleton (replace your-command, assumes a POSIX shell)
nextjs: export PORT=3001 && your-command --port $PORT
vite: export PORT=5173 && your-command --port $PORT
api: export PORT=3000 && your-command --port $PORT
redis: export PORT=6379 && your-command --port $PORT
proxy: export PORT=80 && your-command --port $PORT
metrics: export PORT=5432 && your-command --port $PORT
debug: export PORT=50000 && your-command --port $PORT
docker-compose.yml, ports only (container port is the tool default when known; merge into your own services; 127.0.0.1 keeps the port off other interfaces)
services:
nextjs:
ports:
- "127.0.0.1:3001:3000"
vite:
ports:
- "127.0.0.1:5173:5173"
api:
ports:
- "127.0.0.1:3000:3000"
redis:
ports:
- "127.0.0.1:6379:6379"
proxy:
ports:
- "127.0.0.1:80:80"
metrics:
ports:
- "127.0.0.1:5432:5432"
debug:
ports:
- "127.0.0.1:50000:50000"Read the plan table first, then the findings. The example uses a small built-in table of four tools: Vite, Next.js, PostgreSQL, and Redis.
nextjs has no port, so it takes the default of 3000. api asks for 3000 explicitly, and an explicit port beats a default, so api keeps 3000 and nextjs moves to the nearest free port above it, 3001. vite and redis keep their defaults.
worker is not in the table and has no port, so it is listed as needing one and left out of the snippets. proxy on port 80 is below 1024, which needs elevated privileges on many Unix-like systems. debug on port 50000 is in both the IANA dynamic range and the default Linux ephemeral range, so the operating system may already have given it to an outgoing connection. metrics on 5432 is the default port of PostgreSQL. That is information only, because PostgreSQL is not in the list.
The snippets use the planned ports, so nextjs appears as NEXTJS_PORT=3001.
How to use it
List your services
Write one service per line as
name,name:port, orname port, for exampleapi:4000orweb 3000. A name that matches a tool in the planner’s table can leave the port out and takes that tool’s default. Lines that start with#are comments.Give unknown names a port
A name the planner does not know has no default, so it needs a port. Write it as
worker:4100. The planner keeps your explicit ports unless two services ask for the same one.Read the findings
Start with errors, which stop a service from being planned, then warnings, then information. A warning about a port below 1024 or in an ephemeral range does not block the plan, but it can stop a service from starting.
Check the plan table
Each row shows the port requested, the port planned, and whether the service was kept or moved. A moved row names the service that kept the port. To make a different service move, give the one you want to keep an explicit port, or put its line higher up.
Apply the moves
A moved service needs its new port set in the tool’s own configuration, for example with its port flag or an environment variable. Copy the
.envlines, the Procfile skeleton, or the Compose ports into your project, and replaceyour-commandwith the real command.Check what is running
The planner works from your list and cannot see your machine. Before you start the stack, check that the planned ports are free with the port in use command tool.
How it works
The planner applies these rules in order. Every rule is deterministic, so the same list always gives the same plan.
- Input. One service per line as
name,name:port, orname port. Blank lines are skipped, and a#at the start of a line or after a space starts a comment. A name may use letters, digits, and the characters. _ - + @ /, up to 64 characters. A port is a whole number from 1 to 65535 with no leading zeros. The limits are 100 services and 32 KB. The first line for a name counts. A later line with the same name, ignoring letter case, is ignored and reported. - Known tools. A name matches a tool when it equals the tool’s id, one of its aliases, or its display name, ignoring letter case. A matching name with no port takes the tool’s first TCP default port. An explicit port always wins. Only TCP defaults are used, and every service is treated as TCP.
- Ports below 1024. The IANA registry calls 0 to 1023 the System Ports, and RFC 7605 notes that some systems need privileged access to use them. On Linux the boundary is the
net.ipv4.ip_unprivileged_port_startsetting, which is 1024 by default. The planner always uses 1024. - Ephemeral ranges. IANA does not assign 49152 to 65535, the Dynamic Ports, and operating systems commonly take ephemeral ports from them. The Linux default for
net.ipv4.ip_local_port_rangeis 32768 to 60999. A port in either range may already be in use by an outgoing connection. Your system may use a different range. - Conflicts. Two or more services that resolve to the same port are a conflict. A port that is also the default of another known tool is reported as information only.
- Who keeps a port. An explicit port beats a port taken from a tool default. If both are the same kind, the earlier line wins. The winner keeps the port.
- Where the others move. Services that lose move one at a time, in line order. Each starts at its requested port plus one, or at 1024 if that is higher, and takes the first port the plan has not used, up to 49151. A port the plan has already handed out counts as used. The other TCP default ports of tools in your list, such as a second port for a web interface, are skipped too. If no port is free, the service keeps its port and is reported as unresolved.
- Snippets. The
.envsnippet has oneNAME_PORTline per service. The Procfile skeleton follows the<process type>: <command>format from Heroku’s documentation. It runsexport PORT=3000first, because a shell expands$PORTbefore an inlinePORT=3000prefix takes effect. The Compose snippet quotes every mapping, as the Compose reference advises, so YAML cannot read it as a base 60 number. It binds to127.0.0.1unless you turn that off, because a mapping without a host address publishes the port on every interface. The container port is the tool’s first default port when the name matches a known tool, and otherwise the same as the host port.
Service names become safe identifiers for the snippets: letters are reduced to ASCII, anything else becomes a hyphen, a leading digit gets an svc- prefix, and a repeated identifier gets a numeric suffix. A name with no ASCII letters or digits becomes service- and its line number.
Limitations
- It cannot see what is running on your machine. It plans from your list only, so a port held by something you did not list can still be busy. Check the planned ports with the port in use command tool before you start.
- Default ports come from a table, not from your project. If your project already moves a tool, or a name matches the wrong tool, the default will be wrong. Write the port yourself when you know it. A port the plan moves a service to is not checked against the defaults of other tools.
- The 1024 boundary and the two ephemeral ranges are the common defaults. A system can change them and the planner cannot read your settings. On Linux, check ip_unprivileged_port_start and ip_local_port_range.
- It treats every service as TCP and does not know which address a service binds to, so two services on one port but different addresses are still reported as a conflict.
- It edits nothing and starts nothing. The snippets are starting points: replace your-command, check each container port, and note that the Procfile skeleton assumes a POSIX shell.
Practical applications
Use it whenever several local services compete for ports.
- Starting a new project. List the web app, API, database, cache, and worker before you write any config, and fix the ports in one place.
- Monorepos. Two packages that both default to the same dev port are the classic case. The plan shows which one moves and where to.
- Onboarding. Paste the stack from the README and give a new teammate a list of ports that will not clash.
- Docker Compose next to native servers. Check that a published container port does not collide with a dev server on the host.
- Several projects at once. Put the services of every project in one list to see where they overlap.
- Reviewing a port choice. Check that a port you picked is not in a range the operating system uses for outgoing connections.
Common mistakes
- Moving a service in the plan and not in the tool. The plan is a proposal. Set the new port in the tool’s configuration, for example with its port flag, or the service still starts on its old port.
- Leaving the port off a name the planner does not know. A custom name such as api or worker has no default, so it is reported as needing a port. Write api:4000.
- Choosing a port from 32768 up for a long-running service. The operating system can give that port to an outgoing connection, so the service may fail to bind. Stay between 1024 and 32767 when you can.
- Assuming a planned port is free. The planner only knows your list, and anything already running can hold a port. Check before you start.
- Putting PORT in .env for Next.js. The Next.js docs say PORT cannot be set in .env, because the server starts before that file is read. Pass -p to next dev, or export PORT in the shell.
Frequently asked questions
Which ports are safe to use for local development?
Ports from 1024 to 32767 avoid the privileged and ephemeral range warnings this tool gives. They are above the privileged range, below the default Linux ephemeral range, and outside the IANA dynamic range. Two services can still clash, and many of these ports are the default of some tool, which the planner reports as information. Your system may use different ranges.
Why are ports below 1024 a problem?
On many Unix-like systems a process needs elevated privileges to listen on a port below 1024. IANA calls them the System Ports, and RFC 7605 notes that some systems require privileged access. On Linux the boundary is the net.ipv4.ip_unprivileged_port_start setting, which is 1024 by default, so your system may differ. For local work, run the app on a high port such as 8080 and put a proxy in front of it if you need port 80.
What is the ephemeral port range and why does it matter?
When a program opens an outgoing connection, the operating system picks a local port for it from a range. IANA lists 49152 to 65535 as the dynamic or private ports, and the Linux default is 32768 to 60999 (net.ipv4.ip_local_port_range). If your service listens on a port inside the range, the system may already have given that port to a connection, and the service can fail to bind. The planner warns about both ranges.
How does the planner decide which service moves?
An explicit port beats a port taken from a tool default, and when both are the same kind the earlier line wins. The services that lose move one at a time in line order, each to the first free port above its requested port, never below 1024 and never above 49151. To make a different service move, give the one you want to keep an explicit port, or put its line higher up.
Will my tools use the ports in the plan?
Only if you set them. The planner changes nothing on your machine. Vite, for example, tries the next available port when its port is taken, unless strictPort is on, so it can end up somewhere the plan did not expect. Next.js takes its port from the -p flag or the PORT environment variable. Set each port explicitly so the plan and the running services agree.
Can it tell me which process is using a port?
No. A web page cannot see the processes on your computer. Use the port in use command tool on this site to get the command for your operating system, run it in a terminal, and then add the services you find to your list.
Sources
- IANA: Service Name and Transport Protocol Port Number Registry
- RFC 7605: Recommendations on Using Assigned Transport Port Numbers
- Linux kernel documentation: IP sysctl, ip_local_port_range and ip_unprivileged_port_start
- Docker Compose: services reference, ports
- Heroku Dev Center: the Procfile
- Vite: server options, port and strictPort
- Next.js: next CLI, port and the PORT variable
- PostgreSQL documentation: connections and authentication, port
- Redis documentation: install Redis, default ports
Busy ports in Rig
When a start fails because a port is taken, Rig shows which process owns it when it can, and lets you reclaim the port or choose another. You do not need Rig to use this planner.