[Unit] Description=left4me server instance %i After=network-online.target Wants=network-online.target # Bound the restart loop. Without these, a persistent ExecStartPre or # ExecStart failure spins indefinitely. Note: these are [Unit]-section # directives (systemd 230+), not [Service]. StartLimitBurst=5 StartLimitIntervalSec=60s [Service] Type=simple User=left4me Group=left4me EnvironmentFile=/etc/left4me/host.env EnvironmentFile=/var/lib/left4me/instances/%i/instance.env # `-` prefix: chdir failure is non-fatal. systemd applies WorkingDirectory # before every Exec line — including ExecStartPre — but the merged dir only # exists once ExecStartPre's overlay mount succeeds. With `-`, ExecStartPre # runs in the unit's home (cwd doesn't matter for the mount helper); the # ExecStart re-applies WorkingDirectory after the mount and finds the dir. WorkingDirectory=-/var/lib/left4me/runtime/%i/merged/left4dead2 # Single source of truth for the kernel-overlayfs mount lifecycle: the web # app's start_instance only stages cfg files and asks systemd to enable+ # start this unit; the actual `mount -t overlay` lives here so reboot # auto-start works the same as a UI-driven start. ExecStopPost mirrors it # so the unmount lives in the same place — no Python-side _mounter needed # in stop/delete/reset paths. Both helper verbs are idempotent. # # `+` prefix runs the helper as PID 1 (root, no sandbox). Required because # the unit has NoNewPrivileges=true, which blocks sudo's setuid escalation # — and the helper itself needs root to nsenter into PID 1's mnt namespace # anyway. ExecStopPost (not ExecStop) so unmount runs after the cgroup is # cleared; ExecStop runs while srcds is still alive and would EBUSY. ExecStartPre=+/usr/local/libexec/left4me/left4me-overlay mount %i ExecStart=/var/lib/left4me/installation/srcds_run -game left4dead2 +hostport ${L4D2_PORT} $L4D2_ARGS ExecStopPost=+/usr/local/libexec/left4me/left4me-overlay umount %i Restart=on-failure RestartSec=5 # Resource control baseline — see docs/superpowers/specs/2026-05-09-l4d2-server-host-perf-baseline-design.md Slice=l4d2-game.slice Nice=-5 IOSchedulingClass=best-effort IOSchedulingPriority=4 OOMScoreAdjust=-200 MemoryHigh=1.5G MemoryMax=2G TasksMax=256 LimitNOFILE=65536 KillSignal=SIGINT TimeoutStopSec=15s LogRateLimitIntervalSec=0 # Hardening (unchanged from previous baseline). NoNewPrivileges=true PrivateTmp=true PrivateDevices=true ProtectHome=true ProtectSystem=strict ReadOnlyPaths=/var/lib/left4me/installation /var/lib/left4me/overlays ReadWritePaths=/var/lib/left4me/runtime/%i RestrictSUIDSGID=true LockPersonality=true [Install] WantedBy=multi-user.target