-
Notifications
You must be signed in to change notification settings - Fork 0
Command line
warden introduce itself and list the commands
warden --version print the version
warden serve run the registry
warden tui open the dashboard
warden run start a program on a port warden picked
warden env claim a port and print it as environment
warden register claim a port and print it
warden get print the address of one service
warden ls list every registered service
warden release give a port back
warden reap release ports held by something that is gone
warden heartbeat push a lease out before it runs out
warden pool show how much of the pool is in use
warden history what happened to a port, or to a service
warden events follow what happens, as it happens
warden webhook where events are posted, and whether it works
warden apply register everything a project's warden.project.toml asks for
warden firewall rules for what may cross
warden export write the reverse-proxy configuration
warden ports show what is listening on this machine
warden kill stop whatever is holding a port
warden nodes list the wardens this one knows about
warden doctor check everything and say what is wrong
warden service start warden with the machine
warden setup answer a few questions, once
warden settings show what is in effect, and where it came from
warden update say whether a newer warden exists, and fetch it
Every command that talks to a warden takes --url / -u and --token, both of
which also read WARDEN_URL and WARDEN_TOKEN. Every command that prints a
table also takes --json.
Two flags run through the fleet, and they mean different things:
| Flag | On | Means |
|---|---|---|
--all |
ls, pool, ports, tui
|
Ask every warden, not just this one |
--node NAME |
register, release, heartbeat
|
Do it on that one warden, through this one |
warden kill reads the local machine and ignores --url entirely. warden ports
does too, unless --all is given, which needs a hub to ask.
--install-completion teaches your shell to complete every one of them; see
Installation.
Start a program on a port warden picked, hold the port while it runs, and give it back when it exits.
$ warden run -- npm run dev
shop-api -> 8000
VITE ready, listening on http://localhost:8000Everything after -- is the command. The child gets PORT, WARDEN_PORT,
WARDEN_HOST, WARDEN_ADDRESS and WARDEN_SERVICE added to its environment —
PORT because every framework already reads it.
| Flag | Meaning |
|---|---|
--name |
Register under this name, default the current directory's |
--kind, -k
|
What the service is, default service
|
--project |
Group services of one codebase |
--host |
Interface the process will bind to |
--preferred-port |
Wish for this port, take another if it is not free |
--require-port |
Insist on this port, and fail if it is not free |
--ttl |
Hold a lease this long, renewed while the process runs |
--anyway |
Start on a free port when no warden answers |
Three things it does that a shell wrapper gets wrong:
- The child's exit code becomes warden's, so nothing swallows a failure.
- The port is released whether the program exits, crashes or is interrupted.
- The registration carries the child's process id, which is what later tells a held port from an abandoned one. See warden reap.
With no warden reachable it refuses rather than picking a port itself, because
guessing at a port is the thing warden exists to stop. --anyway overrides
that, takes a free port from the pool, and says on standard error that nothing
is registered.
The same claim, printed instead of wrapped — for an IDE run configuration, a Makefile, or anything else that cannot be started by warden.
$ warden env shop-api --kind backend
PORT=8000
WARDEN_PORT=8000
WARDEN_HOST=127.0.0.1
WARDEN_ADDRESS=127.0.0.1:8000
WARDEN_SERVICE=shop-api| Flag | Meaning |
|---|---|
--kind, -k
|
What the service is, default service
|
--project, --host, --preferred-port, --require-port
|
As warden register
|
--export |
Prefix each line with export
|
--write .env |
Set those keys in a dotenv file, leaving every other line alone |
eval $(warden env shop-api --export)
warden env shop-api --write .env--write replaces each key where it already stands and appends the rest. A
.env is usually somebody else's file too, so nothing warden did not write is
touched.
Run the registry.
| Flag | Meaning |
|---|---|
--host |
Interface to listen on |
--port |
Port to listen on |
--pool 8000-8999 |
Range of ports to hand out |
--reserved 8080,9000-9010 |
Ports never handed out |
--database |
Path to the registry database |
--no-probe |
Do not test ports for existing listeners |
Fleet settings have no flags; they are environment only. See Configuration.
Claim a port. Prints the port and nothing else, so it drops into a script.
| Flag | Meaning |
|---|---|
--kind, -k
|
Required. What the service is |
--project |
Group services of one codebase |
--host |
Interface the service will bind to, default 127.0.0.1
|
--preferred-port |
Wish for this port, take another if it is not free |
--require-port |
Insist on this port, fail if it is not free |
--ttl |
Release the port again after this many seconds |
--pid |
Process id of the service |
--count |
Claim this many at once, named <name>-1 upwards |
--contiguous |
With --count, insist they run back to back |
--node |
Register on that warden in the fleet, through this one |
--preferred-port and --require-port cannot both be given.
A stack that needs four ports can ask four times and hope nothing takes one in between, or ask once:
$ warden register stack --kind backend --count 4
8800
8801
8802
8803They come back as stack-1 to stack-4, chosen and written under one
transaction, so either all four are held or none are. Asking again renews the
same four rather than shuffling a running stack onto different ports.
--contiguous insists they run back to back, for the tools that will not take
a scattered set. When no run is long enough it says so and writes nothing:
$ warden register row --kind backend --count 6 --contiguous
no run of 6 free ports in 8800-8809 on 127.0.0.1warden pool says it before it comes to that, whenever the two numbers differ:
8800-8809 5 allocated 5 free 0 reserved 4 in a row.
A group takes no --require-port: that has no sensible answer for four ports at
once, and a caller who needs one particular port needs one registration.
$ warden register shop-api --kind backend --project shop
8000Print one service's address.
$ warden get shop-api
127.0.0.1:8000Given as node/service, it asks that node through the hub:
$ warden get build-01/build-runner --url http://hub:7010
127.0.0.1:9000Exits 1 with no service registered as 'x' when it is not there.
List registered services.
| Flag | Meaning |
|---|---|
--project |
Only this project |
--kind |
Only this kind |
--holders |
Also say whether each holder is still there |
--stale |
Only the ones whose holder is gone |
--all |
Ask every warden in the fleet, not just this one |
$ warden ls --project shop
SERVICE KIND PROJECT ADDRESS PID
shop-api backend shop 127.0.0.1:8000 -
shop-web frontend shop 127.0.0.1:8001 ---all asks every warden in the fleet and adds a NODE column. Nodes that did
not answer are named on standard error, so a pipe still gets a clean table. See
Cluster.
--holders adds a HOLDER column saying whether whoever asked for each port is
still there:
$ warden ls --holders
SERVICE KIND ADDRESS PID HOLDER
shop-api backend 127.0.0.1:8000 14204 running
old-job worker 127.0.0.1:8001 9930 goneA holder is gone when the process it named no longer exists, or when nothing
is listening on its port. It costs one sweep of the machine's sockets, which is
why it is asked for rather than always done, and it cannot be combined with
--all: only the machine a service runs on can see whether it is still there.
A registration touched in the last 30 seconds is never called gone. A service that has registered and not yet bound its socket is starting, not dead.
Give a port back to the pool.
$ warden release shop-web
released shop-web--node build-01 releases it on that warden instead, through the one you asked.
Release the ports of services that are no longer there.
$ warden reap
release old-job? nothing is on 8001 and pid 9930 is gone [y/N]: y
released 1It asks per service and names why each one qualifies. --yes skips the
questions, for a machine that runs it on a schedule.
Nothing is ever reclaimed on its own. A service in the middle of a restart would lose its port to a timer, and a registry that takes ports back without being asked is worse than one holding a few dead rows.
Push a lease out before it runs out, for a service registered with a --ttl.
$ warden heartbeat ci-runner --ttl 600
127.0.0.1:9000| Flag | Meaning |
|---|---|
--ttl |
Seconds from now; without it, the lease it registered with |
--pid |
Update the recorded process id at the same time |
--node |
Do it on that warden in the fleet |
$ warden pool
8000-8999 2 allocated 997 free 1 reservedCounts only ports inside the pool. A service on a --require-port outside the
range is registered but not counted here.
--all reports every node and the fleet totals, with FREE coloured by how little
is left — the point of the view being to spot the machine about to run out:
$ warden pool --all --url http://hub:7010
NODE POOL HELD FREE RESERVED
build-01 9000-9099 4 96 1
hub 8000-8999 2 997 1
web-02 9000-9099 98 2 1The totals are a sum of what is left, never one pool the fleet shares: two nodes may hand out the same numbers on different machines.
What happened to a port, kept after it stopped being true.
$ warden history 8000
WHEN WHAT SERVICE KIND ADDRESS PID
7s ago released shop-api backend 127.0.0.1:8000 14204
2h ago registered shop-api backend 127.0.0.1:8000 14204| Argument | Meaning |
|---|---|
what |
A port, or a service name. Everything recent without one |
| Flag | Meaning |
|---|---|
--limit |
How many to show, default 20 |
Every registration, renewal, move, release and expiry is written down as it
happens, so warden history 8000 still answers for a service released weeks
ago. Rows are dropped past a cap, so a warden left running for a year does not
grow a database nobody asked for.
Follow what happens, as it happens. Runs until it is stopped.
$ warden events
09:41:02 registered shop-api 127.0.0.1:8600
09:41:11 renewed shop-api 127.0.0.1:8600
09:41:44 released shop-api 127.0.0.1:8600| Flag | Meaning |
|---|---|
--known |
List everything warden can tell you about, and stop |
--json |
One event per line, flushed as it arrives, so it pipes |
The stream carries every action — registered, renewed, moved, released,
expired — and filtering is the reader's business. GET /v1/events is the same
thing as server-sent events, behind the same token as every other read.
See Events and webhooks.
Where events are posted, and whether that is working.
$ warden webhook
address https://discord.com/api/webhooks/...
shape discord
events registered and released
delivered 7| Flag | Meaning |
|---|---|
--test |
Post one made-up event from this machine now |
--event |
Which one to make up, like node.stale
|
--all |
Post one of every event there is, in order |
Without any of them the answer comes from the warden that is running. With
them, the post goes out from this machine using this machine's settings — which
is what warden setup has just written, and a different thing. If the two
disagree, the running warden has not been restarted.
$ warden webhook --all
port.registered posted
port.renewed posted
...
firewall.restored posted
to https://discord.com/api/webhooks/...Thirteen made-up events, one at a time and in order, because a chat window shows them in the order they land. Nothing is registered and nothing changes: only the message is real. It is the quickest way to see what your colours and words look like where they will actually be read.
The address is always shown cut back to its host. A webhook URL is a credential and its path is the half worth stealing.
Register everything a project's warden.project.toml asks for.
$ warden apply
SERVICE KIND ADDRESS WHAT
shop-api backend 127.0.0.1:8900 taken
shop-worker worker 127.0.0.1:8901 taken| Flag | Meaning |
|---|---|
--file, -f
|
Which manifest to read; warden.project.toml here by default |
--env |
Also write the ports into this env file |
--release |
Give the project's ports back instead |
Running it twice changes nothing the second time: it renews, and never shuffles a running project onto different ports. See Projects.
Write the reverse-proxy configuration for what is registered.
$ warden export caddy --domain example.com| Argument | Meaning |
|---|---|
proxy |
caddy, nginx, traefik or hosts
|
| Flag | Meaning |
|---|---|
--domain |
Names become <service>.<domain>
|
--project, --kind
|
Narrow it down |
--all |
Take the whole fleet, each service at its own machine |
--apply |
For hosts only: write it into this machine's hosts file |
It prints and stops, except hosts --apply, which replaces the lines between
warden's own two markers and touches nothing else. That one needs root.
See Reverse proxy.
What is listening on this machine. Needs no server.
| Flag | Meaning |
|---|---|
--port |
Only this port |
--udp / --no-udp
|
Include UDP sockets, on by default |
--all |
Ask every warden in the fleet, and add a NODE column |
The WARDEN column names the service when the port came from the registry, which needs a warden to be reachable. Without one the column is simply blank; the rest still works.
See Ports and processes.
Stop whatever is holding a port. Needs no server.
| Argument | Meaning |
|---|---|
target |
A port, or a process id with --pid
|
| Flag | Meaning |
|---|---|
--pid |
Read the number as a process id |
--force |
Kill it outright if it will not stop politely |
--yes, -y
|
Do not ask first |
List the wardens this one knows about.
| Flag | Meaning |
|---|---|
--forget NAME |
Remove a node that is not coming back |
$ warden nodes --url http://hub:7010
NODE URL POOL VERSION STATUS LAST SEEN
build-01 http://build-01:7010 9000-9099 0.1.0 online 4s agoThe dashboard. --interval sets the refresh in seconds, default 2. --all shows
the whole fleet instead of one warden.
Four tabs across the top — Services, Ports, Firewall, Nodes — the same bar the setup screen has.
| Key | Action |
|---|---|
↑ ↓ j k
|
Move |
tab |
Step through the tabs |
ctrl+left ctrl+right
|
The same, either way round |
n |
Step through one node at a time, with --all
|
a |
Register a service, or write a firewall rule |
d |
Release, stop, close or forget — whatever the tab is showing |
r |
Reload now |
q |
Quit |
a opens a small form, and what it asks for depends on the tab:
Services — name, kind, project, and a port if you want a particular one.
Leave the port empty and warden picks a free one, exactly as warden register
does. With --all there is a fifth field for which node should hand it out.
Firewall — a port, a range, or a catalogue name (ssh, https); the action;
and which network it is for. It writes the rule down, the same way
warden firewall allow does — nothing is applied until
warden firewall apply.
Ports and Nodes have nothing to add and say so. A socket is opened by a process, and a warden announces itself.
d asks first, every time:
| Tab | What d does |
|---|---|
| Services | Release the port, on the node holding it |
| Ports | Stop the process, on the machine it is running on |
| Firewall | Close the rule. It stays in the kernel until the next apply, and the dialog says so |
| Nodes | Forget the warden. It comes back on its own if it is still reporting |
Registering and releasing need the API token, as they always have. Writing or
closing a firewall rule needs allow_remote_firewall on the machine being
asked — the dashboard is a caller like any other, and the refusal comes back in
that machine's own words.
The ports view works with no warden running at all: it falls back to reading the
machine directly, says so in the subtitle, and d stops a process the same way
warden kill does. The services view still needs a registry.
Rules for what may cross. Firewall has the whole of it; this is the shape of the commands.
warden firewall list # every rule, and where each came from
warden firewall allow ssh --from 10.0.0.0/8
warden firewall deny 8080
warden firewall open shop-api # the port the registry handed out
warden firewall dev-mode --from 10.0.0.0/8 --for 2
warden firewall export # what it would become, without applying
warden firewall pending # what is written down and not yet live
warden firewall apply # and it rolls back unless confirmed
warden firewall confirm
warden firewall restore
warden firewall adopt # take over from ufw or firewalld
warden firewall status
warden firewall delete allow-shop-apiNothing is applied by writing a rule down. pending says which rules are in the
book and not in the kernel, export shows what the ruleset would become,
apply takes a snapshot first and arms a rollback, and confirm is what keeps
it. A change nobody confirms undoes itself — which is the whole
reason it is safe to do this over ssh.
| Flag | On | Meaning |
|---|---|---|
--all |
list, status
|
Every warden in the fleet, through the hub |
--all |
open |
Every node that holds the service |
--fleet |
apply, confirm
|
Every node, each applying to its own firewall |
--node |
open, delete, apply, confirm
|
One node in the fleet, through the hub |
--on |
list, status, open
|
Ask the warden at that address instead of this machine |
--from |
allow, deny, open, dev-mode
|
Which address or network the rule is for |
--limit |
allow, deny
|
How often it may happen: 10/second, 6/minute
|
--for |
allow, deny, dev-mode
|
How long it stays: 30s, 15m, 2h, 1d
|
--origin |
list |
Only rules that came from one place |
--rollback |
apply |
Seconds to wait for a confirmation; 0 turns it off |
--node, --all, --fleet and --on all need allow_remote_firewall on the
machine being asked, for anything that changes a rule. Reading is what a token
already allows. A fleet-wide apply refuses to run without a rollback. See
Firewall.
0 on success, 1 on any refusal, with the reason on standard error. The
message is the same one the API would return, so a script can act on it:
if ! PORT=$(warden register shop-api --kind backend 2>/dev/null); then
echo "no port available" >&2
exit 1
fiEverything worth checking, in one command, so "why is this not working" does not mean running four others and comparing what they said.
$ warden doctor
ok warden 0.1.0 answering at http://127.0.0.1:7010, role hub
ok settings from ~/.config/warden/warden.toml
warn listening on 0.0.0.0 with no token set - anyone who can reach this
machine can hand out and release ports
ok pool 8000-8999, 4 held, 995 free
warn 2 of 6 registrations held by something that is gone - warden reap
ok build-01 online, last seen 4s ago
note could not check for updates - github is unreachable| Level | Means |
|---|---|
ok |
Checked, nothing wrong |
note |
Worth knowing, or a check that could not run at all |
warn |
Works, but somebody should look at it |
fail |
Broken |
Exits 1 only when something failed, and 0 on warnings, so it drops into a
health check without an unset token being read as the machine being down.
Have this machine start warden serve at login.
$ warden service install
systemd user unit
~/.config/systemd/user/warden.service
[Unit]
Description=warden - hands out ports and says who holds them
...
systemctl --user daemon-reload
systemctl --user enable --now warden.service
Write it? [y/N]:| Command | Does |
|---|---|
warden service install |
Show what it would write, then write it |
warden service uninstall |
Take it away again |
warden service status |
Say whether it is installed, and whether it is up |
--yes on install and uninstall skips the question.
| System | What gets written |
|---|---|
| Linux | A systemd user unit in ~/.config/systemd/user
|
| macOS | A launchd agent in ~/Library/LaunchAgents
|
| Windows | A command in the Startup folder |
Always as the account that ran it, never as root or SYSTEM: a warden started by another user reads that user's settings and hands out ports from a registry nobody else can see.
On Windows it is a Startup command rather than a scheduled task, because
creating a task is refused outright on plenty of managed machines. That also
means there is no service manager to ask, so status says running when a
warden is answering on the configured port, whoever started it.
Asks the handful of questions that matter and writes the answers to the config file. It keeps any setting it did not ask about. See Configuration.
In a terminal it is one screen in seven tabs — Ports, Reach, Fleet, Events, Embed, Firewall, Risk:
| Key | Action |
|---|---|
ctrl+left ctrl+right
|
Move between tabs |
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 webhook address on screen |
ctrl+s |
Save |
ctrl+q |
Leave without writing anything |
Questions that nothing has earned stay hidden: no token field until the warden
is reachable from elsewhere, no upstream address until it reports to one, no
webhook shape until events go anywhere, no networks to name until the registry
may open ports, and no signing secret unless the shape is json. A refusal
names the field and puts the cursor in it.
The Embed tab picks one of the thirteen events, takes a colour and the words that follow the subject, and draws the message it would send underneath as you type. See Events and webhooks.
If something else is holding this machine's packets - ufw, firewalld - the
firewall tab says so and points at warden firewall adopt. It never takes
over from the settings screen: that reads the other one's rules and shows them
first, which is not something to slip into a form.
It fits an 80 by 24 terminal; under 84 columns the labels move above the fields they name and nothing is cut off.
| Flag | Meaning |
|---|---|
--plain |
Ask one question at a time instead of a screenful |
Without a terminal - a script piping answers in, a job on a build machine - the
questions come one at a time on their own. So do they where TERM is unset or
dumb, which is what a cron job or a serial console gets; warden tui says so
and points at warden ls rather than ending in a traceback.
A warden that is already running keeps the settings it started with. It reads them once, at startup. Restart it to pick up a new webhook or pool.
Take this machine's registrations and firewall rules somewhere else.
warden state export > machine.json
warden state import machine.json --dry-run
warden state import machine.jsonWhat travels is what somebody wrote down: which services hold which ports, and which rules decide what crosses. Not the history — that is a record of what happened on that machine and means nothing on another one. Not the snapshots either: a snapshot is some other firewall's ruleset, and restoring one onto a different machine is the single thing the whole firewall design exists to make impossible by accident.
$ warden state import machine.json --dry-run
service docs
rule allow-ssh
skipped shop-api - already registered here
skipped allow-8443 - already written down here
2 would land, 2 would not - nothing was written
A name already registered here, or a port this machine does not hand out, is
one row's problem rather than a reason to refuse the other forty — and every
one that does not land is named. --dry-run says all of it and writes nothing.
| Flag | Meaning |
|---|---|
--dry-run |
Say what would land, and land nothing |
--any-port |
Let a service whose port is outside this machine's pool take a free one instead |
Without --any-port, a registration whose port this machine does not hand out
is skipped. The pool belongs to the machine, and an imported row does not get
to widen it.
Rules land written down, not applied. warden firewall apply is still the
step that makes them true, and the import says so rather than letting somebody
assume otherwise.
The file carries a shape number and the version that wrote it. A file from a
later warden whose shape this one does not know is refused by name rather than
read hopefully.
Opens the same screen warden setup uses, over what is already written down —
or prints the table where there is no terminal to draw on.
warden settings # the screen, on the first tab
warden settings embed # straight to one part of it
warden settings --plain # the table, with where each value came from
warden settings list # the same table, asked for on purpose
warden settings --json # the same as data
warden settings set pool_end 4199 # write one to the config file
warden settings unset probe # take it back outThe parts are ports, reach, fleet, events, embed, firewall and
risk. Leaving with ctrl+s writes over the file rather than replacing it, so
a setting the screen never asks about survives — which is the difference from
warden setup.
Without a terminal — a script, a build machine, a cron job with TERM unset —
it prints the table instead of trying to draw. Every script that ran
warden settings before still gets exactly what it got.
set types and checks the value before writing it, so the file never holds
something warden would refuse to start with, and it says so when the environment
will override what you just wrote anyway.
warden — nothing binds a port without asking ·
uv tool install warden-ports
Repository · Issues · Releases · PyPI · MIT
Getting started
While it runs
Several machines
Reference