servd

Run and manage many local dev servers at once.
servd runs your registered web projects on stable ports and reverse-proxies
them as friendly nip.io subdomains — so a folder of
client sites becomes http://acme.127.0.0.1.nip.io:8080/,
http://blog.127.0.0.1.nip.io:8080/, and so on, all managed from one CLI or an
interactive TUI.
It is not tied to any one generator.
jigyll/Jekyll
is just one of several detectors; Node,
Hugo, just/make
recipes, plain static dirs, and any project with a Procfile work too.
Install
Homebrew — macOS and Linux
brew install reidransom/tap/servd
Scoop — Windows
scoop bucket add reidransom https://github.com/reidransom/scoop-bucket
scoop install reidransom/servd
Release archive
Download the archive for your operating system and architecture, plus
checksums.txt, from the
latest GitHub release.
Windows archives are ZIP files; macOS and Linux archives are tarballs. Verify
the archive before extracting it:
# macOS or Linux: compare this hash with the matching checksums.txt entry
shasum -a 256 servd_Darwin_arm64.tar.gz
# Windows: compare this hash with the matching checksums.txt entry
Get-FileHash .\servd_Windows_x86_64.zip -Algorithm SHA256
Each archive contains servd (servd.exe on Windows), README.md, and
LICENSE.
Go install
With the Go version declared in go.mod or newer:
go install github.com/reidransom/servd/cmd/servd@latest
Every installation method can report its exact version without reading config
or starting the TUI:
servd version
Quick start
servd add ~/clients/acme # detect the launcher, assign a port + slug
servd up --all # start every registered dev server
servd proxy up # start the nip.io reverse proxy on :8080
servd # open the interactive dashboard (TUI)
Every entry in sites.toml is managed by servd and participates in
up --all and restart --all. To start a single registered site, name it:
servd up acme.
Then visit http://<slug>.127.0.0.1.nip.io:8080/ for any site, or
http://127.0.0.1:8080/ for a landing page listing them all. nip.io resolves
*.127.0.0.1.nip.io to 127.0.0.1 with zero DNS setup.
How a project is launched
For each site, servd resolves a launch command in this order (first match wins):
- Manual override — a command pinned at registration, either quoted
(
servd add . --cmd "…") or unquoted after --
(servd add . -- bundle exec middleman serve -p {port}).
.servd.toml in the project directory — a one-line
cmd = "bundle exec middleman serve -p {port}" lets any project declare how
to serve itself, next to its code.
Procfile (or Procfile.dev) — the web: process, run with $PORT
and $HOST exported (foreman/Heroku
convention). The universal
escape hatch.
- Launcher rules — declarative match-and-run rules, tried in order. Your
own rules from
~/.config/servd/launchers.toml come first, then the
built-in defaults:
| Rule |
Trigger |
Command |
| jigyll / Jekyll |
_config.yml |
jigyll serve -s . -H {host} -P {port} -w |
| Hugo |
hugo.toml / content dir |
hugo serve --bind {host} -p {port} |
| Node |
package.json dev/serve/start script |
npm run <script> (+--port for vite/next/astro) |
| just |
justfile with serve recipe |
just serve |
| make |
Makefile with serve target |
make serve PORT={port} |
| static |
an index.html |
built-in file server (no dependency) |
PORT and HOST are always exported to the child, and {port}/{host}
placeholders are substituted — so Procfiles and rule commands share one
port-injection mechanism. Run servd which <slug> to see exactly what a project
resolves to.
Custom launcher rules
The built-in table above is data, not code — every rule is defined in the same
TOML format you can write yourself. servd launchers prints the effective rule
set; copy any rule into ~/.config/servd/launchers.toml to change it (same
name replaces the built-in, disabled = true turns it off), or add rules for
tools servd has never heard of:
[[launcher]]
name = "middleman"
matches = { file = "Gemfile", regex = "middleman" } # file content regex
bin = "middleman" # required on PATH
cmd = "bundle exec middleman serve -p {port}"
[[launcher]]
name = "static" # replace the built-in fallback file server
exists = ["index.html"]
bin = "python3"
cmd = "python3 -m http.server -b {host} {port}"
User rules are always tried before the built-ins, in file order. A rule matches
a directory when all of its predicates hold; list values match any-of:
| Predicate |
Meaning |
exists = ["hugo.*"] |
any file matches (globs ok) |
dirs = ["content"] |
any directory matches (globs ok) |
bin = "hugo" |
binary is on PATH |
recipe = { files = ["justfile"], target = "serve" } |
make/just file declares the target |
matches = { file = "Gemfile", regex = "…" } |
file content matches the regex |
npm_script = ["dev", "serve"] |
package.json has one of these scripts |
In cmd, {port}/{host} are substituted at launch, {script} is the
matched npm_script entry, and {self} is the servd binary itself. For dev
servers that ignore $PORT, port_flag = " -- --port {port}" is appended when
any port_flag_deps entry appears in package.json dependencies.
Commands
| Command |
Purpose |
servd add <path> [--slug] [--port] [--cmd] [-- <command>…] |
register one project |
servd rm <slug> |
stop and unregister a site |
servd which <slug> |
show the resolved launch command |
servd launchers |
print the effective launcher rules (yours + built-ins) |
servd status [slug] (alias ls) |
table of every site, or one named site, with live status (--json for machines) |
servd up [slug…] [--all] |
start sites (--all starts every registered site; --wait/--json for scripts) |
servd down [slug…] [--all] |
stop sites (--all stops every registered site) |
servd restart [slug…] [--all] |
restart sites (--all restarts every registered site) |
servd logs <slug> [-f] |
show / follow a site's server output |
servd open <slug> |
open the nip.io URL in a browser |
servd proxy up|down|status |
manage the background reverse proxy |
servd proxy |
run the proxy in the foreground |
servd doctor |
check tools, ports and nip.io resolution |
servd version / servd --version |
report version, commit, and build date |
servd / servd tui |
interactive dashboard |
The dashboard is a split view: the site list on the left, and a live tail of
the highlighted site's log on the right, led by the $ command that started
(or would start) it. Moving the selection switches the log panel; tab moves
focus to the log so ↑/↓ scroll it (scroll up to pause the tail, scroll back to
the bottom to resume following).
TUI keys
↑/↓ move · tab focus list/log · s start · x stop · r restart ·
a start-all · X stop-all · o open · p toggle proxy ·
A add a site (type a path, tab completes) ·
h show/hide this key help · q quit
Agents and scripts
Coding agents (and shell scripts) shouldn't have to parse tables or babysit
long-running processes. Two flags give them a structured interface:
servd status --json # everything an agent needs to discover servers
servd status acme --json # status for one known server
status --json prints one object: proxy (running, accepting, pid,
port, url) plus a sites array (containing only the requested site when a
slug is supplied) where each site carries slug, path, port, url (through the proxy), direct_url (straight to the dev server),
launcher, status (stopped | starting | running), and — when live —
pid, cmd, log, started_at, uptime_seconds. Match your project by
path to find its slug, then hit direct_url (or url if the proxy is
accepting).
up --wait polls until the server actually accepts connections (default
--timeout 30s), and exits non-zero — with the log tail in the error — if the
process dies or never binds. With --json the per-site results (including any
error) go to stdout as an array. Servers are already detached by default, so
up never needs backgrounding tricks; a second up on a running site is a
no-op.
Use these instead of reading state.json directly — the file's format is
internal and may change.
Files
-
~/.config/servd/config.toml — settings (port_range_start, proxy_port,
domain_suffix, bind_host)
-
~/.config/servd/sites.toml — the site registry
-
~/.config/servd/launchers.toml — your launcher rules (optional, see above)
-
<project>/.servd.toml — per-project launch command (optional)
-
~/.local/state/servd/state.json — live pids/ports (self-healing)
-
~/.local/state/servd/logs/<slug>.log — per-site server output
Compatibility: legacy enabled site keys and default_enabled settings keys
are ignored and may be deleted.
Servers are launched detached in their own process groups, so they keep
running after the CLI or TUI exits; servd down signals the whole group.
The reverse proxy passes through websocket upgrades, so live-reload / HMR works.
- LAN mDNS publishing is available on macOS and Linux. Windows reports it as
unsupported in
servd doctor; regular loopback and nip.io routing still
work.
- Hosts-file synchronization requires an elevated terminal. The hosts file is
/etc/hosts on macOS and Linux, and
%SystemRoot%\System32\drivers\etc\hosts on Windows.
- Launcher tools such as Node, Hugo, jigyll, Jekyll,
just, and make are
optional host dependencies. servd doctor reports which capabilities are
available.
- User-authored commands run through
sh on macOS and Linux and through
cmd.exe on Windows. Commands that depend on POSIX shell syntax are not
portable to Windows.
Development
go build ./... && go vet ./... && go test ./... -race
Proxy and the Host header
The proxy rewrites the Host header to the backend's own address
(e.g. 127.0.0.1:4001), so dev servers with host allowlists (Vite 5+, Next,
Rails host authorization) accept proxied requests
out of the box. The original
host is still available to the backend via X-Forwarded-Host / X-Forwarded-Proto.
If a site builds absolute URLs from Host and needs the nip.io address
instead, set preserve_host = true on its [[site]] entry in sites.toml —
and add the nip.io hostname to that dev server's own allowed-hosts setting.
MIT licensed. Maintained by r2ware.