Examples
Every page in this section describes what shep does. This one is seven tiny programs and a Flockfile that make it happen where you can watch: a probe waiting, a stop signal being ignored, a memory ceiling firing, a crash loop backing off. Everything lives underexamples/ — a workspace member of its own, built bycargo build --release but never on the path a plaincargo install shep walks.
That first set is Rust on purpose: it needs nothing but a Rust toolchain, works from a fresh clone, and is whatexamples/Flockfile.toml starts. A second layer,examples/Flockfile.polyglot.toml, runs the same ideas against Node, Bun, Python and Go — the stacks most shep operators actually run, and the ones the Rust set structurally can't stand in for: a real Node HTTP server, the same source file under two different runtimes, a Python interpreter resolved through a venv instead of PATH, a Go binary with a build step, and a static site served with no program of the operator's own at all. It's a separate file and a separate section below because it needs runtimes the Rust set doesn't — see The polyglot layer for exactly which ones, and what happens to the rest of the file when one is missing.
Unix, not just cargo. Six of the seven build and run anywhere Rust does, but stubborn traps SIGTERM with a Unix signal mask — on any other platform it prints why and exits 1 instead of running at all. shep start processes a Flockfile top to bottom and stops at the first app that fails to spawn (see the polyglot layer below for the same rule hitting a missing runtime), so on a non-Unix host this file does not run straight through as shown. Comment outstubborn's [[app]] block to run the other six.
No config edits needed otherwise. Every path inexamples/Flockfile.toml is relative toexamples/ itself — a Flockfile app with nocwd runs where its Flockfile lives — so cloning the repo and running the commands below just works, on a Unix host.
Run it
Two of the seven — leaky and crasher — restart on purpose and by design, and shep start registers into whatever $SHEP_HOME is already pointed at. Theexport below is not optional: skip it and this starts inside your own real daemon, alongside whatever it's already supervising.
What each one demonstrates
http_serverThe baseline every probe example needs.curl 127.0.0.1:19081/ and get 200 OK.slow_bootA readiness probe waiting, and listen_timeout expiring.shep flock shows starting, then online anyway once listen_timeout elapses, before the port is even bound.ready_when_toldAn exec probe — the one kind with no everyday real-world example.touch the sentinel file and watch the next poll flip the sheep to online.stubbornEscalation past a stop signal.shep stop stubborn takes exactly kill_timeout, then SIGKILL ends it.leakymax_memory firing.shep flock's MEM column climbs past the ceiling, then RESTARTS increments and it climbs again.crasherRestart policy, growing backoff, and max_restarts.shep logs -f shows the gap between restarts widen until the sheep reaches errored.noisyThe log plane.shep bleats noisy -f, alternating stdout and stderr lines.The exec probe, in full
readiness_probe with kind = "exec" has no everyday real-world stand-in the way an HTTP or TCP probe does —ready_when_told is the minimal program that makes one demonstrable. It exits 0 once a file exists on disk and non-zero otherwise; the Flockfile points a probe at it, gated on a sentinel path this page and the file both agree on:
[app.readiness_probe] kind = "exec" target = "../target/release/ready_when_told /tmp/shep-examples-sentinel" interval = "3s" timeout = "1s"
The next poll — within interval — flipshttp-server-gated-by-sentinel from starting toonline.
kill_timeout, not graceful_timeout
stubborn blocks SIGTERM and never acts on it, so shep's kill ladder has to escalate to SIGKILL — a signal nothing can trap. The field that governs how long shep waits before that escalation on a plain shep stop/restart/delete is kill_timeout, not graceful_timeout: the latter only governs areload's drain window for the old instance. This example setskill_timeout to 3 seconds, short enough to watch land:
Roughly three seconds pass — one "caught SIGTERM, still not dying" line in its log, then silence — beforeshep flock reports it stopped by SIGKILL.
The polyglot layer
examples/Flockfile.polyglot.toml needs, per app,node, bun, python3, orgo — never all four to run any single one of them. Two setup steps, once, before the runtimes that need them:
node-http / node-http-clusterA plain Node HTTP server, plus instances above 1.curl 127.0.0.1:19090/, then 19091 through 19094 for each clustered instance.bun-httpinterpreter, and shep.toml's [interpreters] mapping.The same node-http.js, curl 127.0.0.1:19095/, and a runtime field in the response body instead of a different file.python-appinterpreter pointed at a venv's own python, not a bare python3.curl 127.0.0.1:19096/ after examples/polyglot/setup-venv.sh.go-httpA build step, then no interpreter field at all.curl 127.0.0.1:19097/ after examples/polyglot/go-http/build.sh.static-siteshep serve — no program of the operator's own.curl 127.0.0.1:19098/ after shep serve examples/polyglot/static-site --port 19098.A missing runtime does not sink the whole file, but it does sink everything listed AFTER it. shep start processes a Flockfile's apps top to bottom and stops at the first one that fails to spawn (shep start --help: "Several are started in turn, not atomically") — every app already online stays online, and the failing app reports why (SpawnFailed: No such file or directory), but nothing below it in the file is even attempted in that same invocation. Missing bun would silently keep python-appand go-http from starting too, not justbun-http. If a runtime isn't installed, comment out that app's [[app]] block rather than leaving it in.
One file, two runtimes
node-http and bun-http run the exact same script, examples/polyglot/node-http.js — onlyinterpreter changes, from "node" to"bun":
[[app]] name = "node-http" script = "polyglot/node-http.js" interpreter = "node" args = ["19090"] [[app]] name = "bun-http" script = "polyglot/node-http.js" interpreter = "bun" args = ["19095"]
shep never inspects the script; interpreter is a plain program name, spawned with the script as its first argument. Ashep.toml [interpreters] table does the same job automatically, by extension, so every app in a flock doesn't have to spell interpreter out by hand:
[interpreters] js = "node" py = "python3"
An app's own interpreter field, when it sets one — as every interpreted app in examples/Flockfile.polyglot.tomldoes, though go-http sets none because a compiled binary runs directly — always wins over the extension map. The map exists for a flock of apps that all use the same handful of extensions and would rather not repeat interpreter = "node" five times.
instances, without reuse_port
node-http-cluster is the same script again,instances = 4. Each instance gets its ownSHEP_INSTANCE env var (0 through 3), and the script adds that to its base port: four separate listeners on 19091 through 19094, not one port shared four ways. A readiness_probe is what makes the reload ordering a question at all. Without one, and with wait_ready, a reload overlaps whatever reuse_port says. A probed app that does not set reuse_port is reloaded serially instead: the old instance drains before the new one starts, because a probe against a shared address cannot tell you which of two instances answered it. Setting reuse_port is the app's own claim that it calls SO_REUSEPORT before its bind(). shep never binds a listening socket on an app's behalf and cannot check that claim, so the field buys an overlap attempt rather than a working overlap: an app that claims it wrongly gets a replacement that takes EADDRINUSE. This example offsets the port rather than sharing it, so the question never arises: four listeners, four ports, nothing to share. SeeComing from pm2 for how this compares to pm2's own cluster mode.
A venv's own python
The classic failure python-app exists to head off: an app that runs fine from a shell — your venv is active — and does not under a supervisor, because the supervisor never activated anything and a bare python3 resolved against whatever's on ITS PATH instead. interpreter points straight at the venv's own binary:
[[app]] name = "python-app" script = "polyglot/python-app.py" interpreter = "polyglot/venv/bin/python3" args = ["19096"]
A build step, then no interpreter
go-http is compiled, not interpreted:examples/polyglot/go-http/build.sh runsgo build first, and the Flockfile entry that follows has no interpreter field at all — a native executable already runs itself.
shep serve, not an app
examples/polyglot/static-site isn't in the Flockfile at all. shep serve registers its own sheep for a directory with no program of the operator's own:
See Serve for --bind,--auth, and everything else it refuses by default.