From bfa0840b0e90532ac8e2a055370d1be2fe5fc83b Mon Sep 17 00:00:00 2001 From: Martin Dvorak Date: Tue, 29 Sep 2026 15:46:31 +0000 Subject: [PATCH] run the dev server as a system-wide systemd unit (boots without login/linger) --- README.md | 32 ++++++++++++---- agents.md | 29 ++++++++------- deploy/dvorakmortonxd-dev.system.service | 31 ++++++++++++++++ deploy/install-system-service.sh | 47 ++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 20 deletions(-) create mode 100644 deploy/dvorakmortonxd-dev.system.service create mode 100755 deploy/install-system-service.sh diff --git a/README.md b/README.md index 382a40f..00cd7a9 100644 --- a/README.md +++ b/README.md @@ -114,9 +114,24 @@ site. To force the raw server instead, use `npm run dev:next` (don't point a tun ### Running it as a service (survives reboot) -On this host the dev server runs as a **user** systemd unit, so it comes back after a -reboot without a login (linger is enabled). The unit lives in the repo at -[`deploy/dvorakmortonxd-dev.service`](./deploy/dvorakmortonxd-dev.service): +On this host the dev server runs as a **system** systemd unit, so it starts at boot on +its own — no login or linger needed. The unit lives in the repo at +[`deploy/dvorakmortonxd-dev.system.service`](./deploy/dvorakmortonxd-dev.system.service): + +```bash +# installs /etc/systemd/system/dvorakmortonxd-dev.service and enables it +sudo bash deploy/install-system-service.sh # or: doas bash deploy/install-system-service.sh +``` + +Manage it with `systemctl status|restart|stop dvorakmortonxd-dev` and follow logs with +`journalctl -u dvorakmortonxd-dev -f`. It shares `.next/dev/lock` with a manual +`npm run dev`, so stop the service before starting one by hand. + +
+No root? Use the per-user unit instead + +[`deploy/dvorakmortonxd-dev.service`](./deploy/dvorakmortonxd-dev.service) is an equivalent +**user** unit you can install without root: ```bash mkdir -p ~/.config/systemd/user @@ -126,9 +141,10 @@ systemctl --user enable --now dvorakmortonxd-dev.service loginctl enable-linger "$USER" # once, so it starts at boot ``` -Manage it with `systemctl --user status|restart|stop dvorakmortonxd-dev` and follow logs with -`journalctl --user -u dvorakmortonxd-dev -f`. It shares `.next/dev/lock` with a manual -`npm run dev`, so stop the service before starting one by hand. +Drive it with `systemctl --user …` and `journalctl --user -u …`. Run only one of the two — +they contend for `:3000` and `.next/dev/lock`. + +
## 📡 API Routes @@ -172,7 +188,9 @@ src/app/ scripts/ dev-server.mjs → custom dev server: live updates without the dev-only debug surface deploy/ - dvorakmortonxd-dev.service → systemd user unit so the dev server starts at boot + dvorakmortonxd-dev.system.service → systemd unit (system-wide) so the dev server starts at boot + dvorakmortonxd-dev.service → equivalent per-user unit (no root; needs linger) + install-system-service.sh → installs the system unit (run with sudo/doas) public/ → fonts, wallpapers, icons, friend pages, /revival-inc game, llms.txt, robots.txt, sitemap.xml ``` diff --git a/agents.md b/agents.md index e69c303..8dc4b1e 100644 --- a/agents.md +++ b/agents.md @@ -249,15 +249,18 @@ list reordering, not free placement. Don't confuse the two. `localhost:3000` keeps the full dev experience; the LAN/tunnel get a plain, live-reloading site. Use `npm run dev:next` only if you explicitly want the raw, unprotected server (never point the tunnel at it). -- The dev server normally runs on this host as a **user** systemd unit - (`~/.config/systemd/user/dvorakmortonxd-dev.service`, kept in-repo at - `deploy/dvorakmortonxd-dev.service`). `Linger=yes` for uid 1000 means it starts - at boot without a login. `systemctl --user restart dvorakmortonxd-dev` reloads - it; logs with `journalctl --user -u dvorakmortonxd-dev -f`. From a bare shell - with no session bus, prefix those with - `XDG_RUNTIME_DIR=/run/user/1000 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus`. - The unit pins `HOSTNAME=0.0.0.0` (the script reads that env var) so the tunnel - can reach :3000, and clears a stale `.next/dev/lock` on start. +- The dev server runs on this host as a **system** systemd unit + (`/etc/systemd/system/dvorakmortonxd-dev.service`, kept in-repo at + `deploy/dvorakmortonxd-dev.system.service`, installed by + `deploy/install-system-service.sh`). It is enabled at `multi-user.target`, so + it starts at boot with no login/linger. `systemctl restart dvorakmortonxd-dev` + reloads it; logs with `journalctl -u dvorakmortonxd-dev -f`. The unit pins + `HOSTNAME=0.0.0.0` (the script reads that env var) so the tunnel can reach + :3000, clears a stale `.next/dev/lock` on start, and sets `HOME` because system + units don't inherit a login environment. A per-user twin + (`deploy/dvorakmortonxd-dev.service`) exists for rootless installs; it needs + `loginctl enable-linger` and is driven via `systemctl --user`. Only one of the + two may run at a time — they contend for :3000 and `.next/dev/lock`. - `allowedDevOrigins` (`next.config.ts`) still matters: the tunnel host and any LAN origin must be listed or Next rejects the HMR WebSocket with a 403 (the client then never hydrates — SSR only, icons stacked at `(0,0)`, dead @@ -309,10 +312,10 @@ list reordering, not free placement. Don't confuse the two. - There is **no test suite**. Verification is `tsc --noEmit`, `eslint`, and `next build`. - A dev server may already be running on port 3000 — on this host it is normally - the `dvorakmortonxd-dev` user systemd service, so check - `systemctl --user status dvorakmortonxd-dev` first. Two dev servers on this - repo collide on `.next/dev/lock`; stop the service (or the old process) - before starting the custom server by hand. + the **system** `dvorakmortonxd-dev` service, so check + `systemctl status dvorakmortonxd-dev` first. Two dev servers on this repo + collide on `.next/dev/lock`; stop the service (or the old process) before + starting the custom server by hand. - Dev-server guard check: `curl -s -o /dev/null -w '%{http_code}' \ http://localhost:3000/__nextjs_server_status` must be `200` (localhost keeps debug), while the same path through the LAN IP or `beta.dvorakmortonxd.dev` diff --git a/deploy/dvorakmortonxd-dev.system.service b/deploy/dvorakmortonxd-dev.system.service new file mode 100644 index 0000000..4274a2f --- /dev/null +++ b/deploy/dvorakmortonxd-dev.system.service @@ -0,0 +1,31 @@ +[Unit] +Description=dvorakmortonxd.dev dev server (custom Next.js dev wrapper) +Documentation=file:///home/martin/dvorakmortonxd.dev/README.md +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=martin +Group=martin +WorkingDirectory=/home/martin/dvorakmortonxd.dev +# A system service does not inherit a login environment, so set HOME/PATH +# explicitly (Next.js wants a writable HOME for its cache). +Environment=HOME=/home/martin +Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +# Bind all interfaces so the Cloudflare tunnel host can reach :3000. The dev +# server reads process.env.HOSTNAME, so pin it rather than inheriting it. +Environment=HOSTNAME=0.0.0.0 +Environment=PORT=3000 +# A reboot can leave a stale dev lock behind; clear it before starting. +ExecStartPre=/bin/rm -f /home/martin/dvorakmortonxd.dev/.next/dev/lock +ExecStart=/bin/node /home/martin/dvorakmortonxd.dev/scripts/dev-server.mjs +Restart=always +RestartSec=3 +# Give Next.js a moment to shut down cleanly. +TimeoutStopSec=15 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/deploy/install-system-service.sh b/deploy/install-system-service.sh new file mode 100755 index 0000000..0c2abc5 --- /dev/null +++ b/deploy/install-system-service.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Install the dvorakmortonxd.dev dev server as a SYSTEM systemd unit so it +# starts at boot for the whole machine (no login / no linger needed). +# +# Run it with root, e.g.: +# doas bash deploy/install-system-service.sh +# +# It also retires the per-user unit, so the two don't fight over :3000 and +# .next/dev/lock. +set -euo pipefail + +if [ "$(id -u)" -ne 0 ]; then + echo "root required — run: doas bash \"$0\"" >&2 + exit 1 +fi + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +unit_src="$here/dvorakmortonxd-dev.system.service" +unit_dst="/etc/systemd/system/dvorakmortonxd-dev.service" +user_unit="/home/martin/.config/systemd/user/dvorakmortonxd-dev.service" + +install -m 0644 "$unit_src" "$unit_dst" +systemctl daemon-reload + +# Retire the per-user copy if it exists (installed earlier as martin). +if [ -e "$user_unit" ] || [ -e "/home/martin/.config/systemd/user/default.target.wants/dvorakmortonxd-dev.service" ]; then + if command -v runuser >/dev/null 2>&1; then + runuser -u martin -- env XDG_RUNTIME_DIR=/run/user/1000 \ + DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \ + systemctl --user disable --now dvorakmortonxd-dev.service || true + else + # runuser is unavailable (minimal container). The caller should have run + # `systemctl --user stop dvorakmortonxd-dev` first; we still remove the files + # so the two units can't fight over :3000 on the next boot. + echo "note: runuser not found — stop the per-user unit manually if it is still running" >&2 + fi + rm -f "$user_unit" \ + /home/martin/.config/systemd/user/default.target.wants/dvorakmortonxd-dev.service +fi + +systemctl enable --now dvorakmortonxd-dev.service +systemctl --no-pager --full status dvorakmortonxd-dev.service || true + +echo +echo "Installed. Manage it with:" +echo " systemctl status|restart|stop dvorakmortonxd-dev" +echo " journalctl -u dvorakmortonxd-dev -f"