# Stable Port Assignments for Any Dev Server (AI Agent-Friendly)

**TL;DR** — Give your AI coding agent (GitHub Copilot, Claude Code, OpenAI Codex, Gemini CLI) a `justfile` that launches *any* local development server — Phoenix, Node.js, Python, Go, Ruby, or anything else that listens on a port — with stable, collision-free port assignments using [`phx-port`](https://github.com/chgeuer/phx-port). The agent can start, stop, and open your app without guessing ports or stepping on other running projects.

> Despite the name, `phx-port` is **not** Phoenix-specific. It works with any project that needs a local port.

This article is self-contained: point your coding agent at it and say *"Adopt the pattern in `https://cookbook.geuer-pollmann.de/elixir-beam/phx-port-and-justfile-for-ai-agents.md` for my project."*

***

## Table of Contents

1. [The Problem](#the-problem)
2. [The Pattern](#the-pattern)
3. [Step 1: Install `phx-port`](#step-1-install-phx-port)
4. [Step 2: Add the `justfile`](#step-2-add-the-justfile)
5. [Step 3: Update `.gitignore`](#step-3-update-gitignore)
6. [Step 4: Add Project Instructions for Your Agent](#step-4-add-project-instructions-for-your-agent)
7. [How It All Fits Together](#how-it-all-fits-together)
8. [Elixir / BEAM Bonus: Combining with BEAM Introspection](#elixir--beam-bonus-combining-with-beam-introspection)

***

## The Problem

When you work on multiple web projects, they tend to default to the same port — 4000 for Phoenix, 3000 for Node/Rails, 8000 for Django, 8080 for Go. You end up either:

* Killing the old server before starting the new one
* Manually remembering which port you assigned to which project
* Passing `PORT=4007` and hoping you haven't already used 4007 somewhere else

AI coding agents make this worse. When an agent needs to start your dev server to validate a change, it doesn't know which port to use. If another project is already running on that port, the server fails to bind and the agent wastes time debugging a port conflict instead of doing real work.

**What we want**: every project gets a stable, unique port — automatically — and the agent has a single command to start the server, open the browser, or stop the process.

## The Pattern

Two pieces work together:

1. [**`phx-port`**](https://github.com/chgeuer/phx-port) — a small Rust CLI that maintains a TOML registry (`~/.config/phx-ports.toml`) mapping project directories to port numbers. Each project gets a unique port, allocated once and reused forever. Despite its name, it is **framework-agnostic** — it works with Phoenix, Express, Django, Rails, Go, or any server that reads a `PORT` environment variable. Port 4000 is kept free for ad-hoc use.
2. **A `justfile`** — a [`just`](https://github.com/casey/just) command runner file in the project root that wires together `phx-port` and your project's start command. For Elixir/BEAM projects, it can additionally integrate distributed Erlang (`--sname` / `--cookie`) and the [BEAM introspection script](https://cookbook.geuer-pollmann.de/elixir-beam/beam-introspection-for-ai-agents) (`scripts/dev_node.sh`).

The agent (or you) runs `just start` and gets a server on a known, stable port — every time, on every machine.

### How `phx-port` works

`phx-port` auto-detects behavior based on context:

| Context                                               | Behavior                                                                            |
| ----------------------------------------------------- | ----------------------------------------------------------------------------------- |
| **Piped** (e.g. `PORT=$(phx-port)`)                   | Prints just the port number. Auto-registers the current directory if not yet known. |
| **Interactive** (run in a terminal with no arguments) | Shows help text. Never auto-registers accidentally.                                 |
| `phx-port list`                                       | Shows all registered projects as a directory tree with clickable URLs.              |
| `phx-port open`                                       | Opens the default browser at `http://localhost:<port>` for the current project.     |
| `phx-port register`                                   | Explicitly registers the current directory for a new port.                          |
| `phx-port register debug`                             | Registers a named port role (e.g., for a debug port, metrics endpoint, etc.).       |

Projects can have multiple named port roles:

```bash
# Main server port (works with any framework)
PORT=$(phx-port) npm start           # Node.js
PORT=$(phx-port) mix phx.server      # Phoenix
PORT=$(phx-port) python manage.py runserver 0.0.0.0:$PORT  # Django
PORT=$(phx-port) go run .            # Go (if your app reads $PORT)

# Main + debug port
PORT=$(phx-port) PORT_DEBUG=$(phx-port debug) mix phx.server
```

The registry looks like this:

```toml
[ports."/home/user/projects/my_app"]
main = 4001

[ports."/home/user/projects/api_gateway"]
main = 4002
debug = 4003
```

***

## Step 1: Install `phx-port`

```bash
cargo install --git https://github.com/chgeuer/phx-port
```

Or build from source:

```bash
git clone https://github.com/chgeuer/phx-port
cd phx-port
cargo build --release
cp target/release/phx-port ~/.local/bin/   # or anywhere on your PATH
```

Verify it works:

```bash
phx-port --version
```

***

## Step 2: Add the `justfile`

Create a `justfile` in your project root. This is the single entry point for starting, stopping, and managing the server. Below are examples for different stacks.

### Generic `justfile` (Node.js, Python, Go, etc.)

This works for any server that reads the `PORT` environment variable:

```just
# Start the server (visible output, logs to run.log)
start:
    #!/usr/bin/env bash
    export PORT="${PORT:-$(phx-port)}"
    echo "Starting on port $PORT..."
    exec your-start-command 2>&1 | tee run.log

# Start the server in background (logs to run.log only)
start-bg:
    #!/usr/bin/env bash
    export PORT="${PORT:-$(phx-port)}"
    your-start-command > run.log 2>&1 &
    echo "Started in background on port $PORT (PID $!). Logs in run.log"

# Open the app in a browser
open:
    phx-port open

# Show the assigned port
port:
    @phx-port
```

Replace `your-start-command` with what your project needs — `npm start`, `python manage.py runserver 0.0.0.0:$PORT`, `go run .`, `bundle exec rails server -p $PORT`, etc.

### Elixir / Phoenix `justfile`

For Phoenix or other BEAM projects, the `justfile` can additionally wire in distributed Erlang for live introspection:

```just
# Start the Phoenix server (visible output, logs to run.log)
start:
    #!/usr/bin/env bash
    SNAME="$(basename "$(pwd)")"
    export PORT="${PORT:-$(phx-port)}"
    export ELIXIR_ERL_OPTIONS="-sname $SNAME -setcookie devcookie"
    exec mix phx.server 2>&1 | tee run.log

# Start the Phoenix server in background (logs to run.log only)
start-bg:
    #!/usr/bin/env bash
    SNAME="$(basename "$(pwd)")"
    export PORT="${PORT:-$(phx-port)}"
    export ELIXIR_ERL_OPTIONS="-sname $SNAME -setcookie devcookie"
    exec mix phx.server > run.log 2>&1

# Open the app in a browser (starts the server if not running)
open:
    #!/usr/bin/env bash
    SNAME="$(basename "$(pwd)")"
    if ! scripts/dev_node.sh status > /dev/null 2>&1; then
        echo "Node $SNAME not running, starting in background..."
        export PORT="${PORT:-$(phx-port)}"
        export ELIXIR_ERL_OPTIONS="-sname $SNAME -setcookie devcookie"
        mix phx.server > run.log 2>&1 &
        scripts/dev_node.sh await
    fi
    phx-port open

# Stop the running BEAM node gracefully
stop:
    scripts/dev_node.sh rpc "System.stop()"

# Check if the BEAM node is running
status:
    scripts/dev_node.sh status

# Execute an expression on the running BEAM node
rpc EXPR:
    scripts/dev_node.sh rpc "{{EXPR}}"
```

### What each recipe does

| Recipe              | Description                                                                                                                                                                                                            |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `just start`        | Starts the server in the foreground. Output goes to the terminal **and** `run.log`.                                                                                                                                    |
| `just start-bg`     | Same, but runs silently in the background. All output goes to `run.log`.                                                                                                                                               |
| `just open`         | Opens the app in your browser. For the Elixir version, starts the server in the background first if needed.                                                                                                            |
| `just stop`         | *(Elixir)* Gracefully shuts down the running BEAM node via `System.stop()`.                                                                                                                                            |
| `just status`       | *(Elixir)* Checks whether the BEAM node is registered with `epmd`.                                                                                                                                                     |
| `just rpc '<expr>'` | *(Elixir)* Evaluates an Elixir expression on the running node (requires `scripts/dev_node.sh` from the [BEAM introspection pattern](https://cookbook.geuer-pollmann.de/elixir-beam/beam-introspection-for-ai-agents)). |

### Key design decisions

* **`PORT` respects overrides** — `${PORT:-$(phx-port)}` means you can still do `PORT=9999 just start` if needed, but the default is always the stable `phx-port` assignment.
* **`exec` replaces the shell** — the server process takes over the shell's PID, so signals (Ctrl+C) go directly to it.
* *(Elixir-specific)* **`--sname` is derived from the directory name** — no configuration needed. The project directory `my_app` becomes node `my_app@hostname`.
* *(Elixir-specific)* **`--cookie devcookie`** — a shared development cookie so `dev_node.sh` can connect for introspection.

***

## Step 3: Update `.gitignore`

Add runtime artifacts that shouldn't be committed:

```gitignore
# Runtime files
run.log
.dev_node.log
.dev_node.pid
```

***

## Step 4: Add Project Instructions for Your Agent

Tell your AI coding agent about the `justfile` so it knows how to start and manage the server. Add this to your project's agent instructions file.

### GitHub Copilot — `AGENTS.md` or `.github/copilot-instructions.md`

```markdown
### Server management

Use `just` recipes to manage the development server:

- `just start` — start the server (foreground, visible output)
- `just start-bg` — start the server in the background
- `just open` — open the app in the browser
- `just port` — show the assigned port number

The server uses `phx-port` for stable port assignment. Never hardcode port numbers.
To make HTTP requests against the running server, use `http://localhost:$(phx-port)`.
```

> For Elixir/BEAM projects, add `just stop`, `just status`, and `just rpc '<expression>'` to the list above — see the Elixir `justfile` variant.

### Claude Code — `CLAUDE.md`

```markdown
## Server management

Use `just` recipes to manage the development server:

- `just start` — start the server (foreground, visible output)
- `just start-bg` — start the server in the background
- `just open` — open the app in the browser
- `just port` — show the assigned port number

The server uses `phx-port` for stable port assignment. Never hardcode port numbers.
To make HTTP requests against the running server, use `http://localhost:$(phx-port)`.
```

### OpenAI Codex / Gemini CLI — `AGENTS.md`

Same content as the Copilot section above. Both tools read `AGENTS.md` at the project root.

***

## How It All Fits Together

Here's a typical workflow, whether it's you or an AI agent:

```bash
# First time — phx-port auto-registers the project
~/projects/my_app $ just start
Registered /home/user/projects/my_app → port 4001    # ← stderr, first time only
Starting on port 4001...
Listening on http://localhost:4001

# Second time — port is already known, instant
~/projects/my_app $ just start
Starting on port 4001...
Listening on http://localhost:4001

# Meanwhile, in another project (Node.js, Python, anything) — no conflict
~/projects/api $ just start
Registered /home/user/projects/api → port 4002
Starting on port 4002...
Server running at http://localhost:4002

# Check what's registered across all your projects
$ phx-port list
/home/user/projects
├── my_app ...... http://localhost:4001
└── api ......... http://localhost:4002
```

An AI agent working on `my_app` simply runs `just start-bg`, waits for the server, and can then `curl http://localhost:$(phx-port)` to validate its changes — without worrying about port conflicts with the `api` project running in the background.

***

## Elixir / BEAM Bonus: Combining with BEAM Introspection

This pattern is designed to work together with the [BEAM Live Introspection](https://cookbook.geuer-pollmann.de/elixir-beam/beam-introspection-for-ai-agents) pattern. The `justfile` recipes (`stop`, `status`, `rpc`) delegate to `scripts/dev_node.sh`, which provides full runtime introspection capabilities.

To set up both patterns together:

1. Follow this article to add `phx-port` and the `justfile`
2. Follow the [BEAM introspection article](https://cookbook.geuer-pollmann.de/elixir-beam/beam-introspection-for-ai-agents) to add `scripts/dev_node.sh` and the introspection skill

The `justfile` becomes the high-level interface ("start my server"), while `dev_node.sh` provides the low-level distributed Erlang plumbing ("connect to the running node and evaluate this expression").

```
justfile                    ← Human / agent entry point
├── start / start-bg        ← Uses phx-port for port, --sname for node naming
├── open                    ← Auto-starts + opens browser via phx-port
├── stop                    ← Delegates to dev_node.sh rpc "System.stop()"
├── status                  ← Delegates to dev_node.sh status
└── rpc                     ← Delegates to dev_node.sh rpc

scripts/dev_node.sh         ← BEAM introspection engine
├── start / stop / status   ← Node lifecycle
├── await                   ← Wait for node to be connectable
├── rpc                     ← Execute expression on live node
└── eval_file               ← Evaluate .exs file on live node
```
