Skip to content

Server & systemd

The short way is one command:

Terminal window
curl -fsSL https://get.nouride.com/install.sh | sudo sh

It 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 hostyes — that is the pointonly inside the container
Blast radius of a bad commandthe host, bounded by unit hardening and the exec policythe container
Where exec landsyour actual machinea filesystem that disappears
Restarts on crash / rebootsystemdthe 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

Terminal window
sudo useradd --system --create-home --shell /usr/sbin/nologin nouride
sudo mkdir -p /srv/nouride && sudo chown -R nouride:nouride /srv/nouride

nouride 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

Terminal window
sudo -u nouride tar -xzf /tmp/nouride-linux-x64.tar.gz -C /srv/nouride
sudo ln -sf /srv/nouride/nouride /usr/local/bin/nouride
sudo -u nouride install -m 600 /dev/null /srv/nouride/.env

The 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:

Terminal window
cd /srv/nouride
sudo -u nouride nouride setup

The flags, and why there is no --api-key

4. The unit

Terminal window
cd /srv/nouride
nouride service install --user nouride --dir /srv/nouride

That 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:

Terminal window
sudo cp /srv/nouride/nouride.service /etc/systemd/system/nouride.service
sudo systemctl daemon-reload
sudo systemctl enable --now nouride
journalctl -u nouride -f

The 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=trueThe daemon runs arbitrary commands by design, which is exactly why the blast radius of the daemon itself should be bounded
ReadWritePaths=/srv/nourideThe 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.targetChat gateways and LLM endpoints are all remote; starting earlier just fills the journal with a reconnect storm
TimeoutStopSec=45 with KillSignal=SIGTERMIn-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 machineA 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

Terminal window
nouride service status # which supervisor, and whether exiting gets you respawned
nouride doctor
nouride status
curl -fsS http://127.0.0.1:18254/health

Check /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:

Terminal window
ssh -L 18254:127.0.0.1:18254 you@your-server

To serve it on the network, --host 0.0.0.0 at install time, or:

[dashboard]
enabled = true
host = "0.0.0.0"
port = 18254

Put 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:

Terminal window
nouride service install --user "$USER" --dir ~/nouride

A 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 →