Your first Flockfile
Getting started showed a two-field Flockfile and moved on. This page is what's underneath: every fieldAppConfig understands, the four formats and ten filenames config discovery will find on its own, and the grammar that turns a typo into a parse error instead of a surprise at 3am.
Two fields is still a complete one
name and script are the only two fields with no default. Everything else in the reference table below fills in on its own. autorestart and autostart both default to true, so a bare two-field entry is already a supervised, auto-starting sheep.
[[app]] name = "web" script = "./server"
Or start from a generated one
shep init [path] [--all] [--force]Writes a commented Flockfile to fill in. With no path that's Flockfile.toml in the current directory; give it one and the extension picks the language, out of .toml, .yaml, .yml, .json and .json5.
# Manage your app in a Flockfile # Add as many apps as you would like using TOML syntax #[[app]] # A convenient and unique name for shep to display #name = "my-first-sheep" # The script that shep should use to launch your app #script = "./index.js" # Restarts the process automatically when it exits unexpectedly #autorestart = true # Where the process runs. Without it, the daemon's own directory #cwd = "/srv/app"
Almost every line is commented out, and uncommenting the ones you want is the whole workflow. The generator builds a real document first and comments it in a single pass afterwards, which is why the same scaffold exists in four languages instead of one: the comment marker never touches the structure. A test uncomments each format mechanically and parses the result, so this is a Flockfile that works rather than prose shaped like one.
--all writes every field the grammar has instead of the handful worth meeting on day one, which is 82 lines of TOML against the 12 above. --force replaces a Flockfile that is already there, keeping that file's format rather than the default one, so forcing over a .yaml writes YAML instead of dropping TOML into it. Without --force, an existing Flockfile is a refusal and nothing is written.
JSON has no comment syntax, so the thing this verb makes cannot be made in it. A .json scaffold is a live minimal document with real values and no guidance, and --all is refused there outright rather than fudged, because a JSON document naming every field would pin every default explicitly. That is a Flockfile you would tell somebody not to commit.
$ shep init deploy.json --all error[usage]: JSON has no comment syntax, so a full scaffold would pin every default instead of explaining it; write a .json5 Flockfile for the same syntax with comments, or drop --all
Writing a second Flockfile beside an existing one is legal and said out loud rather than refused, since somebody naming a path explicitly may well be migrating. Discovery walks the fixed order below and stops at the first hit, so the new file can be one shep never reads; shep init prints an init_shadowed notice naming both files when it spots that.
Formats and discovery
TOML, YAML, JSON, and JSON5 all parse the same document shape: a list of app tables under one app key ([[app]] in TOML). shep start always takes an explicit<TARGET>: clap refuses with "the following required arguments were not provided" if you leave it off. Discovery belongs toshep runtime and shep dev, whose[TARGET] is optional: run either with no argument and it searches the current directory for these ten names, in order, and stops at the first one it finds:
Flockfile.toml Flockfile.yaml Flockfile.yml Flockfile.json Flockfile.json5 flockfile.toml flockfile.yaml flockfile.yml flockfile.json flockfile.json5
.js is a fifth format, and deliberately not on that list. Reading one means running node on it (arbitrary code execution), so discovery never selects a .js file no matter what's sitting in the directory. It's read only when you name it explicitly with --flockfile:
$ shep start flock.config.js --flockfile
shep gives node 30 seconds to hand the config back and exit, then kills it. Exporting the config is not enough by itself: a module that leaves a server listening or a timer armed keeps node's event loop alive, so node never exits. An unattendedshep start ends in a refusal instead of waiting forever.
A pm2 ecosystem.config.js is not a Flockfile shep can read this way. shep never parses pm2's config format at all. That's whatshep import is for: it reads a real dump.pm2and writes a Flockfile, once, rather than teaching starta second config dialect. See Coming from pm2.
Without --flockfile, shep start server.jsstill means exactly what it always has: run server.js as a script, not as config. cd-ing into a cloned repository and running shep runtime or shep dev with no argument must never execute a stranger's JavaScript on your machine as a side effect of looking for config. That's the whole reasoning behind the ten-name list above stopping at JSON5.
Strict grammars
Two field types are small parsers of their own rather than bare numbers, so a typo in either fails at parse time instead of silently meaning something else. Both accept plain digits with one optional trailing unit, nothing more:
^\d+(ms|h|m|s)?$Plain digits are milliseconds; ms/h/m/s are milliseconds/hours/minutes/seconds. Lowercase only, and ms is checked before m so a trailing m on its own still means minutes.
^\d+(G|M|K)?$Plain digits are bytes; K/M/G are binary units: KiB/MiB/GiB, not decimal. Uppercase only, and each is exactly one letter.
512M and 30s parse. 512MB,1.5G, and 30S all fail: a fractional size, a two-letter unit, and an uppercase duration suffix, each rejected by the same strict grammar rather than rounded, guessed, or silently dropped:
$ shep start Flockfile.toml error[invalid_config]: invalid TOML Flockfile: TOML parse error at line 4, column 14 | 4 | max_memory = "512MB" | ^^^^^^^ memory size must be ASCII digits with an optional trailing G, M, or K
Unknown fields are a parse error, not a shrug
Every Flockfile field name is checked against the real set. A typo'd key is rejected at load, not ignored. This is the same guarantee spec writers get from #[serde(deny_unknown_fields)], surfaced as a message that names the exact key and lists what would have been accepted:
$ shep start Flockfile.toml error[invalid_config]: invalid TOML Flockfile: TOML parse error at line 4, column 1 | 4 | max_memory_restart = "1G" | ^^^^^^^^^^^^^^^^^^ unknown field `max_memory_restart`, expected one of `name`, `script`, `args`, `cwd`, `interpreter`, `env`, `instances`, `autorestart`, `autostart`, `stop_exit_codes`, `min_uptime`, `max_restarts`, `restart_delay`, `exp_backoff_restart_delay`, `kill_signal`, `kill_timeout`, `shutdown_with_message`, `listen_timeout`, `graceful_timeout`, `action_timeout`, `max_memory`, `watch`, `ignore_watch`, `watch_delay`, `cron_restart`, `fold`, `user`, `group`, `out_file`, `err_file`, `merge_logs`, `channel`, `stdin`, `wait_ready`, `reuse_port`, `readiness_probe`, `liveness_probe`, `watch_options`, `cron_timezone`, `increment_var`
A parsed document with zero [[app]] entries is its own distinct error, Flockfile declares no apps: an empty file isn't a no-op, since shep start with nothing to start almost always means the path was wrong.
Paths
Four fields hold one: script, cwd,out_file and err_file. All four expand a leading ~/ to your home directory, because a Flockfile is read from a file rather than typed at a shell, and nothing would otherwise expand it: shep would look for a directory literally named~.
Deliberately narrow. ~user/ is refused rather than resolved, since answering it means a passwd lookup whose result depends on which user the daemon runs as rather than on who wrote the file. $VAR is never expanded: once a config file expands variables it has to decide whose environment it means, and there is no good answer to that.
Templates: {{instance}} and {{name}}
env values, args, out_file anderr_file all accept two tokens, doubled-brace, filled in at spawn time with the app's own name and the instance's own slot:
[[app]]
name = "z-worker"
script = "./z-worker"
instances = 4
args = ["--metrics-port", "91{{instance}}"]
[app.env]
Z_WORKER_ID = "z-{{instance}}"
Z_DEVICE_ID = "z-{{instance}}d"Any other {{...}} is refused at shep start, not left to reach the child as a literal string:
error[invalid_config]: the daemon reported InvalidConfig: sheep `z-worker`, env.WORKER: `{{instnace}}` is not a template token: valid tokens are `{{instance}}` and `{{name}}`An opening {{ that is never closed is refused the same way, rather than passed through as a literal:
error[invalid_config]: the daemon reported InvalidConfig: sheep `z-worker`, env.WORKER: a `{{` in this value is never closed by a `}}`Doubling the braces escapes them: {{{{ and}}}} render as one literal {{ and}}. Single braces are left alone and never need escaping, so a JSON blob, a regex quantifier like{2,3}, or a Go or Helm template passed through as an arg all survive untouched.
SHEP_INSTANCE and SHEP_NAME are set on every sheep's environment automatically, whether or not the Flockfile uses either template. Setting either one yourself under env is refused rather than silently overwritten:
error[invalid_config]: the daemon reported InvalidConfig: sheep `z-worker` sets `SHEP_INSTANCE` in env, but shep injects it: use a different name, or `{{instance}}` in your own variableChanging the count without editing the file
shep stock <name> <n>An absolute count, not a change: shep stock web 4 means web has four instances afterwards, whatever it had before. There is no +N or -N form, so running it twice leaves the same flock as running it once.
$ shep stock web 4 ID NAME STATUS PID RESTARTS EXIT CPU MEM UPTIME FOLD SMIT 0 web:0 online 62888 0 - - 3.0M 0s - - 1 web:1 online 62889 0 - - 3.0M 0s - - 3 web:2 online 62936 0 - - 176.0K 0s - - 4 web:3 online 62937 0 - - 144.0K 0s - - 2 worker online 62890 0 - - 3.0M 0s - - $ shep stock web 2 ID NAME STATUS PID RESTARTS EXIT CPU MEM UPTIME FOLD SMIT 0 web:0 online 62888 0 - - 3.0M 1s - - 1 web:1 online 62889 0 - - 3.0M 1s - - 3 web:2 online 62936 0 - - 0B 1s - - 4 web:3 online 62937 0 - - 0B 1s - - 2 worker online 62890 0 - - 3.0M 1s - - $ shep flock ID NAME STATUS PID RESTARTS EXIT CPU MEM UPTIME FOLD SMIT 0 web:0 online 62888 0 - - 3.0M 2s - - 1 web:1 online 62889 0 - - 3.0M 2s - - 2 worker online 62890 0 - - 3.0M 2s - -
Stocking up fills the lowest free slots, and stocking down releases the highest, so stocking out and back gives you the same slot numbers, the same SHEP_INSTANCE values and the same log files you started with. Slots are not ids: web:2 above arrived with id 3, because an id is handed out once per sheep and a slot describes its place in the app.
The second command printed four instances because it answers the moment the shepherd accepts, and the two on their way out were still running their stop ladders at that point. They report themselves on the bus under process.delete, and a listing a moment later shows the flock settled. The new count goes into the muster roll, so a reboot keeps it.
The full field reference
Every field AppConfig accepts, grouped by what it's for.readiness_probe and liveness_probe each take a nested object: kind (http/tcp/exec), target, and optionalinterval (10s), timeout (5s), andfailure_threshold (3).
Identity & process
namestringrequiredUnique sheep name. May not contain a path separator or a colon: a colon is the name:slot selector's own separator, and also illegal in a log filename on Windows.scriptstringrequiredExecutable or script path.argsstring[][]Arguments passed to the script; supports {{instance}} and {{name}}.cwdstring?the Flockfile's own directoryWorking directory at spawn. Unset, an app in a Flockfile runs where that Flockfile lives, so a relative script resolves the way you would read it.interpreterstring?noneInterpreter override; "none" runs the script directly.envmap<string,string>{}Merged over the daemon's filtered environment; values support {{instance}} and {{name}}. SHEP_INSTANCE and SHEP_NAME are always set and may not be set here.instancesinteger1How many separate copies of this app to run, each its own process; shep does not share a socket between them (see reuse_port if the app does that itself). SHEP_INSTANCE and SHEP_NAME are always set on every instance; other env values, args, out_file and err_file may use {{instance}} and {{name}} templates.increment_varstring?removedRefused at normalize with the replacement named: set env.YOUR_VAR = "{{instance}}" instead.Restart policy
autorestartbooltrueRestart on unexpected exit.autostartbooltrueStart when the daemon starts, or on shep muster.stop_exit_codesint[][]Exit codes treated as a clean stop, not a restart.min_uptimeduration1sBelow this, an exit counts as unstable.max_restartsinteger16Consecutive unstable exits before errored.restart_delayduration?noneFixed delay before every restart (alternative to backoff).exp_backoff_restart_delayduration?100msInitial backoff; grows ×1.5, capped at 15s. Only applies to unstable exits, and only while restart_delay is unset -- a configured restart_delay wins even over "0" here.Stopping
kill_signalstring?SIGTERMOne of SIGTERM/SIGINT/SIGQUIT/SIGUSR2: the SIG prefix and case are both optional.kill_timeoutduration1.6sGrace period between the stop signal and SIGKILL.shutdown_with_messageboolfalseSend {"kind":"shutdown"} on the shepherd channel instead of a signal.graceful_timeoutduration8sDrain window for the old instance during a reload.Startup signaling
listen_timeoutduration3sReadiness fallback when no ready signal or probe is configured.wait_readyboolfalseExpect {"kind":"ready"} on the shepherd channel before treating the sheep as up.channelboolfalseOpen the shepherd channel on fd 3, even without wait_ready or shutdown_with_message.stdinboolfalseOpen a pipe on the sheep's stdin so shep whisper can write to it.action_timeoutduration3sHow long a triggered action gets to answer before it's TimedOut.Watching & scheduling
watchboolfalseRestart when watched files change.watch_optionsstring[][]Include globs; empty means watch the whole cwd.ignore_watchstring[][]Exclude globs, on top of the daemon's own dot-entry and node_modules defaults.watch_delayduration?500msDebounce window before a watch-triggered restart.cron_restartstring?noneCroner-dialect cron pattern for scheduled restarts.cron_timezonestring?noneIANA timezone name for cron_restart.Resources & organization
max_memorysize?noneMemory ceiling: the polling enforcer restarts the sheep above it.foldstring?noneNames the fold (group) this sheep belongs to.userstring?noneRun as this user (unix).groupstring?noneRun as this group (unix).Logs
out_filestring?logs/<name>-<i>-out.logStdout log file path override; supports {{instance}} and {{name}}. With instances > 1, an explicit path needs {{instance}} or merge_logs, or it's refused: {{name}} renders the same for every instance, so a path carrying only that one still collides.err_filestring?logs/<name>-<i>-err.logStderr log file path override; supports {{instance}} and {{name}}. With instances > 1, an explicit path needs {{instance}} or merge_logs, or it's refused: {{name}} renders the same for every instance, so a path carrying only that one still collides.merge_logsboolfalseCollapse every instance's logs into one file pair.Networking & probes
reuse_portboolfalseAsserts the app sets SO_REUSEPORT itself, which lets a reload overlap the old and new instance. A probed app that leaves this off is reloaded serially instead: the old instance drains before the new one starts.readiness_probeobject?noneHTTP/TCP/exec probe gating reload's await-ready step.liveness_probeobject?noneHTTP/TCP/exec probe whose failures feed the restart policy.A permanently broken app (bad config, missing dependency: it exits before min_uptime every time) still errors out once it hits max_restarts, but the defaultexp_backoff_restart_delay of 100ms, growing ×1.5 each attempt, changes how long that takes. At the defaultmax_restarts = 16, reaching Errored now takes roughly 50-70s instead of well under a second. Setexp_backoff_restart_delay = "0" to go back to failing fast, provided restart_delay is unset -- a fixedrestart_delay takes precedence and still applies its own wait on every restart, even with the backoff disabled.
Get the schema directly
Everything above is generated from the same source the parser uses, so it can't drift from what shep actually accepts.shep schema prints it as JSON Schema, and the same output is committed at crates/shep-core/assets/flockfile.schema.jsonfor an editor to point at:
$ shep schema | head -20
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Flockfile",
"type": "object",
"properties": {
"$schema": {
"description": "The editor's schema hint, read and discarded.\n\nThis is the \"future schema key\" the comment above anticipated, added\nHERE explicitly rather than by relaxing `deny_unknown_fields`: a\ntypo'd key must still fail loudly, and exactly one more key is now\nlegal. shep does not validate against the named schema and makes no\npromise about it — it is a hint for the operator's editor, which is\nthe only consumer that ever reads it.\n\nTOML Flockfiles do not need it: taplo's `#:schema <url>` directive is\na comment, invisible to serde. JSON and JSON5 have no comment an\neditor agrees to look in, which is why this field exists at all.",
"type": [
"string",
"null"
],
"default": null
},
"app": {
"type": "array",
"default": [],
"items": {
"$ref": "#/$defs/AppConfig"
}
},That's the real first twenty lines, unedited. head -20stops just after "app" closes and before"dog" begins, and the "$schema"key's own description is one long escaped line explaining itself, not the tidied multi-line prose above. The full document closes"app", adds the "dog" key described below, then "additionalProperties": false and a"$defs" section with one entry per Flockfile field. Run the command yourself to see the rest.
The "dog" key is the other one shep accepts and ignores. A dog with per-app configuration has nowhere else to put it: it belongs beside the app it describes, in the repository the dog deploys. Before the key existed, a Flockfile carrying any of it was refused outright, so an operator could not register their app at all. shep does not read what is under dog, does not validate it, and promises nothing about it. The table itself is required, though: dog = 5 anddog = ["a"] are refused, because otherwise this would be the one key in the document where a typo does not fail loudly. What is inside it is the dog's: the dog that owns a key is the only thing that understands it, and shep refusing a document because it does not recognise another program's config is a coupling neither side wants. Everything else still fails loudly, which is why it is one nested table rather than loose top-level keys.
That "$schema" key is real and accepted: a JSON or JSON5 Flockfile can set it to a path and get editor completion, since neither format has a comment syntax an editor would look inside for a schema hint. shep itself reads the key and discards it; it never validates against whatever it points to. TOML doesn't need the key at all: a leading #:schema <path> comment (the taplo convention) does the same job, invisibly to the parser.