Add 9router start-on-boot setup (non-root, @reboot cron, no systemd)

This commit is contained in:
cania
2026-08-11 11:30:57 +02:00
commit 40ec0c8506
4 changed files with 203 additions and 0 deletions

136
README.md Normal file
View File

@@ -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.