Files
UmbraTrans/systemd/umbratrans-web.service
umbra de08cfe38e Stop rate-limiting the deployment: any finite budget is exhaustible
The GPU permissions survived a third consecutive boot - all five nodes
660 root:video - so that is now established rather than hopeful.

The deployment was down again for the same reason, and this time the
widened start limit I committed two boots ago turned out not to have
fixed it. It exhausted twenty restarts and returned to the same permanent
failed, with the port free and nobody touching the service.

That fix's verification missed it. The port was freed after three
restarts, inside the window, so the test exercised the easy case and the
commit said "fixed". The real case is a fault that outlasts the budget,
which is the only case that matters.

The rate limit is the wrong mechanism, not the wrong value. Any finite
budget can be exhausted by a fault that lasts longer than it, and the
failure is silent and permanent - `failed` looks like a decision rather
than a give-up. StartLimitIntervalSec=0 disables it: the unit retries for
as long as the fault lasts and binds the moment it clears.

Verified against the case that broke it twice. Held the port for 200
seconds, which drove 31 restarts - past the old limit of 20 - with the
unit never reaching failed. Freeing the port gave 200 from /api/health
within eight seconds, with nothing having started it. A genuine crash
loop now logs every 5s instead of stopping: noisier, and much better than
a deployment that is quietly down.

469 pass with a database, 428 without. Deployment healthy, GPU boundary
intact, database unchanged at users=2 orgs=3 projects=0 audit=43 with no
compute or datastore tables, ollama serving, persistenced up, perms timer
firing, zero stray agents.
2026-10-01 07:11:13 -07:00

104 lines
4.4 KiB
Desktop File

[Unit]
Description=UmbraTrans account and organization service (loopback/LAN, TLS terminated upstream)
Documentation=file:///home/umbra/src/UmbraTrans/docs/umbratrans-backend-design.md
After=network-online.target
Wants=network-online.target
# `failed` must not be a resting state for this service.
#
# Two attempts to get this right, both wrong in the same way.
#
# The original was StartLimitIntervalSec=120 with a burst of 5 and RestartSec=5s,
# which is twenty-five seconds of trying. On three separate boots something else
# held port 8442 - a development shell serving web/, which this backend mounts
# itself - the service exhausted the budget, and systemd then never tried again.
# The deployment stayed down after the port was free, needing a person to notice.
#
# Widening it to 500s with a burst of 20 did not fix that. It only bought more
# time: on the next boot the squatter outlasted twenty restarts and the unit
# returned to the same permanent `failed`. The verification missed it because the
# port was freed after three restarts, inside the window - so the test exercised
# the easy case and the claim said "fixed".
#
# The lesson is that the rate limit is the wrong mechanism entirely, not that it
# was set too low. Any finite budget can be exhausted by a fault that lasts longer
# than the budget, and the failure is silent and permanent.
#
# So: interval 0 disables rate limiting entirely. This service retries for as long
# as the fault lasts and recovers by itself the moment it clears. A genuine crash
# loop logs every RestartSec instead of stopping - noisier, and vastly better than
# a deployment that is quietly down. There is a `systemctl --user stop` for when
# you mean it.
StartLimitIntervalSec=0
StartLimitBurst=0
[Service]
Type=simple
WorkingDirectory=/home/umbra/src/UmbraTrans
# Rootless unit: no User=, because the manager already runs as umbra and setting
# it makes systemd try to resolve a user and fail with status=216/GROUP in this
# environment. This is the copy to install under
# ~/.config/systemd/user/; the system-unit copy in systemd/ keeps User=.
#
# The environment holds the DSN and the SMTP password, so it is read from a
# 0600 file rather than from the unit or the command line, both of which are
# world-readable in /proc.
EnvironmentFile=/home/umbra/.config/umbratrans/umbratrans.env
# The venv interpreter directly, not `uv run`. `uv run` wants to write a lock and
# a build cache under ~/.cache, which ProtectHome=read-only forbids, so it failed
# with "Read-only file system" and the unit restart-looped. A service has no
# business depending on a developer tool's cache anyway: this way the process
# starts from a fixed interpreter with nothing to resolve at boot, and a stale
# cache cannot change what runs.
ExecStart=/home/umbra/src/UmbraTrans/.venv/bin/python -m umbratrans_web serve --host 192.168.123.14 --port 8442 --repository postgres --allow-lan-bind --allow-production
# Kept in step with ExecStart: a dependency change means re-running `uv sync` and
# then `systemctl --user restart`, not editing this file.
Restart=on-failure
RestartSec=5s
UMask=0077
StandardOutput=journal
StandardError=journal
SyslogIdentifier=umbratrans-web
# The first-run setup token is written to this journal and nowhere else. It is
# shown to whoever can read the journal, which is the point: control of the
# service is what authorises creating the first administrator.
# journalctl --user -u umbratrans-web | grep -A2 'FIRST-RUN SETUP'
# --- Sandbox ---------------------------------------------------------------
# Nothing here needs root, a new privilege, or a writable filesystem outside the
# database socket directory.
NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
PrivateDevices=yes
PrivateTmp=yes
ProtectClock=yes
ProtectControlGroups=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectProc=invisible
ProtectSystem=strict
ProtectHome=read-only
# The PostgreSQL socket lives under /run; the connection needs to reach it, and
# an explicit read-write path is narrower than relaxing the whole tree.
ReadWritePaths=/run/postgresql
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
[Install]
WantedBy=default.target