docs · pre-release
Menu
Deploying / Containers

Containers

Two verbs, one shape: run one Flockfile's flock in the foreground, no daemonization, exit when there's nothing left to supervise.shep runtime is that shape for a container.shep dev is the same shape for a laptop, with isolation swapped in for the container's own kind of disposability.

shep runtime: the container entrypoint

shep runtime boots a shepherd in-process, runs one Flockfile's flock, and exits once nothing is left online.--no-restore is always on. A container starts fresh from its Flockfile every time, never from a muster roll left on the image by a previous run. Bleats stream to this process's own stdout/stderr, sodocker logs is the flock's log with no extra plumbing, and the shepherd is still reachable over its own socket the whole time:shep flock from a second terminal, ordocker exec, works exactly as it would against a daemonized one.

Dockerfile
FROM debian:bookworm-slim
COPY shep-runtime /usr/local/bin/shep-runtime
COPY Flockfile.toml /shep/Flockfile.toml
ENV SHEP_HOME=/shep
WORKDIR /shep
ENTRYPOINT ["shep-runtime"]

shep-runtime is a separate binary alias forshep runtime, built so ENTRYPOINT doesn't need to spell the subcommand out. SHEP_HOME is never defaulted for you inside a container. An unset one fails fast naming the flag, rather than inventing /var/lib/shep at 2am.

Exit codes an orchestrator can act on

shep runtime exits 0 once the flock has been empty and clean (every sheep stopped, noneerrored) for three consecutive two-second polls: a batch job finishing its work. It exits 11(flock_empty) instead when the flock emptied with at least one sheep errored (a restart budget exhausted, or a spawn that never came up), so an orchestrator reading the exit status can tell "finished" from "died" and restart the container only for the second.

PID 1 without racing tokio's own reaper

When shep runtime genuinely is PID 1, it splits into a small init process first, rather than reaping orphans inside the supervisor's own process. An in-process subreaper loop would race tokio's own child reaping and corrupt the exact exit statuses shep promises elsewhere. The init instead relies on the kernel already treating real PID 1 as the implicit subreaper (it never callsset_child_subreaper), forwardsSIGTERM/SIGINT/SIGHUP/SIGQUITto the supervisor it spawns, and reaps every orphan itself. A container that skipped this step would accumulate zombies untildocker stop waited out the full grace period before SIGKILLing everything mid-shutdown.

shep dev: the same shape for a laptop

shep dev runs one Flockfile's flock in an isolated, throwaway foreground session: $SHEP_DEV_HOME (default~/.shep-dev), forced watch = true on every app regardless of what the Flockfile says, and a full stop-and-delete teardown the moment the flock empties or the process is signalled, whichever comes first. Either way it leaves nothing running and no shepherd behind; a shep dev that leaked a supervisor would stop being trusted.

real output, 2026-08-16
$ shep dev ./Flockfile.toml
notice[home_ignored]: shep dev ignores --home/$SHEP_HOME; isolation is the whole feature — set $SHEP_DEV_HOME instead
notice[watch_forced]: shep dev: forcing watch on devtest — each app's own `watch` setting is ignored here
careful

--home and $SHEP_HOME are ignored, on purpose. Isolation is the whole feature. Without this, an operator who exports $SHEP_HOME for their real flock would get a "dev" session that shares it and silently forceswatch = true onto production apps.

Where to go next