left4me/docs/superpowers/specs/2026-05-06-left4me-deployment-design.md

244 lines
7.8 KiB
Markdown

# Left4me Deployment Design
## Goal
Provide a production-like test deployment for `left4me` on a real Linux server. The deployment should be quick to run from the local working tree, while keeping the runtime layout, service ownership, and hardening model close enough to the intended production setup that problems appear early.
## Context
`left4me` has two components:
- `l4d2host`, exposed through `l4d2ctl`, manages L4D2 installation, per-server initialization, lifecycle, status, and logs.
- `l4d2web` is the Flask web UI and worker process. It calls host operations through `l4d2ctl` rather than importing host internals.
The existing code uses hard-coded `/opt/l4d2` paths and user systemd units. The deployment design replaces that with a product-level root, root-owned global units, and constrained helper commands so per-server hardening is owned by the host setup rather than by the runtime user.
## Runtime User
The deployment uses one runtime user:
```text
user: left4me
home: /var/lib/left4me
shell: /usr/sbin/nologin
```
The SSH deployment user is separate and must have sudo privileges. The `left4me` user owns runtime state and runs the web app and game servers, but does not own systemd unit definitions, sudoers rules, or helper installation paths.
## Filesystem Layout
The deployed server uses these paths:
```text
/etc/left4me/
host.env
web.env
/opt/left4me/
.venv/
<repository contents>
/var/lib/left4me/
left4me.db
installation/
overlays/
instances/
runtime/
tmp/
/usr/local/lib/systemd/system/
left4me-web.service
left4me-server@.service
/usr/local/libexec/left4me/
left4me-systemctl
left4me-journalctl
/etc/sudoers.d/
left4me
```
`/var/lib/left4me` is both the `left4me` home directory and the product state root. This keeps mutable state in one place and avoids mixing runtime data with deployed code.
## Configuration
Configuration is split by component boundary:
`/etc/left4me/host.env`:
```text
LEFT4ME_ROOT=/var/lib/left4me
```
`/etc/left4me/web.env`:
```text
DATABASE_URL=sqlite:////var/lib/left4me/left4me.db
SECRET_KEY=<generated-or-managed-secret>
JOB_WORKER_THREADS=4
```
`LEFT4ME_ROOT` replaces the previous host-library hard-coded `/opt/l4d2` root. Host paths derive from it:
```text
${LEFT4ME_ROOT}/installation
${LEFT4ME_ROOT}/overlays
${LEFT4ME_ROOT}/instances
${LEFT4ME_ROOT}/runtime
${LEFT4ME_ROOT}/tmp
```
The test deployment script can generate `web.env` when missing. Production config management can own both env files directly.
## Systemd Model
Both web and game servers use global root-owned systemd units under `/usr/local/lib/systemd/system`.
`left4me-web.service`:
- Runs as `User=left4me`.
- Loads `/etc/left4me/host.env` and `/etc/left4me/web.env`.
- Uses `/opt/left4me` as its working directory.
- Starts the web app from `/opt/left4me/.venv`.
- Uses one process worker because lifecycle jobs run in-process.
- Uses moderate systemd hardening, but must still allow the web worker to call `l4d2ctl`, write SQLite state, and run host lifecycle commands.
`left4me-server@.service`:
- Runs each game server as `User=left4me`.
- Reads generated per-server environment from `${LEFT4ME_ROOT}/instances/%i/instance.env`.
- Uses `${LEFT4ME_ROOT}/runtime/%i/merged/left4dead2` as `WorkingDirectory`.
- Starts `${LEFT4ME_ROOT}/installation/srcds_run`.
- Applies stronger hardening than the web unit because each game server is the more important sandbox boundary.
The server unit template is root-owned. A compromised `left4me` process should not be able to edit the template to weaken future server sandboxing.
## Privileged Helper Model
`l4d2ctl` should not call arbitrary `sudo systemctl` or `sudo journalctl` commands. Instead it calls constrained helpers:
```text
sudo -n /usr/local/libexec/left4me/left4me-systemctl start <server-name>
sudo -n /usr/local/libexec/left4me/left4me-systemctl stop <server-name>
sudo -n /usr/local/libexec/left4me/left4me-systemctl show <server-name>
sudo -n /usr/local/libexec/left4me/left4me-journalctl <server-name> --lines <n> --follow|--no-follow
```
The helpers validate the action and server name, then map the server name to `left4me-server@<server-name>.service`. The sudoers rule only allows the `left4me` user to run these helpers as root.
## Host Library Contract Changes
The host library changes from:
```text
/opt/l4d2/installation
/opt/l4d2/overlays
/opt/l4d2/instances
/opt/l4d2/runtime
systemd --user units
```
to:
```text
${LEFT4ME_ROOT}/installation
${LEFT4ME_ROOT}/overlays
${LEFT4ME_ROOT}/instances
${LEFT4ME_ROOT}/runtime
global left4me-server@.service managed through sudo helpers
```
The host library remains fail-fast and prerequisite-light. It assumes deployment or config management installed OS packages, created directories, installed units, and configured sudoers.
## Overlay References
Overlay configuration should store safe relative refs under `${LEFT4ME_ROOT}/overlays`, not absolute paths.
Valid examples:
```text
standard
competitive/base
users/42/custom
```
Rejected examples:
```text
/tmp/bad
../bad
bad/../evil
bad//evil
```
This supports a flat overlay catalog now and leaves room for user-managed overlays later through namespaced refs such as `users/<user-id>/<overlay-name>`.
## Deployment Script
The test deployment script lives in `deploy/deploy-test-server.sh`. It runs locally and takes an SSH target for a sudo-capable deployment user.
The script should:
- Archive the current local working tree.
- Copy it to the remote host.
- Create the `left4me` system user if missing.
- Install OS prerequisites when a supported package manager is detected.
- Create `/etc/left4me`, `/opt/left4me`, `/var/lib/left4me`, `/usr/local/libexec/left4me`, and `/usr/local/lib/systemd/system` as needed.
- Preserve `/var/lib/left4me` across redeployments.
- Replace repository contents under `/opt/left4me` while preserving `/opt/left4me/.venv`.
- Install deployment units, helper scripts, sudoers rules, and env templates.
- Create or update `/opt/left4me/.venv`.
- Install `l4d2host` and `l4d2web` from the deployed local source.
- Initialize the SQLite schema by importing/creating the Flask app.
- Optionally bootstrap an admin user from environment variables.
- Reload systemd and restart `left4me-web.service`.
- Verify `/health` locally on the server.
The script is a test-server convenience, not a replacement for production config management. Production can implement the same layout with stronger package/version control.
## Documentation Structure
Local deployment assets live under:
```text
deploy/
README.md
deploy-test-server.sh
files/
templates/
tests/
```
`deploy/files` mirrors target filesystem paths for root-owned deployment artifacts. `deploy/templates` contains env templates that may need generated secrets. `deploy/README.md` is the operator-facing guide.
Component documentation should explain only component-specific deployment contracts:
- `l4d2host/README.md`: `LEFT4ME_ROOT`, global service helpers, and overlay refs.
- `l4d2web/README.md`: env vars, admin bootstrap, and systemd service expectations.
- Root `README.md`: link to `deploy/README.md`.
## Verification
Local verification before deploying:
```bash
pytest l4d2host/tests -q
pytest l4d2web/tests -q
pytest deploy/tests -q
```
Deployment verification on the target server should include:
- `systemctl status left4me-web.service --no-pager`
- `curl http://127.0.0.1:8000/health`
- `sudo -u left4me /opt/left4me/.venv/bin/l4d2ctl --help`
- helper validation for accepted and rejected server names
- a later gated smoke test for `install`, `initialize`, `start`, `status`, `logs`, `stop`, and `delete`
## Out Of Scope
- Running deployment against a real server as part of this design.
- Replacing production config management.
- Managing overlay file contents through the web UI.
- Adding multi-host SSH transport to the web app.
- Supporting multiple game types beyond the existing L4D2 host workflow.