# 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](/elixir-beam/beam-introspection-for-ai-agents.md) (`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](/elixir-beam/beam-introspection-for-ai-agents.md)). |

### 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](/elixir-beam/beam-introspection-for-ai-agents.md) 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](/elixir-beam/beam-introspection-for-ai-agents.md) 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
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://cookbook.geuer-pollmann.de/elixir-beam/phx-port-and-justfile-for-ai-agents.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
