Guidance for picking a listening port that won't collide on a shared-host fleet: use the registered range (1024-49151), avoid the crowded alt-HTTP cluster (3000/5000/8000/8080/8081/8443/9000...) and never the ephemeral range (49152+, kernel source ports), derive from the service name into a sparse band, and — the actual anti-collision mechanism — record every allocation in a fleet registry. Seeds the registry with the services documented across this repo (pg, neuron, bench, gongfoo controller/agent, cortex) plus newsfeed-api. Motivated by newsfeed-api having been put on 8081 (the alt-HTTP port). Linked from generic.md §9. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fKZzDpvjiJ9eYbPGgJvUP
5.6 KiB
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
-
Derive, don't guess. Human-chosen ports cluster (everyone likes round numbers and
x080). Map the service name deterministically into the band instead:name=newsfeed python3 -c "import hashlib;print(20000 + int(hashlib.sha256('$name'.encode()).hexdigest(),16) % 10000)" # newsfeed -> 22672This is reproducible (anyone can re-derive it) and spreads services across the band.
-
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.
-
Record it in the registry with the same change that introduces the service.
-
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 <port>(guard withsemanage port -l | grepso 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 nginxupstreampoints at<host>:<port>— 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
- Derive a candidate port in
20000–29999from the service name (§3). - Check it's free in the registry (§5); bump if taken.
- Record it in the registry in the same change.
- Put the number in exactly these places, all agreeing: the service's bind config, its
firewalld service XML (§9), its SELinux
semanage portlabel (§10, in host prep), and any nginxupstream(reverse-proxies.md).