From 40ec0c85062bc2788f80ff5e06fe393a6b62c8b8 Mon Sep 17 00:00:00 2001 From: cania Date: Tue, 11 Aug 2026 11:30:57 +0200 Subject: [PATCH] Add 9router start-on-boot setup (non-root, @reboot cron, no systemd) --- .gitignore | 9 ++++ README.md | 136 +++++++++++++++++++++++++++++++++++++++++++++++ install.sh | 32 +++++++++++ start-9router.sh | 26 +++++++++ 4 files changed, 203 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 install.sh create mode 100755 start-9router.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd96540 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# ignore OS / editor junk +.DS_Store +*.swp +*~ +.idea/ +.vscode/ + +# don't commit the reboot marker +/tmp/9router-cron-ran diff --git a/README.md b/README.md new file mode 100644 index 0000000..75012e4 --- /dev/null +++ b/README.md @@ -0,0 +1,136 @@ +# 9router Start on Boot (without systemd) + +Make **9router** (AI router & token saver) auto-start as a **non-root user (`ardhi`)** +when the server boots — using a `@reboot` cron job instead of a systemd unit. + +> Proven working on Ubuntu 26.04 (systemd) after a real reboot. + +--- + +## Why not systemd / plain `9router`? + +- Running `9router` **without** `-t` crashes on a headless server (it waits for an + interactive TTY menu that doesn't exist → crash-loop with `EADDRINUSE`). +- A plain `@reboot` cron calling `9router -t ...` directly fails because: + 1. cron runs the job as the **cron user**, not root — so `runuser` inside is rejected + (`runuser: may not be used by non-root users`). + 2. The log file gets created as **root**, so the `ardhi` process can't write to it + (`cannot create /home/ardhi/9router.log: Permission denied`). + 3. The process is killed when cron's boot session tears down + (`Session terminated, killing shell... killed.`). + +This project solves all three with a tiny **wrapper script** + an **adaptive crontab**. + +--- + +## Files + +| File | Purpose | +|------|---------| +| `start-9router.sh` | Detached launcher. Drops to `ardhi` if run as root, runs directly if already `ardhi`. Uses absolute paths (cron `@reboot` has a minimal `PATH`). | +| `install.sh` | One-shot setup: installs the wrapper, fixes log ownership, and registers the `@reboot` cron as **root**. | +| `README.md` | This file. | + +--- + +## Step-by-step (manual) + +### 1. Install the wrapper +```bash +sudo cp start-9router.sh /usr/local/bin/start-9router.sh +sudo chmod +x /usr/local/bin/start-9router.sh +``` + +### 2. Fix log-file ownership (so `ardhi` can write) +```bash +sudo touch /home/ardhi/9router.log +sudo chown ardhi:ardhi /home/ardhi/9router.log +sudo chmod 644 /home/ardhi/9router.log +``` + +### 3. Register the `@reboot` cron as ROOT +> Must be **root's** crontab, because the wrapper uses `runuser` to drop privileges. +```bash +sudo crontab -l 2>/dev/null; echo "@reboot /usr/local/bin/start-9router.sh" | sudo crontab - +``` +Verify: +```bash +sudo crontab -l +# -> @reboot /usr/local/bin/start-9router.sh +``` + +### 4. Make sure the cron daemon is running +```bash +sudo systemctl enable cron +sudo systemctl start cron +systemctl is-active cron # -> active +``` + +### 5. Disable the (broken) systemd unit if it exists +```bash +sudo systemctl disable 9router 2>/dev/null || true +``` + +### 6. Reboot and verify +```bash +sudo reboot +``` +After the server is back: +```bash +# port should be LISTEN +ss -ltnp | grep 20128 + +# process should run as ardhi +ps -eo pid,user,args | grep -E '9router -t|next-server' | grep -v grep + +# live check +curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:20128/v1/models # -> 200 +``` +Dashboard: **http://127.0.0.1:20128/dashboard** +API endpoint for CLI tools: **http://127.0.0.1:20128/v1** + +--- + +## Or just run the installer + +```bash +sudo ./install.sh +``` +Then reboot. + +--- + +## How the wrapper works + +```sh +if [ "$(id -u)" = "0" ]; then + # root (e.g. root's @reboot cron) -> drop to ardhi + setsid runuser -u ardhi -- sh -c "exec '9router' -t -n --host 127.0.0.1 --port 20128 >> LOG 2>&1 < /dev/null" & +else + # already ardhi -> run directly + setsid 9router -t -n --host 127.0.0.1 --port 20128 >> LOG 2>&1 < /dev/null & +fi +``` + +- `setsid` + `< /dev/null` → detaches the process from cron's session so it survives + the boot-session teardown. +- Absolute paths (`/usr/bin/setsid`, `/usr/sbin/runuser`, `/usr/local/bin/9router`) → + required because cron `@reboot` runs with a minimal `PATH`. +- The redirect happens **inside** `runuser`, so `ardhi` (not root) owns the log file. + +--- + +## Notes / caveats + +- `@reboot` cron starts the service at boot but does **not** auto-restart it if the + parent `-t` process dies. For extra safety you could add a per-minute watchdog cron + that checks the port and re-launches the wrapper if needed. +- Change the user (`ardhi`), port (`20128`), or bind host (`127.0.0.1`) by editing the + wrapper directly. +- This setup was validated on the `9router@0.5.50` standalone build, which crashes when + launched without `-t` on a headless machine. + +--- + +## License +MIT — use freely. diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..5ca4e2e --- /dev/null +++ b/install.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# install.sh — set up 9router auto-start on boot (non-root, no systemd). +# Run as root: sudo ./install.sh +set -e + +WRAPPER_SRC="./start-9router.sh" +WRAPPER_DST="/usr/local/bin/start-9router.sh" +LOG="/home/ardhi/9router.log" +USER="ardhi" + +echo "==> Installing wrapper to $WRAPPER_DST" +install -m 0755 "$WRAPPER_SRC" "$WRAPPER_DST" + +echo "==> Preparing log file ($LOG) owned by $USER" +touch "$LOG" +chown "$USER:$USER" "$LOG" +chmod 644 "$LOG" + +echo "==> Registering @reboot cron as ROOT" +# Must be root's crontab (wrapper uses runuser to drop privileges). +( crontab -l 2>/dev/null | grep -v 'start-9router.sh'; echo "@reboot $WRAPPER_DST" ) | crontab - + +echo "==> Ensuring cron daemon is active" +systemctl enable cron >/dev/null 2>&1 || true +systemctl start cron >/dev/null 2>&1 || true + +echo "==> Disabling broken systemd unit (if any)" +systemctl disable 9router >/dev/null 2>&1 || true + +echo "==> Done. Reboot to verify:" +echo " ss -ltnp | grep 20128" +echo " curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:20128/v1/models" diff --git a/start-9router.sh b/start-9router.sh new file mode 100755 index 0000000..d5bc91b --- /dev/null +++ b/start-9router.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# Start 9router detached at boot. +# Runs as the current user; if that's root, drop privileges to 'ardhi'. +# Uses absolute paths (cron @reboot has a minimal PATH). + +SETSID=/usr/bin/setsid +RUNUSER=/usr/sbin/runuser +BIN=/usr/local/bin/9router +LOG=/home/ardhi/9router.log + +touch /tmp/9router-cron-ran 2>/dev/null + +if [ ! -x "$BIN" ]; then + exit 0 +fi + +if [ "$(id -u)" = "0" ]; then + # Running as root (e.g. root's @reboot cron): drop to ardhi. + "$SETSID" "$RUNUSER" -u ardhi -- sh -c \ + "exec '$BIN' -t -n --host 127.0.0.1 --port 20128 >> '$LOG' 2>&1 < /dev/null" & +else + # Already running as ardhi: start directly. + "$SETSID" "$BIN" -t -n --host 127.0.0.1 --port 20128 >> "$LOG" 2>&1 < /dev/null & +fi +disown 2>/dev/null || true +exit 0