systemd service operation (Linux)
Audience: the operator running brain-server as a Linux systemd service.
Scope: what deploy/install.sh, deploy/systemd/brain-server.service,
deploy/uninstall.sh, and deploy/clean-cycle-check.sh do — and what they
deliberately do not do.
Not this document: filesystem choice, mount options, memory budget, backup ranking, and multi-site shape. Those live in deployment-filesystem.md and the appliance runbook clean-cycle.md. This page complements them; it does not repeat them. General install, configuration, and tiers live in deployment.md.
Honesty posture. Verified 2026-10-06 against the files named above in this tree. Directives, paths, and behaviours below are quoted from those files. The shutdown timings are measured 2026-09-28 on a physical Ubuntu host (14 MB store, 53 KB WAL), as recorded in the unit comments and clean-cycle.md — not re-measured here. If this page and the scripts disagree, the scripts are right.
1. Install flow (deploy/install.sh)
Run as root:
sudo ./deploy/install.sh [--prefix /usr/local] [--data /var/lib/brain-server]
sudo systemctl start brain-server
/usr/local/bin/brain-clean-cycle-check
What the script does, in order:
- Requires root. Exits 1 otherwise (
install.sh must run as root). - Refuses to clobber. If
$DATA/brain.dbexists andBRAIN_FORCEis not1, it exits 1 and prints the upgrade sequence instead:systemctl stop brain-server,cp -a $DATA $DATA.bak.$(date ...), re-run the script,systemctl start brain-server. Deliberate override isBRAIN_FORCE=1. An upgrade that silently overwrites the store is treated as unrecoverable, so the installer will not do it. - Creates the service user. System group and user
brain(groupadd --system,useradd --system --gid brain --home-dir $DATA --shell /usr/sbin/nologin), theninstall -d -m 0750 -o brain -g brainfor$DATAand$DATA/keys, andinstall -d -m 0755for$PREFIX/bin. - Requires local release binaries. It expects executable
target/release/brain-serverandtarget/release/brainrelative to the script (build withcargo build --release --bin brain-server --bin brain). Missing source is a hard error. Before copying, it stops a running instance matched by absolute binary path (pgrep -f "$src"/pkill -TERM -f "$src", up to 60 s wait). It deliberately never matches on the database path or port:BRAIN_DB_PATHlives in the environment, not in argv, sopkill -fon it matches nothing — see also clean-cycle.md. - Installs helpers. Writes
$PREFIX/bin/brain-shutdown-stamp(theExecStopPoststamp writer, §2) and installsdeploy/clean-cycle-check.shas$PREFIX/bin/brain-clean-cycle-check. - Installs the unit. Copies
deploy/systemd/brain-server.serviceto/etc/systemd/system/brain-server.service(mode 0644), then rewrites the data path and prefix actually chosen (sed -i "s#/var/lib/brain-server#$DATA#g; s#/usr/local/bin#$PREFIX/bin#g"), runssystemctl daemon-reload, andsystemctl enable brain-server.service. - Prints the local-block warning. The data volume must be a local
block filesystem (ext4/xfs); a network filesystem cannot provide the
advisory locking and shared memory SQLite WAL requires. The server
refuses to start on one and names the cause (boot check in
src/migration.rs, per the unit comments). Filesystem detail is in deployment-filesystem.md §1.
Defaults are --prefix /usr/local and --data /var/lib/brain-server.
The installed unit, data dir, check binary, start command, and log command
(journalctl -u brain-server -f) are echoed at the end of a successful run.
Custom-path caveat (read before using
--data). The unit file itself is rewritten for your$DATA, but the generated$PREFIX/bin/brain-shutdown-stampstill writes the compiled-in default/var/lib/brain-server/.shutdown-clean, andbrain-clean-cycle-checkdefaults toBRAIN_DB_PATH=/var/lib/brain-server/brain.dbandBRAIN_STAMP=/var/lib/brain-server/.shutdown-cleanunless the corresponding environment overrides are set. A non-default--datainstall must align the stamp path explicitly or the morning check will look in the wrong place. Likewisedeploy/uninstall.shhas no--prefix/--dataflags and removes the default paths only (§3).
2. What the unit does (deploy/systemd/brain-server.service)
Read the unit before editing it. The directives below are verbatim.
Drain on stop
ExecStart=/usr/local/bin/brain-server
KillSignal=SIGTERM
KillMode=mixed
ExecStopPost=/usr/local/bin/brain-shutdown-stamp
systemctl stop brain-server sends SIGTERM. That is the signal the drain
path handles: stop accepting, close the pool, then
PRAGMA wal_checkpoint(TRUNCATE) (src/main.rs: checkpoint-on-shutdown
block; best-effort — a failure is logged, not fatal, because SQLite replays
an un-checkpointed WAL on the next open). ExecStopPost then stamps
date -Is into /var/lib/brain-server/.shutdown-clean. The stamp’s
absence after a stop means the process was killed, not stopped — that is
the signal the morning check reads (§4).
Do not stop the service with pkill -f brain.db. Same reason as the
installer: the database path is not in argv, so the pattern matches nothing
while reporting success.
Timeouts
TimeoutStopSec=30
TimeoutStartSec=90
TimeoutStopSec=30 is the stop budget. Per the unit comments: measured
SIGTERM → exit was 31 ms total, of which wal_checkpoint(TRUNCATE)
was 0.2 ms (14 MB store, 53 KB WAL, physical Ubuntu host, 2026-09-28).
30 s is ~1000× the measured shutdown. The variable term is WAL size at
shutdown, not database size — a write burst leaves a larger -wal and a
larger checkpoint.
A too-short timeout does not lose rows: SQLite replays the WAL on next open
(src/main.rs:89-90). It costs recovery latency and a larger -wal until
it drains. Re-measure against a copy after the store grows materially —
the command is in clean-cycle.md — then update
TimeoutStopSec and record the new figure.
TimeoutStartSec=90 caps the start phase.
Restart
Restart=on-failure
RestartSec=5
A non-clean exit is restarted after 5 s. A clean stop (systemctl stop,
exit 0) is not restarted. Restart=on-failure does not fix a
deterministic boot failure — a bad volume, a refused bind, a missing key
fails the same way every 5 s until the cause is removed. See §5.
Identity, environment, and sandbox
Type=simple
User=brain
Group=brain
WorkingDirectory=/var/lib/brain-server
After=network-online.target
Wants=network-online.target
WantedBy=multi-user.target
Environment=BRAIN_DB_PATH=/var/lib/brain-server/brain.db
Environment=BRAIN_UMP_KEY_DIR=/var/lib/brain-server/keys
Environment=BIND_HOST=127.0.0.1
Environment=BIND_PORT=8765
Environment=RUST_LOG=info
Least-privilege set, verbatim: NoNewPrivileges=true, PrivateTmp=true,
PrivateDevices=true, ProtectHome=true, ProtectSystem=strict with the
single exception ReadWritePaths=/var/lib/brain-server,
ProtectKernelTunables=true, ProtectKernelModules=true,
ProtectControlGroups=true, RestrictSUIDSGID=true,
RestrictRealtime=true, LockPersonality=true, and fully dropped
CapabilityBoundingSet= / AmbientCapabilities=. The server binds an
unprivileged loopback port and writes one directory; it is granted nothing
else. Auth, bind, and provider configuration beyond these five defaults
live in deployment.md and
configuration.md — the unit does not invent them.
3. Uninstall guarantees (deploy/uninstall.sh)
sudo ./deploy/uninstall.sh
- Requires root (
uninstall.sh must run as root). - If
brain-server.serviceis active, stops it (systemctl stop brain-server.service— SIGTERM → drain →wal_checkpoint(TRUNCATE)), thensystemctl disableit. - Removes the unit (
/etc/systemd/system/brain-server.service) and the four binaries (/usr/local/bin/brain-server,/usr/local/bin/brain,/usr/local/bin/brain-clean-cycle-check,/usr/local/bin/brain-shutdown-stamp), thensystemctl daemon-reload.
Guarantee: removes the service, never the state. The data directory
($DATA, default /var/lib/brain-server) is intact and untouched — store,
audit chain, and keys all still there. The server cannot start after this
without a reinstall.
Deliberate data removal is spelled out, not automated. The script instructs:
brain shred --db $DATA/brain.db— asserts byte-level erasure; not optional ceremony.- Remove the directory by hand. The destructive command is deliberately not written out — an operator who types it has decided to.
Limit: the script takes no flags and removes the default
/usr/local/bin/* paths. A custom --prefix/--data install is only
partly uninstalled by it; remove the relocated paths by hand.
4. The morning clean-cycle check (deploy/clean-cycle-check.sh)
Run before opening the console:
/usr/local/bin/brain-clean-cycle-check
Exit 0 is PASS — safe to serve. Non-zero is FAIL with the reason on
stdout, suitable for gating a start script. Overrides:
BRAIN_BIN (default /usr/local/bin/brain),
BRAIN_DB_PATH (default /var/lib/brain-server/brain.db),
BRAIN_STAMP (default /var/lib/brain-server/.shutdown-clean).
Three checks, in script order:
| # | Check | FAIL means |
|---|---|---|
| 1a | PRAGMA integrity_check via sqlite3 (expects ok) | store structurally damaged — stop and investigate; restore from the off-site copy, do not VACUUM in place |
| 1b | PRAGMA journal_mode (expects wal or memory) | volume cannot do WAL — move the data to a local block filesystem (ext4/xfs); cites sqlite.org/lockingv3.html §6.0. memory is the deliberate in-memory test store |
| 2 | brain anchor --db $DB through the server’s own verifier | anchor failed — the off-host anchor no longer matches; possible behind-the-chain tampering |
| 3 | stamp file exists | no clean-shutdown stamp — previous process was killed, not stopped; WAL replays automatically but expect a slower first query and a larger -wal until it drains; investigate what killed it (power, OOM, kill -9, operator) |
Skips are honest, not silent: without sqlite3 installed the script prints
[skip] for integrity and journal mode; without an executable $BIN it
prints [skip] for the audit chain. A PASS with skips is a partial check
— install what is missing before trusting it.
Evening/morning cadence, storage rules, backup rules, and the signed off-site approval live in clean-cycle.md. Single-node and two-site shapes live in deployment-filesystem.md §5.
5. Troubleshooting a failed service
Work in this order. Every command below appears in the scripts or their output, or in the linked runbooks — nothing here is a second way to stop the server.
- Is it the unit or the store?
systemctl status brain-serverandjournalctl -u brain-server -f. Ajournal mode is 'delete', not 'wal'refusal is the storage gate: move the data to a local block filesystem. There is no override, by design. Detail: deployment-filesystem.md §1 and §6. - Was the last stop clean? Run the morning check (§4) and read the stamp line. Missing stamp + slow first query = killed process with WAL replay, not corruption. Find the killer before serving.
- Is it restart-looping?
Restart=on-failurewithRestartSec=5retries a failing boot indefinitely. Stop the loop (sudo systemctl stop brain-server), fix the cause (volume, bind, auth material per deployment.md), then start once. - Did a deploy just land? Confirm the unit at
/etc/systemd/system/brain-server.servicematchesdeploy/systemd/brain-server.serviceplus your--prefix/--datarewrite, thensystemctl daemon-reload. Confirm the binaries in$PREFIX/binare the just-built release pair —install.shrefuses to proceed without them. - Is the check itself degraded?
[skip]lines mean a missingsqlite3or$BIN. Install them and re-run; do not promote a skipped check to a passed one.
Never delete a -wal file by hand, never copy brain.db without its
-wal, and never probe the live database with a tool that opens and
closes it while the service runs (the probe’s close() can drop the
server’s POSIX advisory locks). Ranked copy mechanisms and the close()
hazard are in deployment-filesystem.md §4.
6. Honest limits
- One host, one active, no failover. The unit manages a single
Type=simpleprocess. Losing the host means a restore from the signed off-site copy. Automatic failover and split-brain protection are not built — do not run two actives. Larger shapes are recorded, with unmeasured parts labelled, in deployment-reference-architecture.md. - The stop budget is one measurement, not a law. 30 s covers ~1000× a 31 ms shutdown with a 53 KB WAL (2026-09-28). A store with a far larger WAL at shutdown checkpoints longer. Re-measure per clean-cycle.md after material growth; until then the margin is reasoned, not proven.
- Custom
--prefix/--datainstalls are second-class. The stamp writer keeps the default data path, the check defaults keep the default paths, and uninstall removes the default paths only. Non-default layouts work only with explicitBRAIN_STAMP/BRAIN_DB_PATHalignment and manual uninstall of relocated files. - A skipped check is not a passed check. Without
sqlite3or thebrainCLI the morning script reports[skip]and can still exitPASS. Treat that as unverified, not as healthy. - No compliance conclusion. This page states what the unit and scripts do. Whether a given deployment satisfies any statute or framework is a determination for a qualified assessor (and, in the Philippines, for counsel) — same posture as deployment-filesystem.md §7.