DEV Community

Cover image for Stop Manually Tracking Development Ports: I Built an Automated Solution
Ben Dechrai
Ben Dechrai

Posted on • Originally published at bendechr.ai on

Stop Manually Tracking Development Ports: I Built an Automated Solution

Announcing:

██████╗ ███████╗██╗   ██╗██████╗  ██████╗ ██████╗ ████████╗███████╗
██╔══██╗██╔════╝██║   ██║██╔══██╗██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝
██║  ██║█████╗  ██║   ██║██████╔╝██║   ██║██████╔╝   ██║   ███████╗
██║  ██║██╔══╝  ╚██╗ ██╔╝██╔═══╝ ██║   ██║██╔══██╗   ██║   ╚════██║
██████╔╝███████╗ ╚████╔╝ ██║     ╚██████╔╝██║  ██║   ██║   ███████║
╚═════╝ ╚══════╝  ╚═══╝  ╚═╝      ╚═════╝ ╚═╝  ╚═╝   ╚═╝   ╚══════╝

Port allocation manager for multi-project development

an npm package that automatically manages port allocation and service configuration across development projects and git worktrees. Install it globally with npm install -g devports or check it out on NPM and GitHub.

If you're working with multi-service development environments and git worktrees, you've probably run into the same problem I did. You've got your main branch with Postgres on 5432, your API server on 8000, your web app on 3000. You create a worktree for a feature, and suddenly you need different ports for every service to avoid conflicts, whether they're running in Docker containers, as local processes, or in development VMs.

I found myself maintaining a text file to track it all, and remembering to ignore configuration changes in .env files, docker-compose.yml, and various config files when committing work back. I accepted the manual labour on top of every feature I worked on as a cost of doing business. It wasn't ideal, but it also wasn't the end of the world.

Lately, though, I've been a getting a little more frustraed. Now that I'm making use if LLM-based coding assistance, and with the rise of AI-driven spec-driven development tooling like Kiro, Tessl, and Spec-Kit, it's not just me creating these local branches. An LLM is doing it, and its ability to update the configuration files to avoid clashed is about as successful as you'd imagine.

So I started building helper tools.

The Band-Aid Phase

My first attempt was pragmatic: a few scripts that the LLM could call. One that would keep track of used ports and return an unused one, another to search and replace port numbers in my .env files, and a third that would update service configurations—whether that meant Docker container names, database connection strings, or API endpoint URLs. Nothing sophisticated, just enough to unblock the LLM from doing its work autonomously.

It worked. Sort of. But it was a mess of shell scripts that was becoming harder to maintain. The scripts grew. They became more interconnected. I started adding configuration files to define port range blocks and reserved ports that I didn't want to clobber.

Building the Actual Solution

I was done patching around the edges. Building these scripts out over the course of a few different projects was a great way to iteratively work out what the problem actually looked like, and how I could solve it.

It was time to build something that unified all of this.

A single tool that could:

  • Track port allocations across worktrees and projects
  • Generate unique, deterministic service identifiers
  • Render configuration files from templates using those values
  • Work programmatically so an LLM agent could call it without intervention

The approach is straightforward. You define your configuration once - which services need ports, what the templates look like, how to name things. Then you create allocations: "this worktree gets these ports, this service name gets this identifier." devports manages the allocations, and when you need to render your configs, it does the substitution.

It's essentially a small system for tracking state and templating configuration. Nothing fancy, but it handles the messy part cleanly.

The Real Problem (With Real Port Numbers)

Let me show you exactly what I mean. Say you're working on an e-commerce platform with your main branch running:

  • Postgres database: port 5432
  • API server: port 3000
  • Frontend app: port 8080
  • Redis cache: port 6379

