Files
9router_start_onboot/README.md

5.1 KiB

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.


Optional: local watchdog (auto-restart if it dies)

@reboot starts the service at boot but does not restart it if the parent -t process later dies. Add a local watchdog that probes the API every minute and restarts 9router if it isn't healthy.

9router-watchdog.sh checks http://127.0.0.1:20128/v1/models — if it does not return HTTP 200, it calls start-9router.sh again. Run it from root's crontab:

# root crontab — every minute
* * * * * /usr/local/bin/9router-watchdog.sh

Or just run sudo ./install.sh (it registers both the @reboot job and the per-minute watchdog for you).

Notes:

  • The check is a real HTTP 200 probe (not just "is the port open"), so a hung Next.js process that still holds the port will also be detected and restarted.
  • Max recovery delay is ~1 minute (the cron interval).
  • The watchdog runs locally on the same machine — no external monitoring infra needed.

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).
9router-watchdog.sh Local health monitor. Probes /v1/models; restarts via the wrapper if not HTTP 200.
install.sh One-shot setup: installs both scripts, fixes log ownership, and registers the @reboot cron + per-minute watchdog as root.
README.md This file.

Step-by-step (manual)

1. Install the wrapper

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)

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.

sudo crontab -l 2>/dev/null; echo "@reboot /usr/local/bin/start-9router.sh" | sudo crontab -

Verify:

sudo crontab -l
# -> @reboot /usr/local/bin/start-9router.sh

4. Make sure the cron daemon is running

sudo systemctl enable cron
sudo systemctl start cron
systemctl is-active cron   # -> active

5. Disable the (broken) systemd unit if it exists

sudo systemctl disable 9router 2>/dev/null || true

6. Reboot and verify

sudo reboot

After the server is back:

# 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

sudo ./install.sh

Then reboot.


How the wrapper works

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.