docs · pre-release
Menu
Get it running / Examples

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.

careful
Unix, not just cargo. All but one 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. 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.
careful
No config edits needed otherwise. Every path in 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.

$ cargo build --release --workspace
$ export SHEP_HOME="$(mktemp -d)"
$ ./target/release/shep start examples/Flockfile.toml
$ ./target/release/shep flock

examples/src/bin/ holds the source of each, and examples/Flockfile.toml wires them up.

What each one demonstrates

ProgramDemonstratesWatch for
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:

$ examples/polyglot/setup-venv.sh
$ examples/polyglot/go-http/build.sh
$ examples/polyglot/go-chatty/build.sh
$ export SHEP_HOME="$(mktemp -d)"
$ ./target/release/shep start examples/Flockfile.polyglot.toml
careful
A missing runtime does not sink the whole file, but it does sink everything ordered after it. 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":

examples/Flockfile.polyglot.toml
[[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:

shep.toml
[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:

examples/Flockfile.polyglot.toml
[[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.

$ ./target/release/shep trigger '*chatty' ping
ID NAME OUTCOME DETAIL
20 chatty replied pong from rust pid=40076, up 7.1s, level info
10 go-chatty replied pong from go pid=40075, up 7.1s, level info
8 node-chatty replied pong from node pid=40077, up 7.1s, level info
9 python-chatty replied pong from python pid=40078, up 7.1s, level info
$ ./target/release/shep trigger '*chatty' nonsense
ID NAME OUTCOME DETAIL
20 chatty replied unknown action: nonsense
10 go-chatty replied unknown action: nonsense
8 node-chatty replied unknown action: nonsense
9 python-chatty replied unknown action: nonsense

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:

$ ./target/release/shep serve examples/polyglot/static-site --port 19098

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:

examples/Flockfile.toml
[app.readiness_probe]
kind     = "exec"
target   = "../target/release/ready_when_told /tmp/shep-examples-sentinel"
interval = "3s"
timeout  = "1s"
$ touch /tmp/shep-examples-sentinel

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:

$ ./target/release/shep stop stubborn

Roughly three seconds pass — one "caught SIGTERM, still not dying" line in its log, then silence — before shep flock reports it stopped by SIGKILL.