# Port allocations for internal services Extends `generic.md` §9 (firewalld) and §3 (Binaries and Runtime). Every service that listens on a TCP port needs a port *number*, and on a fleet where many services share hosts — and where a runner, a proxy, and an app can land on the same box — a careless choice collides. This doc is how we pick numbers that don't, and the **registry** of what's already assigned. Decision rule (the whole thing in one line): > **Pick an uncommon port in the IANA *registered* range (1024–49151), never a crowded > alt-HTTP port and never the ephemeral range, and record it in the registry below.** The registry — not cleverness in the number — is what actually prevents collisions. Check it before you assign; add your service when you do. --- ## 1. Why the default ports bite The ports frameworks and tools reach for by default form a dense cluster that half the software world also uses: `3000` `3001` `4000` `5000` `5173` `8000` `8008` `8080` **`8081`** `8443` `9000` `9090` … On a dedicated single-service host these are fine. On our fleet they are not: hosts run gongfoo runners, nginx, sometimes more than one app, plus whatever a container or a dev shell brought up. Two things pick `8080`/`8081` and one fails to bind (`EADDRINUSE`) — or, worse, *doesn't* fail: it binds a different interface, the health probe hits the **other** service on that port, and you debug a green check that lies. Avoid the whole cluster. ## 2. The three ranges | Range | IANA class | Use for a listener? | | --- | --- | --- | | `0–1023` | well-known / system | **No.** Requires privilege; reserved for standard services. TLS terminates at nginx (`:443`), so apps don't need these. | | `1024–49151` | **registered / user** | **Yes.** This is where our services live. | | `49152–65535` | dynamic / ephemeral | **No.** The kernel hands these out as *source* ports for outbound connections. A fixed listener here can collide with an ephemeral allocation and fail intermittently — the worst kind of bug. | So: **registered range, and specifically a sparse part of it.** The crowded cluster (§1) sits at the bottom of the registered range; climb out of it. A good default band is **`20000–29999`** — clear of the alt-HTTP mess below and the ephemeral range above, and sparsely used. `30000–39999` is equally fine (cortex already lives there). ## 3. Picking the number 1. **Derive, don't guess.** Human-chosen ports cluster (everyone likes round numbers and `x080`). Map the service name deterministically into the band instead: ```sh name=newsfeed python3 -c "import hashlib;print(20000 + int(hashlib.sha256('$name'.encode()).hexdigest(),16) % 10000)" # newsfeed -> 22672 ``` This is reproducible (anyone can re-derive it) and spreads services across the band. 2. **Check the registry (§5).** If the derived number is taken, bump by 1 until free, or pick a memorable nearby number — then it's a deliberate, recorded choice. 3. **Record it** in the registry with the same change that introduces the service. 4. A memorable hand-picked number in the band is equally valid (`neuron` = `13131`, `cortex` = `31313`). The rule is *uncommon + registered + recorded*, not *hashed*. ## 4. Consequences elsewhere - **firewalld** (`generic.md` §9): ship the port in the app's named service XML. The registry and the XML must agree. - **SELinux** (`generic.md` §10): a non-standard port the daemon binds must be labelled, or the bind is denied. `semanage port -a -t http_port_t -p tcp ` (guard with `semanage port -l | grep` so re-runs are no-ops). Do this in the host-prep step (`infra-setup.sh`), before the first service start. - **Binding scope**: bind loopback (`127.0.0.1`) when nginx is on the same host, or the mesh-routable address when the proxy is on a different host (e.g. an API on one host fronted by the site's edge proxy on another). firewalld's named service bounds who may reach it; the registry bounds what else may bind it. - **Reverse proxy** (`reverse-proxies.md`): the nginx `upstream` points at `:` — the same number as the registry and the service's bind config. ## 5. The registry Canonical list of internal-service port assignments. **Append here when you allocate a port**; treat a duplicate as a bug. (Seeded from the services documented across this repo; extend as you encounter others — this table is authoritative going forward.) | Port | Service | Host(s) | Notes | | --- | --- | --- | --- | | `5432` | PostgreSQL | `magrathea`, `frankie` (standby) | standard well-known assignment; mTLS | | `13131` | helexa **neuron** | each GPU host | OpenAI-compatible inference API | | `13132` | helexa-bench read API | `bob` | benchmark telemetry | | `18443` | gongfoo **controller** | `golgafrinchans` | agent-facing RPC (mTLS) | | `22672` | **newsfeed-api** | `slartibartfast` | REST/JSON; fronted by oolon | | `28443` | gongfoo **agent** | each runner host | controller-facing RPC (mTLS) | | `31313` | helexa **cortex** API | `hanzalova` | unified OpenAI/Anthropic gateway | | `31314` | helexa **cortex** metrics | `hanzalova` | Prometheus | `443` (nginx edge, every proxy host) and the ephemeral range are intentionally omitted — they aren't allocations. ## 6. Checklist for a new listening service 1. Derive a candidate port in `20000–29999` from the service name (§3). 2. Check it's free in the registry (§5); bump if taken. 3. Record it in the registry in the same change. 4. Put the number in exactly these places, all agreeing: the service's bind config, its firewalld service XML (§9), its SELinux `semanage port` label (§10, in host prep), and any nginx `upstream` (`reverse-proxies.md`).