You create a worktree for a payment integration feature. Now you need different ports for that feature branch to avoid conflicts. Without devports, you'd manually pick:

  • Postgres database: port 5433 (hope it's not taken)
  • API server: port 3001 (hope it's not taken)
  • Frontend app: port 8081 (hope it's not taken)
  • Redis cache: port 6380 (hope it's not taken)

Then you'd update your .env file, docker-compose.yml, and remember not to commit those changes. Create another worktree? Repeat the whole process, keeping track of what ports you've used across all branches.

With devports, it becomes:

devports allocate payment-feature api --type api
# ✅ Allocated port 3002 for payment-feature/api

devports allocate payment-feature database --type postgres
# ✅ Allocated port 5434 for payment-feature/database

devports allocate payment-feature frontend --type app
# ✅ Allocated port 5002 for payment-feature/frontend
Enter fullscreen mode Exit fullscreen mode

No guessing, no conflicts, no manual tracking. The tool knows what's available and allocates accordingly.

Why This Matters Now

The thing is, git worktrees already solve branch management well. But they expose this gap: how do you manage the local configuration that changes per worktree without manual intervention?

With LLM agents in the mix, that gap becomes a blocker. An agent can create a worktree and start working, but if it can't autonomously configure the ports and service endpoints, it either has to wait for you to intervene or it fails. That's not useful.

devports fills that gap. An agent can call it to allocate ports for a new worktree, render the configuration files for whatever services you're running, and keep working. You're not sitting there fixing things up after every run.

How It Actually Works

The workflow is dead simple. First, see what's currently allocated:

devports list
Enter fullscreen mode Exit fullscreen mode

This shows you a beautiful table of all your current port allocations, grouped by project:

🏗️  my-main-app
┌──────┬──────────────────┬──────────────────┬──────────────────────────┐
│ Port │ Service          │ Type             │ Allocated                │
├──────┼──────────────────┼──────────────────┼──────────────────────────┤
│ 3002 │ api              │ api              │ 11/19/2025, 04:28:49 PM  │
│ 5002 │ frontend         │ app              │ 11/19/2025, 04:28:55 PM  │
│ 5434 │ database         │ postgres         │ 11/19/2025, 04:28:53 PM  │
└──────┴──────────────────┴──────────────────┴──────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

When you need ports for a new feature or worktree:

# Allocate specific service types
devports allocate feature-auth api --type api
devports allocate feature-auth database --type postgres
devports allocate feature-auth cache --type redis
Enter fullscreen mode Exit fullscreen mode

Template-Based Configuration

The real power comes from devports' template system. Create configuration templates with .devports extensions:

# docker-compose.yml.devports (template file that gets rendered)
services:
  api:
    name: {devports:project}-api
    ports:
      - "${API_PORT}"
  database:
    name: {devports:project}-database
    ports:
      - "${DATABASE_PORT}"
  frontend:
    name: {devports:project}-frontend
    ports:
      - "${FRONTEND_PORT}"
Enter fullscreen mode Exit fullscreen mode
# .env.devports (template file that gets rendered)
API_PORT={devports:api:api}
DATABASE_PORT={devports:postgres:database}
FRONTEND_PORT={devports:app:frontend}
Enter fullscreen mode Exit fullscreen mode

Then get started in seconds:

# Set up your project
# - finds all *.devports files, allocates ports, renders configs
devports setup

# Renders both files with actual port numbers and unique project names
# Example output:
# ✅ Rendered docker-compose.yml.devports → docker-compose.yml
# ✅ Rendered .env.devports → .env
Enter fullscreen mode Exit fullscreen mode

For git worktrees, it's even better:

# Creates worktree, generates unique project name, re-renders all templates
devports worktree add ../feature-auth -b feature/auth

# Both templates get new port numbers and unique container names automatically
# No naming conflicts between worktrees!
Enter fullscreen mode Exit fullscreen mode

This approach means you define your configuration templates once, and devports handles the port allocation and rendering across all your worktrees.

When you're done with a worktree, clean up is one command:

devports worktree remove ../feature-auth  # Removes git worktree AND releases all ports
Enter fullscreen mode Exit fullscreen mode

From Scripts to Package

Once I had something that worked, I realised it was useful enough to package up properly. I turned it into an npm package so other people dealing with the same problem could just install it and use it without having to maintain their own collection of scripts.

The result is on GitHub and NPM. It's designed to be minimal - just enough to solve the actual problem without being opinionated about how you structure your projects - and will run globally or as a dependency in your project.

The LLM Angle

The scale changes when LLM agents are involved. Manually managing a few worktrees isn't terrible—you create one, set up the ports, work, clean up. But when agents are creating and destroying branches frequently, the manual steps become a bottleneck.

LLM agents handle shell commands and JSON responses well, but they're not good at tracking state between conversations. devports manages the state, so the agent can just call commands.

If you try devports out, I'd love to hear how it works for you. Does it solve your port management problems? Are there features missing that would make it more useful? Hit me up with feedback - it's a small tool but I want to make sure it actually solves the problem well.


Have a burning question or comment? Find me on LinkedIn or Bluesky. I'd love to hear from you.

Top comments (0)