Server & systemd
The short way is one command:
curl -fsSL https://get.nouride.com/install.sh | sudo shIt does everything on this page. What follows is what it does, for when something goes wrong or the machine is unusual enough that you want to drive it yourself.
Native or Docker?
The choice is not about convenience.
| Native (systemd) | Docker | |
|---|---|---|
| Agent can run commands on the host | yes — that is the point | only inside the container |
| Blast radius of a bad command | the host, bounded by unit hardening and the exec policy | the container |
Where exec lands | your actual machine | a filesystem that disappears |
| Restarts on crash / reboot | systemd | the container restart policy |
Pick native if you want an agent that can do things on your machine. Pick Docker if you want one that talks and reads, isolated from the host it happens to run on.
Running the native shape is the reason the exec permission model exists. Running Docker and then bind-mounting your home directory into it gets you the risk of native with the illusion of isolation.
Not on memory grounds — the two measure within ~10 MB of each other.
1. A user that is not you, and not root
sudo useradd --system --create-home --shell /usr/sbin/nologin nouridesudo mkdir -p /srv/nouride && sudo chown -R nouride:nouride /srv/nouridenouride service install refuses to generate a unit that runs as root. This daemon executes
commands chosen by a language model, reachable from a chat app; every other boundary in it — the exec
policy, the workspace sandbox, the 0600 file modes — assumes an unprivileged account. sudo is on
the never-grantable list precisely so escalation is a per-command human decision, and running as root
makes it the default.
A separate user also means ProtectHome=read-only in the unit protects your home directory rather
than the daemon’s own.
2. The binary and its files
sudo -u nouride tar -xzf /tmp/nouride-linux-x64.tar.gz -C /srv/nouridesudo ln -sf /srv/nouride/nouride /usr/local/bin/nouridesudo -u nouride install -m 600 /dev/null /srv/nouride/.envThe symlink is safe: the daemon resolves its real path, so it still finds /srv/nouride/dashboard
and the generated unit still names the real location.
.env holds provider keys and bot tokens. It is 0600, owned by the service account, and referenced
from the unit as EnvironmentFile= — never as Environment=. Unit files are world-readable, get
committed, and get pasted into issue reports; a key in Environment= is a leaked key.
3. Set it up
The daemon boots with no configuration at all, so this step is optional — the browser can do it. Over SSH:
cd /srv/nouridesudo -u nouride nouride setupThe flags, and why there is no --api-key →
4. The unit
cd /srv/nouridenouride service install --user nouride --dir /srv/nourideThat writes /srv/nouride/nouride.service and prints the commands to activate it. It does not
install or enable anything: enabling a system service is a privileged, host-changing action, and a
CLI you cannot safely run to see what it would do is a CLI you end up not running.
Read the file, then:
sudo cp /srv/nouride/nouride.service /etc/systemd/system/nouride.servicesudo systemctl daemon-reloadsudo systemctl enable --now nouridejournalctl -u nouride -fThe generated unit is checked by systemd-analyze verify — every directive in it is one systemd
recognises.
What it does beyond Restart=always
ProtectSystem=strict, ProtectHome=read-only, PrivateTmp=true, NoNewPrivileges=true, RestrictSUIDSGID=true | The daemon runs arbitrary commands by design, which is exactly why the blast radius of the daemon itself should be bounded |
ReadWritePaths=/srv/nouride | The necessary exception. ProtectSystem=strict makes everything read-only, so without this the daemon cannot write its own database and dies at boot |
After=network-online.target | Chat gateways and LLM endpoints are all remote; starting earlier just fills the journal with a reconnect storm |
TimeoutStopSec=45 with KillSignal=SIGTERM | In-flight turns are persisted on SIGTERM, and a stop timeout shorter than the daemon’s shutdown budget loses conversation context on every restart |
MemoryHigh at 80% of the machine | A soft ceiling, so a heavy command is throttled rather than something being killed |
MemoryHigh is written as an absolute number computed at install time, not 80%: inside an LXC,
systemd resolves a percentage against the host’s memory, and on a 2 GB container of a 62 GB node
that came out as 49.8 GB.
Commands the agent spawns also run at nice 10, so a build yields the CPU to a turn rather than
making every chat go silent until it finishes.
5. Check it
nouride service status # which supervisor, and whether exiting gets you respawnednouride doctornouride statuscurl -fsS http://127.0.0.1:18254/healthCheck /health, not just the service state. systemctl start returning 0 means the process was
spawned, not that it is serving — a daemon that refuses its config exits after systemd reports
success.
If nouride service status says none, nouride restart will refuse to run. A “restart” that
means “stop until you notice” is the worst kind of wrong, so it is a 409 with an explanation rather
than a silent shutdown.
Exposing the dashboard
It binds to loopback by default, and the right answer for most people is to leave it there:
ssh -L 18254:127.0.0.1:18254 you@your-serverTo serve it on the network, --host 0.0.0.0 at install time, or:
[dashboard]enabled = truehost = "0.0.0.0"port = 18254Put a reverse proxy with TLS in front of it if it is reachable from anywhere you do not control. The dashboard is a login, and a login over plain HTTP is a password on the wire.
macOS
Same flow, launchd instead:
nouride service install --user "$USER" --dir ~/nourideA LaunchAgent runs as you and only while you are logged in — fine for a laptop, not for a server.
Detection matches launchd’s job label rather than the mere presence of XPC_SERVICE_NAME: macOS
exports that variable into every process from a GUI session, and presence-based detection once made a
hand-started daemon believe launchd would revive it. It did not.
Running the test suite on the box
Safe now, and it was not. bun run verify on a 2-core / 2 GB LXC used to take the container down —
not the tests, which peak at 556 MB and finish in 28 seconds, but the typecheck, which spawned all
twelve tsc processes at once. One of them peaks at 694 MB on its own; twelve is three to four
gigabytes against two, with the daemon and its gateways living in the same container.
The typecheck now sizes its pool from the machine — CPU count, and available memory against a
measured 700 MB per job — and prints the width it chose. On that box it picks two, and the whole of
verify runs in 49 seconds at a 1242 MB peak with the daemon answering throughout.
NOURIDE_TYPECHECK_JOBS=1 forces it lower.
What to back up
.nouride/ and .env, with the daemon stopped.
Backup, upgrade, uninstall →