Skip to content

One machine

vxnsin edited this page Sep 2, 2026 · 3 revisions

One machine

The usual setup. One warden looks after one machine and hands out ports from a single pool.

Start it

warden serve
██             ██        ██     ██  █████  ██████  ██████  ███████ ███    ██
 ███         ███         ██     ██ ██   ██ ██   ██ ██   ██ ██      ████   ██
    █████████            ██  █  ██ ███████ ██████  ██   ██ █████   ██ ██  ██
    ██     ██            ██ ███ ██ ██   ██ ██   ██ ██   ██ ██      ██  ██ ██
    █████████             ███ ███  ██   ██ ██   ██ ██████  ███████ ██   ████
█████████████████
███  █ ███ █  ███    nothing binds a port without asking
███  █ ███ █  ███
███           ███    v0.1.0  listening on http://127.0.0.1:7010
                     pool 8000-8999  1 reserved

It listens on 127.0.0.1:7010 and hands out 8000-8999. The registry port itself is reserved automatically, which is the 1 reserved above.

Leave it running. Everything else talks to it.

Start a project on a port warden picked

The everyday way in, with nothing to change in the project itself:

$ warden run -- npm run dev
shop-api  ->  8000

  VITE ready, listening on http://localhost:8000

The name comes from the directory. The process gets PORT in its environment, holds the port while it runs, and gives it back when it exits — including when you interrupt it.

For a run configuration in an IDE, or anything else that cannot be started by warden, ask for the same thing and print it:

$ 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
eval $(warden env shop-api --export)   # into this shell
warden env shop-api --write .env       # into a dotenv file

Claim a port

Underneath both of those:

$ warden register shop-api --kind backend --project shop
8000

The only output is the port, so it drops straight into a script:

PORT=$(warden register shop-api --kind backend)
exec ./server --port "$PORT"

--kind says what the service is: backend, frontend, worker, database, cache, proxy, or anything else you like. It is what colours the dashboard and what --kind filters on. --project groups the services of one codebase.

See what is held

$ warden ls
SERVICE   KIND      PROJECT  ADDRESS         PID
shop-api  backend   shop     127.0.0.1:8000  -
shop-web  frontend  shop     127.0.0.1:8001  -

Narrow it with --project shop or --kind backend, or get machine-readable output with --json.

A registration outlives the process that asked for it, so ask whether anyone is actually still there:

$ warden ls --holders
SERVICE   KIND      PROJECT  ADDRESS         PID    HOLDER
shop-api  backend   shop     127.0.0.1:8000  14204  running
old-job   worker    -        127.0.0.1:8002  9930   gone

$ warden reap
release old-job? nothing is on 8002 and pid 9930 is gone [y/N]: y
released 1

Nothing is ever taken back on its own: a service in the middle of a restart would lose its port to a timer.

What used to be on this port

$ 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

The registry only knows the present. Every registration, renewal, move, release and expiry is written down as it happens, so this answers for services that were released weeks ago. warden history shop-api follows one service instead of one port.

Look one up

Instead of hardcoding a neighbour's address:

$ warden get shop-api
127.0.0.1:8000

Give it back

warden release shop-web

Asking for a particular port

Two different wishes, two different flags:

# "3000 would be nice, but anything free will do."
warden register shop-web --kind frontend --preferred-port 3000

# "It has to be 3000, the number is baked into a config I cannot change."
warden register legacy-crm --kind backend --require-port 3000

--preferred-port quietly falls back to the pool when the port is taken, reserved or already in use. --require-port fails with a 409 instead. Both may name a port outside the pool, which is how a legacy service on 3000 joins the registry at all.

From Python

A service can ask for its own port as it starts:

import uvicorn
from warden import register

port = register("shop-api", kind="backend", project="shop")
uvicorn.run(app, port=port)

Look a neighbour up rather than hardcoding it:

from warden import WardenClient

with WardenClient() as client:
    backend = client.lookup("shop-api")
    base_url = f"http://{backend.address}"

Short-lived processes can hand the port back on the way out:

from warden import reserve

with reserve("test-fixture", kind="worker") as port:
    run_server(port)

Leases, for things that will not clean up

A registration lasts until it is released. In CI or a test fixture, nothing gets the chance:

warden register ci-runner --kind worker --ttl 600

The entry disappears ten minutes later unless something renews it. A heartbeat pushes the expiry out again:

warden heartbeat ci-runner --ttl 600

Sent without a ttl, a heartbeat renews the lease the service registered with. It can never turn a lease into a permanent registration by accident.

The dashboard

warden tui

Two live tables, two seconds apart. tab swaps between what warden handed out and what is actually listening.

Key Action
j k Move
tab Switch view
r Reload now
d Release the service, or stop the process
q Quit

Choosing a different pool

warden serve --pool 4000-4999 --reserved 4200,4500-4510

Or, so it sticks:

WARDEN_POOL_START=4000
WARDEN_POOL_END=4999
WARDEN_RESERVED=4200,4500-4510

See Configuration for the full list, including a .env file.

Keeping it running

$ warden service install
systemd user unit
  ~/.config/systemd/user/warden.service
  ...
Write it? [y/N]: y
warden starts at login - running

A systemd user unit on Linux, a launchd agent on macOS, a Startup command on Windows — whichever this machine has. It prints the whole thing before writing it, and warden service uninstall takes it away again.

Always as the account that ran it. A warden started by root or SYSTEM would read that account's settings and hand out ports from a registry nobody else can see.

The registry survives restarts, so services keep the ports they had.

When something is not working

$ warden doctor
ok    warden 0.1.0 answering at http://127.0.0.1:7010, role hub
ok    settings from ~/.config/warden/warden.toml
ok    pool 8000-8999, 2 held, 997 free
warn  1 of 3 registrations held by something that is gone - warden reap

One command instead of four: whether a warden is there, where its settings came from, whether it is open to the network without a token, how much pool is left, whether anything is held by a process that is not, whether the nodes are still reporting, and whether there is a newer version. It exits 1 only when something failed, so it drops straight into a health check.

Clone this wiki locally