docs · pre-release
Menu
Reference / Flockfile reference

Flockfile reference

Getting started showed a two-field Flockfile and moved on. This page is what's underneath: every field AppConfig 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.

Or start from a generated one

shep init
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.

Flockfile.toml
# 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 92 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.

note
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 to shep 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 unattended shep 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 what shep import pm2 is for: it reads a real dump.pm2 and writes a Flockfile, once, rather than teaching start a second config dialect. See Coming from pm2.

careful
Without --flockfile, shep start server.js still 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:

Durations
^\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.

Sizes
^\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:

Flockfile.toml → shep start
$ 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, and the message names the exact key including which app it sat under:

max_memory_restart is not a real field
$ shep start Flockfile.toml
error[invalid_config]: Flockfile names unrecognized key: app.0.max_memory_restart

A key at the top level rather than inside an app is reported by the parser itself, so that one does name the alternatives: unknown field \`apps\`, expected one of \`$schema\`, \`dog\`, \`app\`. The per-app set is large enough that listing it would bury the key you actually mistyped, which is what the reference below is for.

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}}, {{name}}, {{SHEP_HOME}} and {{secret:...}}

env values, args, out_file and err_file all accept doubled-brace tokens. Three are filled in at spawn time: {{instance}} and {{name}} from the sheep's own identity, {{SHEP_HOME}} from the directory the shepherd keeps its state in. Every one of the four fields takes all three:

Flockfile.toml
[[app]]
name = "z-worker"
script = "./z-worker"
instances = 4
args = ["--metrics-port", "91{{instance}}"]
out_file = "{{SHEP_HOME}}/logs/{{name}}-{{instance}}-out.log"

[app.env]
Z_WORKER_ID = "z-{{instance}}"
Z_DEVICE_ID = "z-{{instance}}d"

That out_file line is the default written out, so uncommenting it changes nothing. Pair it with merge_logs and it does: an explicit path spelling {{instance}} keeps the instances in separate files anyway. shep init --all scaffolds the line, and a path built on {{SHEP_HOME}} moves with the shepherd rather than pinning the Flockfile to one machine's layout. {{SHEP_HOME}} is $SHEP_HOME when that is set and ~/.shep when it is not. It renders absolute, so unlike a relative path it is never joined onto cwd, and it renders the same for every instance, so a clustered app still needs {{instance}} or merge_logs in its log paths.

Casing says where a token reads from

An uppercase name is something from the environment, a lowercase one is something from the config. SHEP_HOME is an environment variable, so the token is {{SHEP_HOME}}; name and instance are fields of the app, so they stay lowercase. {{secret:API_KEY}} is both halves at once: secret: names the mechanism, and the key beside it is an environment-variable-shaped name. A secret key is matched as written, so the uppercase there is a convention rather than a rule.

The fourth token reads a value rather than the app's own identity: {{secret:NAME}} looks up NAME in the secrets store instead of putting a live value in the Flockfile at all. This one is for env values and args only: out_file and err_file refuse it, because a resolved log path becomes a filename on disk and is reported by shep flock and shep describe.

Flockfile.toml
[app.env]
DB_PASSWORD = "{{secret:DB_PASSWORD}}"

Any other {{...}} is refused at shep start, not left to reach the child as a literal string:

a typo'd token
error[invalid_config]: the daemon reported InvalidConfig: sheep `z-worker`, env.WORKER: `{{instnace}}` is not a template token: valid tokens are `{{instance}}`, `{{name}}`, `{{SHEP_HOME}}`, `{{secret:...}}`

An opening {{ that is never closed is refused the same way, rather than passed through as a literal:

an unclosed token
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, SHEP_NAME and SHEP_ENVIRONMENT are set on every sheep's environment automatically, whether or not the Flockfile uses any template at all. Setting one yourself under env is refused rather than silently overwritten:

env sets a reserved variable
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 variable

Changing the count without editing the file

shep stockscale
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  CFG  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  CFG  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  CFG  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 optional interval (10s), timeout (5s), and failure_threshold (3). depends_on has a page of its own: see boot order.

Lookout's config pane builds its form straight off this same Flockfile JSON Schema, the way it builds one for a dog's own --schema answer. A row it cannot edit says why: = for read-only, like name, and ~ for a shape with no widget, like the probes. dogs opens a sub-screen instead, editing each dog's table through that dog's own schema rather than this one.

Identity & process

FieldDefaultWhat it does
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}}, {{name}}, {{SHEP_HOME}} and {{secret:...}}. A resolved secret in an argument is visible in ps; see Secrets.
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}}, {{name}}, {{SHEP_HOME}} and {{secret:...}}. A value may be a string, a bare boolean, or a bare whole number (SOME_BOOL = true, PORT = 8080), and is read as its string either way. A float is refused, since 1.10 would reach the process as 1.1. In YAML an unquoted yes reads as "true" and 0x1F as "31", so quote a value whose text must survive. SHEP_INSTANCE, SHEP_NAME and SHEP_ENVIRONMENT are always set and may not be set here.
environmentstring?[daemon] environment, "production" by defaultWhich environment a {{secret:...}} reference resolves against on this sheep. May not be "all", the secrets store's own every-environment slot; see Secrets.
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}}, {{name}} and {{SHEP_HOME}} templates.
dogsmap<string,table>{}One table per dog, keyed by dog name, for this sheep alone. shep stores it and hands it to the dog it names, without reading a key inside. The name picks a table and proves nothing: any client that can reach the shepherd's socket can read every dog's tables. A dog name need not be one shep knows about yet. Never read at a spawn or a decision, so a write takes effect the moment it lands. Lookout's dogs row opens a sub-screen over these tables, one dog at a time, editable when that dog publishes a schema for it.

Restart policy

FieldDefaultWhat it does
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

FieldDefaultWhat it does
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

FieldDefaultWhat it does
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

FieldDefaultWhat it does
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

FieldDefaultWhat it does
max_memorysize?noneMemory ceiling: the polling enforcer restarts the sheep above it.
foldstring?noneNames the fold (group) this sheep belongs to.
depends_onstring[][]Sheep to start before this one, when shep is starting more than one thing at once. Naming a dog only warns and this app starts anyway; [daemon] boot_first_dogs is the only lever that moves a dog. Ordering, not a guarantee: a dependency with no readiness signal is waited out rather than waited for, and one that is missing or sets autostart = false warns and lets this app start anyway. Names, never name:slot. Read once when a batch is ordered, so an edit reaches the next boot, muster, staged start, restart or reload rather than the running child.
userstring?noneRun as this user (unix).
groupstring?noneRun as this group (unix).

Logs

FieldDefaultWhat it does
out_filestring?{{SHEP_HOME}}/logs/{{name}}-{{instance}}-out.logStdout log file path override; supports {{instance}}, {{name}} and {{SHEP_HOME}}. A relative path lands under the app's cwd, and a {{SHEP_HOME}} one is absolute already. 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?{{SHEP_HOME}}/logs/{{name}}-{{instance}}-err.logStderr log file path override; supports {{instance}}, {{name}} and {{SHEP_HOME}}. A relative path lands under the app's cwd, and a {{SHEP_HOME}} one is absolute already. 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.
log_timestampsbooltrueStart each line in out_file and err_file with the time it was written. Set false to keep the lines exactly as the app wrote them, which is pm2's default; shep start --no-log-timestamps does the same for a sheep started without a Flockfile. Only the files change: shep bleats shows the same lines either way.
level_rules{ pattern, level }[][]How this app's own lines announce a level, for lookout's level filter. Each pattern is a regex and each level is one of trace, debug, info, warn, error; the first rule that matches a line wins, so order them. An invalid pattern is refused at load, and so is a list of more than 64 rules, since a client recompiles the whole list on every redraw. Declared rules replace shep's built-in reading for this app rather than adding to it, which is how an app whose lines it misreads turns the guessing off. A line no rule matches carries no level and passes every filter.

Networking & probes

FieldDefaultWhat it does
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.
note
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 default exp_backoff_restart_delay of 100ms, growing ×1.5 each attempt, changes how long that takes. At the default max_restarts = 16, reaching Errored now takes roughly 50-70s instead of well under a second. Set exp_backoff_restart_delay = "0" to go back to failing fast, provided restart_delay is unset -- a fixed restart_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.json for 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 -20 stops 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 and dog = ["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.