README
¶
section3
Service supervisor in a single static binary.
section3 starts the services listed in one YAML file, restarts them when they crash, and rotates their logs. It is built to be the entrypoint of a Docker container that runs more than one process, and works the same way outside containers.
CLI
section3 Start the supervisor (default when run without args)
section3 status Show status of all services
section3 status <name> Show status of one service
section3 start <name> Start a service
section3 stop <name> Stop a service
section3 restart <name> Restart a service
section3 reload Reload config (add/remove/redefine services)
section3 tail [-n N] [name] Show last N log lines (default: 20, all if no name)
section3 version Show binary version
section3 self exit Stop all services and exit
section3 self update Update the binary to the latest release
section3 help Show this help
The first invocation starts the daemon; every other command talks to it over a unix socket.
Configuration
section3 reads /workspace/section3.yml:
defaults:
dir: /workspace # working directory for services that don't set their own
restart: always # always | never | on-crash
log_max_size: 10M # log rotation threshold, default 1M
services:
web:
command: /usr/local/bin/my-web-server --port 8080
worker:
command: /usr/local/bin/my-worker --queue default
restart: on-crash
depends_on:
- web
init-once:
command: /usr/local/bin/migrate-db.sh
restart: never
dir: /tmp
dockerd:
command: dockerd-rootless.sh
stop_timeout: 60s # needs longer than the 5s default to stop containers
Restart policies:
always— restart on any exit (default)never— do not restarton-crash— restart only on non-zero exit
Restarts back off exponentially up to 60s; a run of at least 60s counts as
recovery and resets the backoff. section3 reload (or SIGHUP) stops services
removed from the config, starts newly added ones, and restarts any whose
command, dir or restart policy changed. Services whose definition is
unchanged keep running and are not disturbed.
log_max_size, stop_timeout and depends_on do not count as changes: the
first decides where output goes rather than what runs, the second how it stops,
and the third is start ordering that has already happened. All take effect for
the next start of that service.
Docker
section3 works as the container entrypoint, supervising everything inside:
RUN curl -fsSL https://signalshell.com/install-section3 | sh
COPY section3.yml /workspace/section3.yml
ENTRYPOINT ["/usr/local/bin/section3"]
With docker-compose:
services:
app:
build: .
init: true
Use init: true (or docker run --init) so Docker puts a small init
process at PID 1. section3 reaps its own direct children, but when a
service forks and dies, its orphaned children reparent to PID 1, and the
init process reaps those.
Docker lifecycle:
docker stop→ SIGTERM → section3 stops all services gracefully, then exitsdocker kill -s HUP <ctr>ordocker exec <ctr> section3 reload→ config reloaddocker exec <ctr> section3 status→ service status from outside
Alternatively, from an entrypoint script, exec it so signals reach the
supervisor directly:
#!/bin/bash
exec /usr/local/bin/section3
Install
curl -fsSL https://signalshell.com/install-section3 | sh
Downloads the latest release (linux amd64/arm64), verifies its minisign
signature against the same key the binary embeds, and installs to
/usr/local/bin or ~/.local/bin (or $SECTION3_INSTALL_DIR).
It refuses to install anything it cannot verify. If neither minisign nor
openssl is present it stops and says so, rather than continuing unverified.
That closes the one gap in the chain: self update has always checked the
signature, but the first install had no trusted binary to check with, and
until 2026-09-07 it compared a sha256 fetched from the same server as the
binary, which catches a corrupted download and nothing else.
Source: install.sh in this repo, kept byte-identical to install-section3
in the signalshell-landing repo, which is what the URL serves. make check-installer diffs them; sh install_test.sh tests the real thing against
the live release (including that a tampered binary is refused by both
verifiers).
Update later with:
section3 self update # fetch latest release, verify minisign signature, replace binary
A running daemon keeps the old version until it is restarted. Note that restarting the daemon restarts all of its services.
Run
# Start supervisor (blocks)
section3
# Or in background, then check status
section3 &
section3 status
Logs
Logs go to /tmp/section3-logs/<name>.log
Output is piped through the supervisor, so logs rotate even while a service
is running, keeping last 5 versions (<name>.log.1 ... <name>.log.5). The
rotation threshold is log_max_size (default 1MB), settable per service or
in defaults:. The daemon's own section3.log rotates the same way.
Building from source
make build # → bin/section3
make test
Releasing (maintainer)
make release
Bumps VERSION, builds linux amd64/arm64, signs with minisign, uploads the
binaries and install.sh to signalshell.com/releases/section3/, commits and
tags. The self update command verifies against the public key embedded in
selfupdate.go.
Pushing that tag runs .github/workflows/release.yml, which mirrors the
published, signed artifacts onto GitHub Releases. It does not build or sign:
the key stays on the release machine, and publishing unsigned binaries would
reopen the hole install.sh closes. signalshell.com remains the source of
truth for self update.
The public key appears in three places (selfupdate.go, install.sh, the
workflow). The workflow fails if they disagree.
Files
main.go # supervisor source
selfupdate.go # self version/update commands
install.sh # signature-verifying installer (mirrored to the landing site)
install_test.sh # differential test of install.sh against the live release
Makefile # build + release
README.md # this file
SPEC.md # design spec
docs/ # architecture docs
VERSION # version
section3.yml # example config
bin/section3 # compiled binary (gitignored)
Documentation
¶
There is no documentation for this package.