# Hub service installation The initial production target is a supported Linux host with systemd, PostgreSQL, Node.js 20 or newer, `pg_isready`, `runuser`, `setpriv`, bubblewrap, `socat` and `cph`. The Hub runs as the dedicated non-login `cph-hub` user; do not create or run the unit as root. Build or deploy the Hub under `/srv/curriculum-project-hub`, then run: ```sh sudo BASE=/srv/curriculum-project-hub \ HUB_DIR=/srv/curriculum-project-hub/hub \ bash /srv/curriculum-project-hub/hub/deploy/install_service.sh ``` On a clean host the first invocation creates `/srv/curriculum-project-hub/.secrets/platform.env` with every supported production key and exits with status 78. Fill every required blank, set `CPH_BIN` to an executable compatible `cph`, keep the file mode at `0600`, and run the same command again. No systemd unit is installed until configuration, paths, Node, bubblewrap, `socat`, `cph --version`, PostgreSQL connectivity and directory ownership all pass preflight. The deployment path also runs `npm run audit:production` from the locked clean install before building or restarting the service. A current high or critical production advisory therefore blocks deployment instead of becoming a warning buried in CI output. An existing service account is accepted only when its primary group, home and non-login shell match the requested configuration and it has no supplementary groups. After provisioning, the installer executes path-access, built Hub, Prisma, an authenticated database query, `cph`, `socat` and bubblewrap namespace probes as that account with `no_new_privs` set. Inaccessible parents, UID-specific database/network policy, or a host that forbids unprivileged user namespaces therefore fail before unit installation. The default persistent layout is deliberately outside every source/release tree: ```text /var/lib/cph-hub/home /var/lib/cph-hub/state /var/lib/cph-hub/workspaces /var/cache/cph-hub ``` Custom paths are supported with `SERVICE_HOME`, `STATE_DIR`, `CACHE_DIR` and `WORKSPACE_ROOT`, but all must be absolute. The installer rejects any persistent path that is equal to, above, or below `BASE`; this keeps rsync, release switching, rollback and pruning unable to traverse customer workspaces. Set the same custom workspace in `HUB_PROJECT_WORKSPACE_ROOT` inside `platform.env`. After a successful installation: ```sh sudo systemctl start cph-hub.service sudo systemctl status cph-hub.service ``` `HOST` and `PORT` in `platform.env` are passed to the real HTTP listener. The default `127.0.0.1:8788` expects a local TLS reverse proxy; the public OAuth URL must be an HTTPS `HUB_PUBLIC_BASE_URL`.