From f6b53620e9d367f81025323623d7f4f19e8d31ef Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 6 Jun 2026 18:05:53 +0200 Subject: [PATCH] feat(deploy): systemd user unit + install script for membershipd Add deploy/unibus-membershipd.service (Restart=always, binds both planes to 0.0.0.0 for LAN reachability), an idempotent deploy/install.sh that builds the binary, symlinks the unit, and enables+starts it, plus deploy/README.md with operate/health instructions. Restart=always is deliberate: a clean SIGTERM exits 0 and Restart=on-failure would not restart it, leaving the service silently dead (the sqlite_api gotcha). Co-Authored-By: Claude Opus 4.8 (1M context) --- deploy/README.md | 67 +++++++++++++++++++++++++++++++ deploy/install.sh | 31 ++++++++++++++ deploy/unibus-membershipd.service | 22 ++++++++++ 3 files changed, 120 insertions(+) create mode 100644 deploy/README.md create mode 100755 deploy/install.sh create mode 100644 deploy/unibus-membershipd.service diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..a94ad60 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,67 @@ +# Running membershipd as a systemd user service + +`membershipd` is the unibus control plane (rooms, members, sealed keys, blob +store) and, unless you point it at an external NATS with `--nats-url`, it also +runs the embedded NATS + JetStream data plane. Running it as a **systemd user +service** keeps it alive across logout/reboot and restarts it if it crashes. + +The unit (`unibus-membershipd.service`) binds both planes to `0.0.0.0`: + +| Plane | Port | Reachable from | +|--------------|-------|----------------| +| HTTP control | 8470 | LAN (`http://:8470/healthz`) | +| NATS data | 4250 | LAN (`nats://:4250`) | + +## Install (idempotent) + +```bash +cd ~/fn_registry/projects/message_bus/apps/unibus +./deploy/install.sh +``` + +This builds the binary, symlinks the unit into `~/.config/systemd/user/`, +reloads systemd, and enables + starts the service. + +## Manual steps (what install.sh does) + +```bash +cd ~/fn_registry/projects/message_bus/apps/unibus + +# 1. Build the pure-Go binary (no CGO). +CGO_ENABLED=0 go build -o membershipd ./cmd/membershipd + +# 2. Link the unit into the systemd user directory. +mkdir -p ~/.config/systemd/user +ln -sf "$PWD/deploy/unibus-membershipd.service" ~/.config/systemd/user/unibus-membershipd.service + +# 3. Reload, enable (start on login) and start now. +systemctl --user daemon-reload +systemctl --user enable --now unibus-membershipd.service + +# (optional) survive logout without an active session: +# sudo loginctl enable-linger "$USER" +``` + +## Operate + +```bash +systemctl --user status unibus-membershipd.service # is it active? +systemctl --user restart unibus-membershipd.service # after a rebuild +systemctl --user stop unibus-membershipd.service +systemctl --user disable unibus-membershipd.service # stop starting on login +journalctl --user -u unibus-membershipd.service -f # follow logs + +# Health (local and from another LAN host): +curl -fsS http://127.0.0.1:8470/healthz +curl -fsS http://:8470/healthz +``` + +## Notes + +- Writable state (SQLite DB, JetStream store, blobs) lives under `local_files/` + relative to `WorkingDirectory`, which the unit sets to the app directory. +- After editing the app code, rebuild (`CGO_ENABLED=0 go build -o membershipd + ./cmd/membershipd`) and `systemctl --user restart unibus-membershipd.service`. +- To run against an external NATS instead of the embedded one, append + `--nats-url nats://:4222` to `ExecStart` and re-run `daemon-reload` + + `restart`. diff --git a/deploy/install.sh b/deploy/install.sh new file mode 100755 index 0000000..d17ba11 --- /dev/null +++ b/deploy/install.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Build membershipd and install/enable/start it as a systemd user service. +# Idempotent: safe to re-run after a code change to rebuild and restart. +set -euo pipefail + +APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +UNIT="unibus-membershipd.service" +USER_UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" + +cd "$APP_DIR" + +echo "==> building membershipd (CGO_ENABLED=0)" +CGO_ENABLED=0 go build -o membershipd ./cmd/membershipd + +echo "==> linking unit into $USER_UNIT_DIR" +mkdir -p "$USER_UNIT_DIR" +ln -sf "$APP_DIR/deploy/$UNIT" "$USER_UNIT_DIR/$UNIT" + +echo "==> reloading systemd and (re)starting the service" +systemctl --user daemon-reload +systemctl --user enable --now "$UNIT" +# If the service was already running, enable --now does not restart it; do so to +# pick up the freshly built binary. +systemctl --user restart "$UNIT" + +echo "==> status" +systemctl --user --no-pager status "$UNIT" || true + +echo +echo "Health check:" +echo " curl -fsS http://127.0.0.1:8470/healthz" diff --git a/deploy/unibus-membershipd.service b/deploy/unibus-membershipd.service new file mode 100644 index 0000000..f8c3398 --- /dev/null +++ b/deploy/unibus-membershipd.service @@ -0,0 +1,22 @@ +[Unit] +Description=unibus membershipd — control plane (rooms, keys, blobs) + embedded NATS/JetStream +Documentation=https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/unibus +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +WorkingDirectory=%h/fn_registry/projects/message_bus/apps/unibus +# --bind 0.0.0.0 exposes BOTH the HTTP control plane (:8470) and the embedded +# NATS data plane (:4250) to the LAN so phones / other PCs can connect. +ExecStart=%h/fn_registry/projects/message_bus/apps/unibus/membershipd --bind 0.0.0.0 +# Restart=always (NOT on-failure): a clean SIGTERM shutdown exits 0, and +# on-failure would then NOT restart, leaving the service silently dead. always +# brings it back regardless of exit code. See .claude/rules/function_tags.md. +Restart=always +RestartSec=2 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=default.target