added install script for agent

changed backend to agent
This commit is contained in:
Philipp
2026-06-04 14:05:50 +02:00
parent 7f2785fb05
commit 0b059aec1d
669 changed files with 767 additions and 70582 deletions
+25 -6
View File
@@ -2,21 +2,38 @@
## Node Agent
Run this on every Incus host:
Run this on every Incus host. The installer clones or updates the repository, creates the agent state/config directories, prepares `agent/.env`, installs npm dependencies, installs the systemd unit, and starts the service:
```bash
cd /opt/incus-backup-ui/backend
cp .env.example .env
npm install
sudo npm start
curl -fsSL https://forgejo.digital-droplets.de/philschlo/incus-backup-ui/raw/branch/main/deploy/install-agent.sh | sudo sh
```
To pin the source repository or branch:
```bash
curl -fsSL https://forgejo.digital-droplets.de/philschlo/incus-backup-ui/raw/branch/main/deploy/install-agent.sh \
| sudo INCUS_BACKUP_REPO_URL="https://forgejo.digital-droplets.de/philschlo/incus-backup-ui.git" INCUS_BACKUP_BRANCH="main" sh
```
Useful installer environment variables:
- `INSTALL_DIR`: install location, default `/opt/incus-backup-ui`.
- `INCUS_BACKUP_REPO_URL`: Git repository URL.
- `INCUS_BACKUP_BRANCH`: Git branch, default `main`.
- `API_TOKEN`: explicit agent token. If omitted, the installer generates one and prints it once.
- `ALLOWED_MANAGEMENT_IPS`: optional comma-separated management server IP allowlist.
- `HTTPS_ENABLED`, `TLS_CERT_FILE`, `TLS_KEY_FILE`: direct HTTPS settings for the agent.
- `SKIP_START=true`: install/update but do not start the service.
If you are upgrading an older installation, the node-agent directory was previously named `backend`. The installer handles the code directory rename and moves an existing ignored `backend/.env` to `agent/.env` when needed.
Important `.env` values:
```env
HOST="0.0.0.0"
PORT=3000
API_TOKEN="long-random-token-at-least-32-characters"
AGENT_DATABASE_PATH="/var/lib/incus-backup-agent/agent.sqlite"
HTTPS_ENABLED=true
TLS_CERT_FILE="/etc/incus-backup-agent/tls.crt"
TLS_KEY_FILE="/etc/incus-backup-agent/tls.key"
@@ -25,6 +42,8 @@ ALLOWED_MANAGEMENT_IPS="management-server-ip"
`API_TOKEN` is required and must be at least 32 characters long. If `ALLOWED_MANAGEMENT_IPS` is set, the agent only accepts requests from those comma-separated IP addresses.
`AGENT_DATABASE_PATH` stores recent agent jobs. Put it on persistent local storage, for example under `/var/lib/incus-backup-agent/`. If the agent restarts while a backup or restore is active, the persisted job is marked `failed` on startup and stale per-VM locks are cleared.
For private networks such as NetBird, the agent can serve HTTPS directly with an internal CA. Create one CA and sign one certificate per agent. The certificate must contain the NetBird IP or internal DNS name as a SAN:
```bash
@@ -41,7 +60,7 @@ sudo install -m 644 tls.crt /etc/incus-backup-agent/tls.crt
Copy `agent-ca.crt` to the management server and set `AGENT_CA_FILE` there.
Install systemd service:
Manual systemd service installation, if not using the installer:
```bash
sudo cp deploy/systemd/incus-backup-agent.service /etc/systemd/system/
+29 -30
View File
@@ -12,8 +12,8 @@ Sortiert nach Risikoklasse. Datei-/Zeilenreferenzen beziehen sich auf den Stand
- README/`docs/deployment.md` entsprechend anpassen (noch offen).
- [x] **2. `API_TOKEN` im Node-Agent zur Pflicht machen** ✅ erledigt
- `backend/src/config.js:44` — Start schlägt fehl wenn Token fehlt oder kürzer als 32 Zeichen.
- `backend/src/index.js``if (!config.apiToken) next()` entfernt; Token-Prüfung immer aktiv.
- `agent/src/config.js:44` — Start schlägt fehl wenn Token fehlt oder kürzer als 32 Zeichen.
- `agent/src/index.js``if (!config.apiToken) next()` entfernt; Token-Prüfung immer aktiv.
- [x] **3. HTTPS zwischen Management und Agent erzwingen** ✅ erledigt
- `management/src/routes/nodes.js:102-112``validateBaseUrl` erzwingt `https://`. Escape-Hatch `ALLOW_INSECURE_AGENT_HTTP=true` nur für Dev.
@@ -29,16 +29,14 @@ Sortiert nach Risikoklasse. Datei-/Zeilenreferenzen beziehen sich auf den Stand
- Audit-Event `login_blocked` und `login_failed` implementiert.
- Hinweis: Kein externer `express-rate-limit` nötig, eigenständige Implementierung ausreichend.
- [ ] **6. Restore atomarisieren — kein direktes `dd` auf Produktiv-Volume**
- Datei: `backend/src/executor.js:92` (`streamResticDumpToDd`), `backend/src/routes/restore.js`
- Aktuell: Wenn `restic dump` abbricht, sind GB bereits auf der VM-Disk → Disk irreversibel kaputt.
- Fix-Optionen:
- In temporäres ZFS-Volume schreiben, danach Größen-/Hash-Verifikation, dann atomarer `zfs rename`/clone-Swap.
- Oder: vor Restore automatisch Disk-Snapshot anlegen; bei Fehler rollback.
- Größe vorab prüfen (`restic stats` vs. `zfs get volsize`).
- [x] **6. Restore atomarisieren — kein direktes `dd` auf Produktiv-Volume** ✅ staged Restore umgesetzt
- Datei: `agent/src/executor.js:92` (`streamResticDumpToDd`), `agent/src/routes/restore.js`
- Umsetzung: Restore schreibt zuerst in ein temporäres ZFS-Volume, prüft die Größe vorab und tauscht danach per `zfs rename` gegen das Produktiv-Volume.
- Das alte Volume bleibt als `*.pre-restore-*` Rollback-Kopie erhalten.
- Bei Fehlern vor dem Swap wird nur das temporäre Volume entfernt; bei Fehlern nach dem Swap versucht der Agent den alten Volume-Namen wiederherzustellen.
- [ ] **7. Backup-Verifikation einbauen**
- Datei: `backend/src/routes/backup.js` (VM-Pfad ~Z.71, Container-Pfad ~Z.137)
- Datei: `agent/src/routes/backup.js` (VM-Pfad ~Z.71, Container-Pfad ~Z.137)
- Aktuell: Bei Pipe-Fehler (`zfs send` !=0, `createReadStream`-Abbruch) committed restic ggf. einen unvollständigen Snapshot mit Status „success".
- Fix:
- Vor Start erwartete Größe ermitteln (`zfs get volsize` / `used`).
@@ -57,14 +55,15 @@ Sortiert nach Risikoklasse. Datei-/Zeilenreferenzen beziehen sich auf den Stand
- Offen: `management/.env.example` hat `SESSION_COOKIE_SECURE=false` — in Deployment-Doku explizit als "in Produktion auf `true` setzen" dokumentieren.
- [ ] **10. Race beim Sichtbarmachen des Snapshot-Devices**
- Datei: `backend/src/routes/backup.js:57`
- Datei: `agent/src/routes/backup.js:57`
- Aktuell: 2 s `sleep` reicht nicht garantiert.
- Fix: Polling-Schleife auf `fs.access(snapshotDevice)` mit Timeout; zusätzlich `udevadm trigger && udevadm settle`.
- [ ] **11. Persistenter Job- und Lock-Store im Agent**
- Datei: `backend/src/jobs.js`
- Aktuell: in-memory; bei Crash gehen laufende Jobs verloren, Locks bleiben hängen oder verschwinden inkonsistent (z.B. ZFS `volmode=dev`).
- Fix: SQLite-Tabelle für Jobs/Locks. Beim Start: alle `running`/`queued` Jobs zu `failed` markieren, Cleanup-Pfad ausführen.
- [x] **11. Persistenter Job- und Lock-Store im Agent** ✅ erledigt
- Datei: `agent/src/jobs.js`
- Umsetzung: Agent speichert Jobs in SQLite (`AGENT_DATABASE_PATH`).
- Beim Start werden `running`/`queued` Jobs als `failed` markiert, damit Management einen finalen Zustand pollen kann und VM-Locks nicht hängen bleiben.
- Offen für später: ressourcenspezifische Cleanup-Recovery für abgebrochene Host-Operationen.
- [ ] **12. Toten `SESSION_SECRET` aufräumen**
- Datei: `management/src/config.js:8`
@@ -72,7 +71,7 @@ Sortiert nach Risikoklasse. Datei-/Zeilenreferenzen beziehen sich auf den Stand
- Fix: Variable und zugehörigen `.env.example`-Eintrag entfernen, oder Session-IDs HMAC-signieren und Variable dann sinnvoll nutzen.
- [ ] **13. Snapshot-ID-Prefix-Matching eindeutig machen**
- Datei: `backend/src/validators.js:66`
- Datei: `agent/src/validators.js:66`
- Aktuell: `startsWith` — bei Prefix-Kollision wird stillschweigend der erste Treffer genommen.
- Fix: Bei >1 Treffer 409 zurückgeben; UI auf 12-Hex-Prefix umstellen.
@@ -81,12 +80,12 @@ Sortiert nach Risikoklasse. Datei-/Zeilenreferenzen beziehen sich auf den Stand
- Fix: Mindestens Rollen `admin` / `operator` / `viewer`. Restore nur für `admin`.
- [ ] **15. ENV-Escaping in `writeEnvSettings` verbessern**
- Datei: `backend/src/config.js:105`
- Datei: `agent/src/config.js:105`
- Aktuell: Nur `\` und `"` escaped; `$`, Backticks, Newlines nicht.
- Fix: Eigener Serializer mit korrektem Escaping aller Sonderzeichen.
- [ ] **16. Settings-Endpunkt sperren bis `API_TOKEN` initial gesetzt ist**
- Datei: `backend/src/routes/settings.js`
- Datei: `agent/src/routes/settings.js`
- Hängt mit Fix #2 zusammen — nach #2 automatisch erfüllt, hier zur Sicherheit dokumentieren/testen.
---
@@ -99,18 +98,18 @@ Sortiert nach Risikoklasse. Datei-/Zeilenreferenzen beziehen sich auf den Stand
- [ ] **18. Systemd-Hardening für den Agent**
- Datei: `deploy/systemd/incus-backup-agent.service`
- Hinzufügen: `NoNewPrivileges=true`, `ProtectSystem=strict`, `ProtectHome=true`, `PrivateTmp=true`, `ReadWritePaths=/dev/zvol /var/lib/incus /opt/incus-backup-ui/backend`, `CapabilityBoundingSet=...`, eingeschränkte `AmbientCapabilities`.
- Hinzufügen: `NoNewPrivileges=true`, `ProtectSystem=strict`, `ProtectHome=true`, `PrivateTmp=true`, `ReadWritePaths=/dev/zvol /var/lib/incus /opt/incus-backup-ui/agent`, `CapabilityBoundingSet=...`, eingeschränkte `AmbientCapabilities`.
- [ ] **19. `npm start` durch direkten `node`-Aufruf ersetzen**
- Datei: beide `deploy/systemd/*.service`
- `ExecStart=/usr/bin/node src/index.js` — kein npm-Wrapper-Prozess, kein PATH-Risiko.
- [ ] **20. CORS am Agent entfernen**
- Datei: `backend/src/index.js:16`
- Datei: `agent/src/index.js:16`
- Agent wird nie aus dem Browser angesprochen → Angriffsfläche raus.
- [ ] **21. `schedules.json`-Pfad explizit konfigurierbar**
- Datei: `backend/src/scheduler.js:6`
- Datei: `agent/src/scheduler.js:6`
- Aktuell: `process.cwd()`-abhängig.
- Fix: über Env (`SCHEDULES_PATH`) absolut konfigurieren, Default unter `/var/lib/incus-backup-agent/`.
@@ -127,16 +126,16 @@ Sortiert nach Risikoklasse. Datei-/Zeilenreferenzen beziehen sich auf den Stand
- Pro VM und global (z.B. max N parallele Streams nach S3). Disk-Space-/Quota-Check vor Start.
- [ ] **25. `incus snapshot delete` Retry verallgemeinern**
- Datei: `backend/src/routes/backup.js:303`
- Datei: `agent/src/routes/backup.js:303`
- Aktuell: Substring-Match auf englische Stderr — bricht bei lokalisierten Builds.
- Fix: Generischer Retry (n Versuche, Backoff) bei nicht-0 Exit-Code.
- [ ] **26. Restic-`ls` streamen statt vollständig in RAM laden**
- Datei: `backend/src/routes/snapshots.js:19`
- Datei: `agent/src/routes/snapshots.js:19`
- Für Container-Backups mit vielen Files relevant.
- [ ] **27. Container-Restore implementieren oder Container-Backup deaktivieren**
- Datei: `backend/src/routes/restore.js:21`
- Datei: `agent/src/routes/restore.js:21`
- Aktuell: 501. Backups laufen, aber nicht wiederherstellbar = Backup-Theater.
- Fix: Sicheren `zfs receive`-Workflow umsetzen oder Container-Backup im UI/API ausschalten bis fertig.
@@ -151,8 +150,8 @@ Sortiert nach Risikoklasse. Datei-/Zeilenreferenzen beziehen sich auf den Stand
- [ ] **30. `frontend/dist/` ist eingecheckt** — ignorieren und löschen.
- [ ] **31. `incus-backup-ui-plan.md` auf Secrets/Bucket-Namen prüfen** bevor OSS.
- [ ] **32. Container-Limit (Restore fehlt) prominent in README dokumentieren.**
- [ ] **33. Scheduler-Jitter einbauen** (`management/src/scheduler.js`, `backend/src/scheduler.js`) — sonst belasten viele Nodes synchron S3.
- [ ] **34. `formatBytes` deduplizieren** (`backend/src/routes/backup.js`, `restore.js`) → `executor.js` oder `utils.js`.
- [ ] **33. Scheduler-Jitter einbauen** (`management/src/scheduler.js`, `agent/src/scheduler.js`) — sonst belasten viele Nodes synchron S3.
- [ ] **34. `formatBytes` deduplizieren** (`agent/src/routes/backup.js`, `restore.js`) → `executor.js` oder `utils.js`.
- [ ] **35. `.env`-Dateipermissions dokumentieren**`chmod 600` im Deployment-Doc verlangen.
- [ ] **36. Schema-Versionierung statt `addColumnIfMissing`** — z.B. `schema_version`-Tabelle + nummerierte Migrationen.
- [ ] **37. `audit_events.details` Größenlimit** oder JSON-Spalte (SQLite hat JSON1).
@@ -175,7 +174,7 @@ Sortiert nach Risikoklasse. Datei-/Zeilenreferenzen beziehen sich auf den Stand
---
## Status-Übersicht kritische Punkte (Stand 2026-05-21)
## Status-Übersicht kritische Punkte (Stand 2026-06-04)
| # | Titel | Status |
|---|-------|--------|
@@ -184,15 +183,15 @@ Sortiert nach Risikoklasse. Datei-/Zeilenreferenzen beziehen sich auf den Stand
| 3 | HTTPS Management↔Agent | ✅ erledigt |
| 4 | CORS-Whitelist + SameSite=Strict | ✅ erledigt |
| 5 | Brute-Force-Schutz Login | ✅ erledigt |
| 6 | Restore atomarisieren | ⬜ offen |
| 6 | Restore atomarisieren | ✅ staged Restore umgesetzt |
| 7 | Backup-Verifikation | ⬜ offen |
| 8 | Bearer-Token aus localStorage | ✅ erledigt |
| 9 | Session-Cookie `Secure`-Flag | ⚠️ teilweise |
## Nächste Prioritäten
1. **#6/#7** Restore- und Backup-Verifikation — größter Aufwand, größter Impact auf Datenintegrität.
1. **#7** Backup-Verifikation weiter härten — Pipeline-/Stream-Fehler testen und vollständiger absichern.
2. **#9** `SESSION_COOKIE_SECURE=true` in Deployment-Doku festschreiben.
3. **#12** Toten `SESSION_SECRET` entfernen.
4. **#15** ENV-Escaping vervollständigen (`$`, Backticks, Newlines).
5. **#11** Job- und Lock-Store auf SQLite persistieren (Crash-Sicherheit).
5. **#11 Folgearbeit** ressourcenspezifische Cleanup-Recovery nach Agent-Crash definieren.
+108 -27
View File
@@ -1,8 +1,24 @@
# Issue Backlog
## Status snapshot
Last reviewed against code: 2026-06-04.
Several earlier backlog items have already landed in the codebase:
- Management persists accepted agent jobs in `job_history`.
- Management polls agent jobs and updates final status, error, finish time, duration, current step, and parsed backup snapshot ID.
- Operations page shows job history and audit log.
- Node-agent health checks cover required commands, ZFS pool state, `/dev/zvol`, and Restic repository access.
- Backup jobs verify the stored Restic file size and remove failed snapshots on verification errors.
Known caveat: agent-side jobs are persisted, but subprocesses cannot survive an agent restart. Active jobs are marked `failed` on startup; deeper cleanup recovery for partially changed host resources is still future work.
## 1. Persist final agent job status in management
Management currently records when a backup or restore was accepted by an agent, but it does not persist the final agent job result.
Status: mostly done.
Management records when a backup or restore was accepted by an agent and now persists the final agent job result when the node-agent remains reachable long enough to be polled.
### Goal
@@ -10,21 +26,27 @@ Persist reliable end-to-end job status in the management database.
### Tasks
- Add polling for accepted agent jobs from management.
- Store final `success` or `failed` status in `job_history`.
- Store duration, finished timestamp, error message, and agent job logs summary.
- Store created Restic snapshot ID for successful backup jobs when available.
- Surface final status in the Operations page.
- [x] Add polling for accepted agent jobs from management.
- [x] Store final `success` or `failed` status in `job_history`.
- [x] Store duration, finished timestamp, error message, and current step.
- [ ] Store agent job logs summary.
- [x] Store created Restic snapshot ID for successful backup jobs when available.
- [x] Surface final status in the Operations page.
- [x] Persist recent agent-side jobs.
- [x] Mark active agent jobs as `failed` after an agent restart so management can poll a final state.
### Acceptance Criteria
- A backup started through management eventually shows `success` or `failed`.
- A restore started through management eventually shows `success` or `failed`.
- Agent restart or management restart does not lose already persisted history.
- [x] A backup started through management eventually shows `success` or `failed` while the agent remains reachable.
- [x] A restore started through management eventually shows `success` or `failed` while the agent remains reachable.
- [x] Management restart does not lose already persisted history.
- [x] Agent restart does not lose the active job record; active work is marked `failed` because the subprocess cannot survive restart.
## 2. Expand node-agent health checks
The current health endpoint should provide deeper operational checks for backup readiness.
Status: mostly done.
The health endpoint now provides deeper operational checks for backup readiness.
### Goal
@@ -32,23 +54,52 @@ Make `/api/health` useful for diagnosing whether a node can actually run backup
### Tasks
- Check that required commands exist: `incus`, `zfs`, `zpool`, `restic`, `udevadm`, `dd`.
- Check that configured ZFS pool exists.
- Check that `/dev/zvol` is accessible.
- Check Restic repository access.
- Check S3/Restic credentials by running a safe Restic command.
- Include ZFS pool capacity and free space.
- Return structured check names and messages.
- [x] Check that required commands exist: `incus`, `zfs`, `zpool`, `restic`, `udevadm`, `dd`.
- [x] Check that configured ZFS pool exists.
- [x] Check that `/dev/zvol` is accessible.
- [x] Check Restic repository access.
- [x] Check S3/Restic credentials by running a safe Restic command.
- [x] Include ZFS pool capacity and free space.
- [x] Return structured check names and messages.
- [ ] Surface detailed per-node health output in the management UI, not just a compact aggregate.
### Acceptance Criteria
- Management UI shows degraded node health with actionable check names.
- A missing command, wrong pool, or wrong Restic credentials is visible in health output.
- [~] Management UI shows degraded node health with actionable check names. Detailed output exists through node health endpoints; UI can still be improved.
- [x] A missing command, wrong pool, or wrong Restic credentials is visible in health output.
## 3. Add per-VM backup policy
## 3. Harden backup verification
Status: partially done.
Backup jobs now verify the backed-up Restic file size after `restic backup` and attempt to remove failed snapshots. This reduces the risk of accepting a truncated backup, but the implementation still needs stronger stream failure handling and broader verification semantics.
### Goal
Ensure a backup is marked `success` only when the expected source data was fully stored and verified.
### Tasks
- [x] Parse the created Restic snapshot ID after backup.
- [x] Verify stored Restic file size against streamed source bytes or expected ZFS size.
- [x] Remove failed Restic snapshots with `forget` and `prune` on verification errors.
- [ ] Make stream/pipeline failure handling explicit for all sources.
- [ ] Avoid marking success if source stream closes early but Restic exits successfully.
- [ ] Add tests with mocked command failures and short reads.
### Acceptance Criteria
- [x] Successful backup jobs include a verifiable Restic snapshot ID in management history.
- [x] Size mismatches fail the job.
- [ ] Simulated source stream errors cannot produce a successful job.
- [ ] Verification behavior is covered by automated tests.
## 4. Add per-VM backup policy
Retention and schedule behavior is currently broad. Per-VM policies would make production usage more flexible.
Current state: schedules can be enabled per VM with interval and time of day. Retention is still global per agent.
### Goal
Allow each VM to define its own backup policy.
@@ -57,7 +108,7 @@ Allow each VM to define its own backup policy.
- Add management database table for VM backup policies.
- Support per-VM retention values: hourly, daily, weekly, monthly.
- Support per-VM schedule enablement, interval, and time.
- [x] Support per-VM schedule enablement, interval, and time.
- Add optional policy flag: backup only when VM is running.
- Update Scheduler UI to edit policies per node and VM.
- Send policy retention to the agent backup request or apply it in management scheduling.
@@ -68,10 +119,12 @@ Allow each VM to define its own backup policy.
- Two VMs on the same node can have different retention policies.
- Disabled policies do not trigger backups.
## 4. Improve restore safety workflow
## 5. Improve restore safety workflow
Restore is destructive and should be guarded with a clearer preflight and confirmation flow.
Current state: VM restore requires typed confirmation, validates the snapshot against the VM, checks source/target size, writes the Restic dump to a staged ZFS volume, stops the VM, and swaps the staged volume into place with `zfs rename`. The previous production volume is kept as a rollback copy.
### Goal
Reduce the risk of accidental or unsafe restores.
@@ -82,17 +135,20 @@ Reduce the risk of accidental or unsafe restores.
- Validate node health before restore.
- Show selected snapshot metadata before restore.
- Show target VM status and disk size before restore.
- Require explicit typed confirmation including VM name.
- [x] Require explicit typed confirmation including VM name.
- Optionally offer “create backup before restore” when the VM is accessible.
- Log restore intent in audit log before dispatch.
- [x] Log restore intent in audit log before dispatch.
- [x] Replace direct `dd` to production ZVOL with a staged restore workflow.
- [ ] Define a safe container restore workflow with `zfs receive` or disable container restore surfaces entirely.
### Acceptance Criteria
- UI displays a restore plan before the final confirmation.
- Restore is blocked when required preflight checks fail.
- Audit log records restore attempts and results.
- [x] Restore stream failures happen on the staged volume, not the production disk.
## 5. Add failure notifications
## 6. Add failure notifications
Operators need to know when scheduled backups or restores fail.
@@ -115,7 +171,7 @@ Send notifications for failed or degraded operations.
- A test notification can be triggered from the UI.
- Notification failures are visible in Operations or audit logs.
## 6. Implement real snapshot file browsing
## 7. Implement real snapshot file browsing
Current snapshot browsing shows Restic contents, which for block-level backups is usually only `/vm.raw`.
@@ -138,7 +194,7 @@ Allow browsing files inside a backed-up VM disk image.
- Mounted/temporary resources are cleaned up after use.
- The workflow refuses unsupported or unsafe disk images with a clear error.
## 7. Add agent and management version reporting
## 8. Add agent and management version reporting
Multi-node setups need version visibility.
@@ -159,3 +215,28 @@ Show software version and compatibility state for management and each agent.
- Nodes page displays agent version.
- Management can identify incompatible agents.
- Health output includes version information.
## 9. Containerized management and UI deployment
The node-agent must remain a host-level systemd service because it needs direct Incus, ZFS, `/dev/zvol`, Restic, and device access. Management and the frontend do not need those host privileges and are good candidates for container deployment.
### Goal
Provide a production-ready Docker/Compose deployment for the management API and frontend while keeping node-agents installed as systemd services on Incus hosts.
### Tasks
- Add a `management` container image.
- Add a frontend image that serves the Vite build through a small static server or Nginx.
- Provide `compose.yaml` with persistent SQLite volume for management.
- Mount the agent CA certificate into the management container as read-only.
- Document required `CORS_ORIGINS`, `SESSION_COOKIE_SECURE`, `DATABASE_PATH`, and reverse-proxy assumptions.
- Decide whether the frontend calls the management API through the same origin reverse proxy or a separate API origin.
- Add health checks for both containers.
### Acceptance Criteria
- Management API and frontend can be started with Compose without installing Node.js on the management host.
- Management database survives container recreation.
- Cookie login works behind HTTPS.
- Management can connect to HTTPS node-agents using the configured CA file.