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