Run multiple dev servers locally: which approach fits
Pick the lightest tool that keeps four things under control: where each server’s output goes, what happens when one crashes, which port each one uses, and how you start the same set again tomorrow. Terminal tabs cover two servers. concurrently or a Procfile runner covers a repeatable set in one terminal. Docker Compose covers services that already run as containers. A process manager app such as Rig keeps logs, port handling, restarts, and health checks for your servers in one place. This guide compares them and says when each is the right choice, including when you do not need Rig.
The four problems to solve
Every approach below answers the same four questions, so you can use them to judge any other tool too.
- Output. Can you see which server printed a line, and read it after the terminal closes?
- Crashes. Do you notice that the worker died, and does anything bring it back?
- Ports. What happens when a server asks for a port another process already holds?
- Repeatability. Can a teammate, or you next Monday, start the whole set without remembering the order?
The example stack is a web frontend on port 3000, an API on port 4000, a background worker with no port, and a database that already runs in a container.
Option 1: several terminal tabs
Open a tab per server and run each command by hand. There is nothing to install, and for two servers in a short session it is the right choice.
It stops scaling for structural reasons. Each tab holds one process and its own scrollback, so there is no combined view. A crashed server stays down until you look at its tab, and a port clash appears only when a server starts. The set of servers exists only as open tabs, so it is written down nowhere you can reuse.
Option 2: a terminal multiplexer such as tmux
tmux gives one terminal several windows and panes. Detaching leaves the session and the programs in it running, so you can close the terminal and attach again later. A short script builds the same layout every time:
tmux new-session -d -s dev -n web 'npm run dev:web'
tmux new-window -t dev: -n api 'npm run dev:api'
tmux new-window -t dev: -n worker 'npm run dev:worker'
tmux attach -t devThis suits you if you already live in tmux and want each server one window away. Watch for one thing. By default a window closes when its command exits, so a crash can take the error message with it. Set remain-on-exit to keep the window open, and restart the command with respawn-window. A pane-died hook can run that command for you.
Option 3: concurrently or npm-run-all
concurrently runs several commands from one command, and npm-run-all runs several npm scripts. Both live in your package.json, so everyone who clones the repository gets the same start command after a normal npm install.
{
"scripts": {
"dev": "concurrently --names web,api,worker \"npm:dev:web\" \"npm:dev:api\" \"npm:dev:worker\""
}
}With npm-run-all the equivalent is run-p --print-label dev:web dev:api dev:worker. Each output line carries the script name. concurrently also has options to restart a failed command (--restart-tries) or to stop the rest when one exits (--kill-others), and run-p stops the others when a script exits with a non-zero code unless you pass --continue-on-error. The npm-run-all README warns that some tools stop colouring their output under --print-label. Both are command runners: which port each server takes is whatever its own script sets.
Option 4: Procfile runners
Foreman, Honcho, Overmind, and heroku local read a Procfile and start its lines:
web: npm run dev:web
api: npm run dev:api
worker: npm run dev:workerThe file is plain text and works outside Node. Runners differ in what happens when one process exits, so kill the worker once and watch. Overmind, for example, stops the others by default, and its -r option restarts the processes you name. Overmind is also the one that changes the workflow, because each process gets its own tmux window and you can attach to one with overmind connect or restart only that one with overmind restart. The Procfile guide covers the syntax, the process types, and each runner’s commands. Foreman sets PORT for each process, starting at the base port you give with -p and rising by 100 for each process line, and your app still has to read it.
Option 5: Docker Compose
Compose is the right tool when the services already exist as containers, such as a database, a cache, or a message broker, or when every developer needs the same environment. One file describes the services, and docker compose up starts them and streams their combined output. docker compose logs shows that output later, with each line labelled by the service it came from.
services:
api:
build: ./api
ports:
- "4000:4000"
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "healthcheck.js"]
interval: 10s
timeout: 3s
retries: 5
worker:
build: ./api
command: node src/worker.js
depends_on:
api:
condition: service_healthyHere unless-stopped restarts the container whatever its exit code, but not after you stop it, and the health check gates the worker: Compose waits until api is healthy before it starts worker. The left side of "4000:4000" is the host port, so it can still clash with a process that is not in the file.
The cost is that your code runs inside a container. Each service you edit needs an image or Dockerfile, and edits reach the container through a bind mount or through Compose Watch. Docker’s documentation says Watch sync rules can replace bind mounts for many development uses. A practical split is databases in Compose and the code you are editing outside it.
Option 6: a process manager app such as Rig
A process manager app watches the servers instead of only starting them. This is what Rig does, from its own behaviour:
- Detection. Rig detects
package.jsonscripts, a Procfile, and a Compose file in a project, and offers each as a script you can start. For a Compose file it runsdocker compose up. Starting a group uses health-aware ordering. - Logs. One log per project. Rig keeps the last 300,000 characters in memory and writes the log to disk, rotating at 2 MB into a single
.log.1file. - Ports.
who_owns_portshows the process holding a port and suggests the first free port in the 50 above the one you asked for.kill_portsends SIGTERM and then SIGKILL if the port is still bound. When a port is taken,start_projectcan kill the owner, switch to a suggested port, or report the conflict. Vite starts with--port N --strictPort. - Auto-restart. After a crash or failed health checks, Rig can restart a process with exponential backoff, capped at 30 seconds, and by default it pauses after 5 attempts. It frees an
EADDRINUSEport before it restarts. - Health polling. Every 5 seconds Rig sends a GET request with a 4 second timeout to the first of the project’s URLs that answers. A status from 200 to 499 counts as healthy, and a 5xx or an error counts as unhealthy.
Rig is not the right choice in every case. Its health check is an HTTP request. When no URL answers, as with a worker that serves no HTTP, the status is unknown, and a 404 counts as healthy, so the check shows that something answered, not that the page is right. Each Procfile process type becomes a script you select, with extra scripts running alongside it, which is a different model from a runner that starts every line in one command. If your whole stack is containers, Compose already handles it, and Rig runs it as docker compose up rather than replacing it. Check the download page for supported platforms before you plan around it.
Decision table
| Approach | Setup effort | Logs | Crashes | Ports | Main limit |
|---|---|---|---|---|---|
| Terminal tabs | None. One tab per server. | One scrollback per tab, no combined view. | Nothing restarts a server. You notice by looking. | You choose each port. A clash shows when the server starts. | The set of servers lives in open tabs. |
| tmux | Install tmux and keep a script for the layout. | One window per server, and the session survives a detach. | A window closes on exit unless remain-on-exit is set. respawn-window restarts it. | You choose each port. | Terminal only. You maintain the layout script. |
| concurrently or npm-run-all | One dev dependency and one script. | One stream, with a label on each line. | concurrently can restart a failed command or stop the others. run-p stops the others on a non-zero exit. | Set in each script. | Runs commands. Ports and readiness are left to your scripts. |
| Procfile runners | A Procfile and a runner install. | Depends on the runner. Overmind gives each process its own tmux window. | Differs by runner. Overmind stops the others by default. | Foreman sets PORT, and the app must read it. | Needs a runner install, and Overmind needs tmux. |
| Docker Compose | A compose file plus an image or Dockerfile per service. | docker compose logs labels each line by service. | A restart policy per service, with health checks gating depends_on. | ports maps a host port to a container port. The host side can still clash. | Your code runs in a container, so live editing needs a bind mount or Compose Watch. |
| Rig | Install the app. It detects scripts, a Procfile, and a Compose file. | One log per project, kept in memory and on disk. | Restart with backoff after a crash or failed health check, paused after 5 attempts by default. | who_owns_port, kill_port, and a suggested free port. | Health checks are HTTP only, and a Procfile process type is one script you select. |
How to choose
- Two servers, a short session: use tabs.
- A team that wants
npm run devto work after a normalnpm install: useconcurrentlyornpm-run-all. - An app that deploys with a Procfile: use a Procfile runner, so the file is the single list.
- A terminal-first workflow where you want detach and attach: use tmux, or Overmind if you already have a Procfile.
- Databases, caches, and brokers, or identical environments for every developer: use Compose for those services.
- Several servers that you restart daily, ports that collide, or crashes you want caught: use a process manager app.
These combine. A practical setup is Compose for the database, a Procfile for the app processes, and a process manager above both.
The natural next step
If the tab problem is the one you recognise, the comparison with terminal tabs sets out what changes, the features page lists the process controls, and you can download Rig to try it on a project you already run. If the port problem is the sharper one, find and free a port works without any of these tools.
Frequently asked questions
What is the simplest way to run a frontend and an API at the same time?
Two terminal tabs, or one concurrently script if you want a single command. Add structure only when you start missing crashes, hitting port clashes, or forgetting the start order.
Should I run my dev servers in Docker Compose?
Compose fits services you already run as containers, such as databases, caches, and brokers. For code you edit all day, running it outside Compose avoids the bind-mount or Compose Watch setup, and it can still reach the containers through their published ports.
How do I stop two dev servers fighting over a port?
Give each server its own fixed port in its config, and check who owns a port before you start. The guide to finding and freeing a port has the commands for each operating system. Rig’s who_owns_port looks the owner up with lsof -nP -iTCP:<port> -sTCP:LISTEN (netstat and tasklist on Windows), and kill_port frees the port.
Can one command start everything and show which server printed each line?
Yes. concurrently --names and run-p --print-label label each line with the script name, and docker compose logs labels each line with the service. Overmind lets you attach to one process by name. Check how your Procfile runner formats its output.
Does Rig replace Docker Compose?
No. When a project has a compose file, Rig runs docker compose up as one of its scripts. Compose remains the place to define container services.
When is a process manager worth installing?
When the manual approach has a visible cost: you restart the same servers daily, ports collide, or a crash goes unnoticed. Until then, a script or a Procfile is enough.
Sources
- tmux wiki: Getting started
- tmux(1) manual page, OpenBSD
- concurrently README
- npm-run-all: run-p
- npm-run-all: npm-run-all command
- Heroku Dev Center: Running Apps Locally
- foreman(1): manage Procfile-based applications
- Honcho: manage Procfile-based applications
- Overmind: process manager for Procfile-based applications and tmux
- Docker: define services in Docker Compose
- Docker: control startup and shutdown order in Compose
- Docker: docker compose logs
- Docker: docker compose up
- Docker: use Compose Watch
- Rig documentation: Procfile projects (Rig behaviour confirmed in Rig’s source code)
- Rig documentation: Health-aware start order
Run your whole stack from one window
Rig is a Mac app that starts, stops, and watches the processes in your project, with logs and ports in one place. Local use is free, and your source code stays on your Mac.