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.
104 lines
4.4 KiB
Desktop File
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
|