Examples
Every page in this section describes what shep does. This one is a set of tiny programs and a Flockfile that make it happen where you can watch: a probe waiting, a memory ceiling firing, a crash loop backing off. Everything lives under examples/, a workspace member built by cargo build --release, never on the path a plain cargo install shep walks.
That first set is Rust on purpose. It needs nothing but a Rust toolchain, runs from a fresh clone, and is what examples/Flockfile.toml starts. A second layer, examples/Flockfile.polyglot.toml, runs the same ideas against the stacks most shep operators actually run: Node, Bun, Python and Go. It needs those runtimes installed, so it lives in its own file, below.
stubborn traps SIGTERM with a Unix signal mask: on any other platform it prints why and exits 1 instead of running at all. That is an exit, not a refusal to spawn, so the start succeeds and the rest come up. stubborn then restarts into the same exit until max_restarts (16) is spent and it settles at errored. Comment out its [[app]] block to skip that. A missing runtime is harsher: a spawn that fails ends the start where it stands, as the polyglot layer below shows.examples/Flockfile.toml is relative to examples/ itself: a Flockfile app with no cwd runs where its Flockfile lives.Run it
Two of them — leaky and crasher — restart on purpose, and shep start registers into whatever $SHEP_HOME is already pointed at. The export below is not optional: skip it and this starts inside your own real daemon, alongside whatever it's already supervising.
examples/src/bin/ holds the source of each, and examples/Flockfile.toml wires them up.
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.busyLoad worth plotting, on two different periods.shep lookout's CPU column draws a shape instead of a flat line, and the two rows differ.chattyThe shepherd channel: readiness, a metric, and custom actions.shep trigger chatty ping, then nonsense for the unknown-action reply. shep stop sends a JSON shutdown instead of a signal.The polyglot layer
examples/Flockfile.polyglot.toml needs, per app, node, bun, python3, or go — never all four to run any single one of them. Three setup steps, once, before the runtimes that need them:
shep start works through a Flockfile's apps in stage order, and in the file's own order inside a stage, stopping at the first one that fails to spawn. This file declares no depends_on, so every app is in one stage and that is the file's order top to bottom. Everything already online stays online, and the failure names it (this start already has children running and leaves them alone: node-http, node-http-cluster), but nothing after it is even attempted in that same invocation. Missing bun would silently keep python-app and go-http from starting too, not just bun-http. If a runtime isn't installed, comment out that app's [[app]] block rather than leaving it in. See boot order for what a staged start leaves running.One file, two runtimes
node-http and bun-http run the exact same script, examples/polyglot/node-http.js — only interpreter 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. A shep.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, always wins over the extension map.
instances, without reuse_port
node-http-cluster is the same script again, instances = 4. Each instance gets its own SHEP_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. See Coming 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 runs go build first, and the Flockfile entry that follows has no interpreter field at all — a native executable already runs itself.
The shepherd channel, in four languages
chatty and the three *-chatty apps are one program in four languages: Rust over shep-channel, the rest by hand, all to one contract.
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.
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 — flips http-server-gated-by-sentinel from starting to online.
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 a reload's drain window for the old instance. This example sets kill_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 — before shep flock reports it stopped by SIGKILL.