Nothing binds a port without asking.
One place that decides which local port a service runs on. Services register under a name, say what they are, and get a port back. The same name keeps the same port across restarts, so a backend never wakes up on the port its frontend grabbed while it was down.
$ warden run -- npm run dev
shop-api -> 8000
VITE ready, listening on http://localhost:8000
Nothing to change in the project: the port arrives as PORT, is held while the
process runs, and goes back when it exits.
Install
uv tool install warden-ports # the `warden` command, anywhere
uvx --from warden-ports warden ports # or just once, without installing
The distribution is called warden-ports because warden on PyPI belongs to
something else. The command it installs is warden either way.
Installation covers pipx,
pip and a checkout.
What it does
| Hands out ports | A name asks, a port comes back, and it stays that port. One machine |
| Shows what is listening | warden ports reads the machine, not the registry — no server needed. Ports and processes |
| Remembers | warden history 8000 answers what had this port last week. |
| Says it as it happens | A live event stream, and webhooks for Discord, Slack, Teams or your own endpoint. Events and webhooks |
| Writes your proxy config | warden export caddy turns the registry into a Caddyfile. Reverse proxy |
| Reads a project file | warden.project.toml says which ports a project needs; warden apply makes it true. Projects |
| Decides what may cross | A firewall over nftables, iptables, pf or Windows, where every change undoes itself unless you confirm it. Firewall |
| Spans machines | One hub, many wardens, one view. Cluster |
| Answers for itself | warden doctor replaces four commands and a guess. Troubleshooting |
A tour
Ask for a port
$ warden register shop-api --kind backend --project shop
8000
$ warden ls
SERVICE KIND PROJECT ADDRESS PID
shop-api backend shop 127.0.0.1:8000 14204
Ask again tomorrow and it is still 8000. --preferred-port wishes for one,
--require-port insists and fails if it cannot have it, and
warden register stack --count 4 takes four at once — all of them or none.
See what is actually listening
$ warden ports --port 3000
PORT PROTO PROCESS PID USER ADDRESS WARDEN
3000 tcp node.exe 25084 dev 0.0.0.0 -
$ warden kill 3000
Stop node.exe (25084) on port 3000? [y/N]: y
Neither needs a warden running: they read the machine directly. The WARDEN column names the service whenever the port did come from the registry, so anything unmarked arrived some other way.
Let a project say what it needs
# warden.project.toml, beside the code
[project]
name = "shop"
[services.api]
kind = "backend"
[services.web]
kind = "frontend"
preferred_port = 8905
$ warden apply --env .env
SERVICE KIND ADDRESS WHAT
shop-api backend 127.0.0.1:8900 taken
shop-web frontend 127.0.0.1:8905 taken
wrote .env
Run it again and nothing moves — it renews rather than reshuffling a running project. A service that cannot get the port it insists on fails the whole run before anything is written. Projects has the whole file format.
Hear about it while it happens
$ warden events
09:41:02 registered shop-api 127.0.0.1:8600
09:41:44 released shop-api 127.0.0.1:8600
GET /v1/events is the same stream as server-sent events. A webhook sends them
somewhere else — discord, slack and teams post something the chat window
renders, and json posts the event as it is, signed with an HMAC over exactly
the bytes sent so the far end can tell it really came from you.
Nothing ever waits on a webhook: delivery happens after the change is
committed, off the request path, and warden doctor says when the last one did
not arrive — because from the inside, a webhook failing all day looks exactly
like a quiet day.
Events and webhooks
has where to get an address, and how to check the signature.
Write the proxy config nobody wants to write by hand
$ warden export caddy --domain example.com
# Written by `warden export` from the warden on hub. Regenerate it; do not edit it.
shop-api.example.com {
reverse_proxy 127.0.0.1:8000
}
caddy, nginx and traefik. --all takes the whole fleet and points each
service at the machine it actually runs on. It prints and stops: nothing is
written in place, and no proxy is reloaded.
Reverse proxy has the
rest.
Decide what may cross
warden also holds the machine's firewall, in whatever the machine actually uses — nftables, iptables, pf on macOS and the BSDs, Windows Defender Firewall:
$ warden firewall allow ssh --from 10.0.0.0/8
$ warden firewall apply
12 rules applied
rolling back in 60s unless you run `warden firewall confirm`
Every change undoes itself unless you confirm it. A snapshot is taken first, the rollback is armed second, and the change applied third. The watchdog runs detached, so it outlives the ssh session that armed it — a rule that locks you out is a minute of waiting rather than a drive to the machine.
warden firewall adopt takes over from ufw or firewalld: it reads their rules,
shows them, applies them as its own, and turns the other one off only once you
confirm. Until then it is still enabled, so rolling back returns the machine
exactly as it was. Anything it cannot translate is named before you decide —
a rule quietly lost here is a door quietly left open.
And because the registry is in the same program, a rule can belong to a service rather than to a number:
$ warden firewall open shop-api # the port the registry handed out
$ warden firewall dev-mode --for 2 # the whole pool, for the afternoon
Both close themselves: the first when the service's lease lapses, the second
when its clock runs out. Neither can reach a port warden does not hand out —
22 and 3389 are outside the pool, and stay there.
Firewall has the whole of it, including the bounds a rule from the registry can never cross.
Find out why it is not working
$ warden doctor
ok warden 0.2.0 answering at http://127.0.0.1:7010, role hub
ok settings from ~/.config/warden/warden.toml
ok pool 8000-8999, 3 held, 996 free
warn 1 of 3 registrations held by something that is gone - `warden reap`
ok events to https://discord.com/... as discord, 12 delivered
One command instead of four and a guess. It exits non-zero only on fail, so a
warning about an unset token does not make a health check call the machine down.
Set it up once
warden setup
One screen: which ports to hand out, whether other machines may reach it, which
hub it reports to, where events go, and ctrl+t to post a test event before
anything is saved. Questions that nothing has earned stay hidden — no token
field until it listens beyond loopback, no webhook shape until events go
anywhere at all.
| Key | Action |
|---|---|
tab shift+tab |
Move between fields |
space |
Toggle a switch or a tick box |
enter |
Open a menu, or pick from it |
pgup pgdn |
Scroll without leaving the field you are in |
ctrl+t |
Post a test event to the address on screen |
ctrl+s ctrl+q |
Save · leave without writing |
It fits an 80 by 24 terminal, which is the size an ssh session usually opens at.
Without a terminal — a script piping answers in, a job on a build machine — the
same questions come one at a time, and warden setup --plain asks for that on
purpose. Configuration
has every setting there is.
More than one machine
# on the hub
warden serve
# on each other machine
WARDEN_UPSTREAM=http://hub:7010 WARDEN_ADVERTISE=http://build-01:7010 warden serve
Each warden still hands out its own ports and never waits on the hub. The hub
adds one view over all of them: warden ls --all, warden pool --all,
warden tui --all, and a node that did not answer is named rather than quietly
left out. Cluster has the
tokens, the trust rules and what happens when a machine goes quiet.
From your own code
from warden import reserve
with reserve("shop-api", kind="backend") as port:
serve(port) # held while the block runs, released after
PORT=$(warden register shop-api --kind backend) # or from any shell
Python client has the client, the leases and the error types. HTTP API has every endpoint, for everything that is not Python.
Documentation
The wiki is the long form. This page is the tour.
| Page | For |
|---|---|
| Installation | Getting the warden command |
| One machine | The usual setup: a registry for your own projects |
| Ports and processes | Seeing and freeing ports, no server needed |
| Python client | Asking for a port from your own code |
| Projects | A warden.project.toml beside the code, and warden apply |
| Events and webhooks | Hearing about it as it happens, in chat or your own endpoint |
| Reverse proxy | Turning the registry into a Caddyfile, nginx or Traefik |
| Firewall | Deciding what may cross, and taking over from ufw or firewalld |
| Cluster | Several machines, one hub that knows them all |
| Docker | The image, a compose file, and what a container can see |
| Updates | Knowing a new version is out, and rolling it across a fleet |
| Configuration | Every setting there is |
| Command line | Every command and flag |
| HTTP API | Endpoints, payloads, status codes |
| Troubleshooting | When something does not behave |
Good to know
- The registry binds to loopback and has no token by default. Set
WARDEN_TOKENbefore binding it anywhere else. - The registry cannot open a port by itself. A rule that comes from it may only ever touch a port inside the pool, may only reach networks declared in advance, and closes when the service's lease does.
firewall_from_registryis off until you turn it on. WARDEN_ALLOW_KILLis off on purpose. Stopping processes over the API is a much bigger thing to hand out than a port number.warden killon the command line acts locally and never asks the API.- macOS will not let an unprivileged process enumerate sockets, so
warden ports, the dashboard's ports view,warden ls --holdersandwarden reapneedsudothere. Handing out ports does not. - On a Linux server, check that your account lingers. A systemd user unit
stops when your last session ends.
warden service installlooks and says so.
The palette, if you are drawing something that has to match
It lives in warden/theme.py, so the dashboard, the setup screen and the
command line never drift apart.
| Role | Colour | |
|---|---|---|
| Ground | #08100f |
sculk black |
| Surface | #0e1a1c |
panels and tables |
| Border | #1e3538 |
|
| Text | #d9e4e2 |
|
| Muted | #6d8687 |
labels, empty cells |
| Live | #2be0d6 |
ports, focus, the banner |
frontend |
#a87fe0 |
|
worker |
#e0b457 |
also a lease about to run out |
database |
#4fd98c |
also free capacity |
| Conflict | #e5544b |
expired leases, errors |
Development
git clone https://github.com/vxnsin/warden
cd warden
uv sync --all-groups
uv run pytest
uv run ruff check .
The suite runs on Linux, macOS and Windows across Python 3.11, 3.12 and 3.13, and the Docker image and its three-warden compose file are built and brought up on every change.
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file warden_ports-0.3.0.tar.gz.
File metadata
- Download URL: warden_ports-0.3.0.tar.gz
- Upload date:
- Size: 199.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
48a04f03efdcd80a10f0dd412d94b8e237086a361493a84b3829b1ae7d53753a
|
|
| MD5 |
bfeeefd5aa06c8896db5b66dd3a9d0de
|
|
| BLAKE2b-256 |
414b823e4ebbe96f661b97cc5526b6e83970ec79269b50ba3a4b6a3a2146a9cf
|
Provenance
The following attestation bundles were made for warden_ports-0.3.0.tar.gz:
Publisher:
release.yml on vxnsin/warden
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
warden_ports-0.3.0.tar.gz -
Subject digest:
48a04f03efdcd80a10f0dd412d94b8e237086a361493a84b3829b1ae7d53753a - Sigstore transparency entry: 2748041690
- Sigstore integration time:
-
Permalink:
vxnsin/warden@eebc9efd24638c62206af178c22fcbf3accf20c9 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/vxnsin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@eebc9efd24638c62206af178c22fcbf3accf20c9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file warden_ports-0.3.0-py3-none-any.whl.
File metadata
- Download URL: warden_ports-0.3.0-py3-none-any.whl
- Upload date:
- Size: 129.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8f2937ed3db3e8acaca4a424bc864777dd5cb1efa2f5cf0c42a932b767e2075b
|
|
| MD5 |
19fd9082ca9256e0a611baeb953fc3c1
|
|
| BLAKE2b-256 |
2a13a297ad7b96843d4f022eb2189034ee704f6a1157686866c08982eb0261b9
|
Provenance
The following attestation bundles were made for warden_ports-0.3.0-py3-none-any.whl:
Publisher:
release.yml on vxnsin/warden
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
warden_ports-0.3.0-py3-none-any.whl -
Subject digest:
8f2937ed3db3e8acaca4a424bc864777dd5cb1efa2f5cf0c42a932b767e2075b - Sigstore transparency entry: 2748041721
- Sigstore integration time:
-
Permalink:
vxnsin/warden@eebc9efd24638c62206af178c22fcbf3accf20c9 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/vxnsin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@eebc9efd24638c62206af178c22fcbf3accf20c9 -
Trigger Event:
push
-
Statement type: