update.sh pulls the code and restarts, and says nothing about the unit -- so a host can run a new release under the old confinement and fail in a way that points nowhere. Dropping ProtectKernelTunables is exactly such a change: without it applied, an agent chat cannot start a sandbox at all. It compares the *template* against the one last applied here rather than against the installed file. An installed unit grows host-specific lines -- an ordering dependency on whatever serves the models, a note about how the prefix is mounted -- and diffing the files would warn about those forever. A warning that always fires is one nobody reads. Reinstalling automatically would clobber those same lines, so it only says so and leaves the merge to a person. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deployment
Installs LLeMbas as a system service behind nginx with a self-signed certificate. Written for a systemd + nginx host; tested on Arch.
| Default | |
|---|---|
| Service user | lembas (system account, nologin) |
| Home | /home/lembas |
| Install prefix | /srv/lembas (bind mount of the home) |
| Checkout | $PREFIX/app |
| Virtualenv | $PREFIX/venv |
| Database | $PREFIX/data/lembas.db |
| Environment | $PREFIX/lembas.env (mode 600) |
| Unit | /etc/systemd/system/lembas.service |
| Vhost | /etc/nginx/conf.d/<host>.conf |
| Listens on | 127.0.0.1:8080 — reachable only through nginx |
The prefix defaults to a bind mount of the service user's home because on many
machines the root filesystem is small while /home is not, and the virtualenv
plus database belong on the larger volume. Set PREFIX=$HOME_DIR to skip it.
First install
SITE_HOST=chat.example ./deploy/install.sh
Idempotent — safe to re-run. It creates the user and bind mount, clones the
repo, builds the venv, generates lembas.env with a fresh LEMBAS_SECRET_KEY,
installs the unit and vhost, issues a self-signed certificate, adds a
/etc/hosts entry if the name does not already resolve, and enables the
service.
Then open https://<SITE_HOST>, accept the certificate warning, and create the
first account — it becomes the administrator.
Everything is overridable from the environment:
| Variable | Default | |
|---|---|---|
SITE_HOST |
lembas.local |
nginx server_name and certificate CN |
APP_PORT |
8080 |
loopback port the service binds |
SERVICE_USER |
lembas |
system account to run as |
HOME_DIR |
/home/lembas |
that account's home |
PREFIX |
/srv/lembas |
install root (bind mount of HOME_DIR) |
REPO_URL |
this checkout's origin |
so a fork deploys itself |
LEMBAS_BRANCH |
main |
branch to deploy |
Deploying a change
git push
./deploy/update.sh
update.sh fetches, hard-resets the deployment checkout to origin/main,
reinstalls dependencies and restarts, printing the commits it pulled. The hard
reset is deliberate: nothing is ever edited in place there, so there is no local
work to preserve and no conflicts to resolve.
Operating it
systemctl status lembas
journalctl -u lembas -f
sudo -u lembas /srv/lembas/venv/bin/lembas info # paths and counts
Configuration lives in $PREFIX/lembas.env. Edit it and restart.
Notes
The secret key is generated once. install.sh will not overwrite an
existing lembas.env. Rotating LEMBAS_SECRET_KEY signs every user out and
makes stored upstream API keys unreadable — they would have to be re-entered.
nginx buffering is off for a reason. Replies stream as server-sent events.
With proxy_buffering on (the default) nginx holds the entire reply and
delivers it in one lump at the end, which is indistinguishable from streaming
being broken. proxy_read_timeout is raised to an hour because a model can
think for minutes before the first token.
Hardening is deliberately moderate. ProtectSystem=full, not strict,
because agent chats run commands. Two lines in the unit are load-bearing and
worth knowing before anyone tidies them:
ProtectKernelTunablesis absent on purpose. With it, bubblewrap cannot start at all — it bind-mounts/proc/sysread-only, and the kernel then refusesmount -t procinside a user namespace. The unit says why, and why the obvious workaround is worse.TasksMaxandMemoryMaxbound the whole service, because a sandbox has no cgroup of its own andRLIMIT_NPROCis counted per uid — the same uid the server runs as.
Local agent execution is off until an administrator turns it on, and the sandbox never binds the deployment prefix, so a command cannot read the database or the encryption key.
Use a real certificate if this is exposed beyond a trusted LAN. The
self-signed cert exists so the install works with no external dependencies;
point ssl_certificate at a real one and nothing else needs to change.