docs · pre-release
Menu
Deploying / Serve

Serve

shep serve <dir> registers a sheep whose command line is this same invocation, canonicalized, with --foregroundappended: the worker answering every request is the registered sheep, so shep describe shows exactly what restarts if it dies. It's hand-rolled (six small modules over an HTTP layer shared with the metrics dog), not built on axum or tower-http.

Try it

$ shep serve ./dist --port 8080

Add --foreground to run the worker directly in this terminal instead of registering a sheep. Same worker either way, just without the supervision.

The security posture, checked against real requests

Every default below is the safe one, and every unsafe one is a named flag rather than a config file nobody reads. Binds loopback (127.0.0.1:8080) unless you pass a wider--bind, which prints a stderr notice naming what it just exposed. These four requests were run against a realshep serve process, docroot containing anindex.html, a .env, and a symlink:

$ curl -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/
200
$ curl -o /dev/null -w '%{http_code}\n' 'http://127.0.0.1:8080/..%2f..%2fetc%2fpasswd'
400
$ curl -o /dev/null -w '%{http_code}\n' --path-as-is 'http://127.0.0.1:8080/../../../../etc/passwd'
400
$ curl -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/.env
404
$ curl -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/link.html
404

Traversal is refused in every encoding it was tried in, not just the literal one. Dotfiles 404: serving a repo checkout withshep serve . would otherwise publish .env and the whole .git history. And any symlink under the docroot 404s by default, not only one that leaves it: an in-docroot link like dist/current -> ../releases/2026-08-15 is refused along with everything else, because the leaf open carries O_NOFOLLOW but the component walk ahead of it isn't atomic, and the default per-component walk is what closes that race.

careful

--follow-symlinks reopens exactly that race: a canonicalize-then-check that a file created in the docroot between the walk and the open can still slip past. It exists because a real deploy layout needs it (current -> releases/2026-08-15 won't serve without it), and the notice fires every time you pass it, on every request in the default mode that gets refused because you didn't:

$ shep serve ./dist --follow-symlinks --format json
{"schema_version":1,"notice":{"code":"follow_symlinks","message":"shep serve: --follow-symlinks reopens the check-then-open race (TOCTOU) the default per-component walk closes — a symlink under the docroot can now point anywhere this process can read"}}

The rest of the flags

--listing turns on directory listings for a folder with noindex.html, off by default, since a listing publishes every filename under it. --hidden serves dotfiles, mainly for .well-known/acme-challenge. --spa servesindex.html for a path that doesn't exist, only for requests that accept HTML, so a missing script still 404s.--auth <file> requires one user:passwordline, mode 0600, on every request. Sent as plain HTTP basic auth, base64 rather than encryption, so put a real proxy in front if that matters.

What isn't here: range requests, conditional requests, ETags, compression, keep-alive, TLS, or HTTP/2not built yet (range/conditional requests are v1.1 candidates).shep serve takes no configuration from the environment: pass --port, --bind, --spa,--auth and the flags above on the command line instead. The visible cost today is no video seeking and a full re-read per request.

Where to go next