forked from EduCraft/curriculum-project-hub
feat: provision non-root Hub service
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
# 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
|
||||
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, `cph --version`, PostgreSQL connectivity and directory
|
||||
ownership all pass preflight.
|
||||
|
||||
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` 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`.
|
||||
@@ -2,12 +2,22 @@
|
||||
Description=Curriculum Project Hub (Feishu agent + cph)
|
||||
After=network-online.target postgresql.service
|
||||
Wants=network-online.target
|
||||
# ADR-0018: refuse to start when the configured OS sandbox binary disappears
|
||||
# after installation. Deployment preflight validates the executable itself;
|
||||
# this start-time assertion keeps later package/host changes fail-closed.
|
||||
AssertPathExists=__BWRAP_BIN__
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=__SERVICE_USER__
|
||||
Group=__SERVICE_GROUP__
|
||||
WorkingDirectory=__HUB_DIR__
|
||||
EnvironmentFile=__ENV_FILE__
|
||||
Environment=HOME=__SERVICE_HOME__
|
||||
Environment=XDG_STATE_HOME=__STATE_DIR__
|
||||
Environment=XDG_CACHE_HOME=__CACHE_DIR__
|
||||
Environment=PATH=__RUNTIME_PATH__
|
||||
UMask=0027
|
||||
# Run prisma migrations before each start, then launch the Hub.
|
||||
ExecStartPre=__NODE_BIN__ __HUB_DIR__/node_modules/prisma/build/index.js migrate deploy --schema __HUB_DIR__/prisma/schema.prisma
|
||||
ExecStart=__NODE_BIN__ __HUB_DIR__/dist/server.js
|
||||
@@ -16,9 +26,6 @@ RestartSec=5
|
||||
# Graceful shutdown: lark ws close + in-flight runs.
|
||||
KillSignal=SIGTERM
|
||||
TimeoutStopSec=30
|
||||
# ADR-0018: the agent sandbox (bubblewrap) must be available on the host —
|
||||
# failIfUnavailable makes the Hub refuse to run unsandboxed. Install bwrap.
|
||||
AssertPathExists=/usr/bin/bwrap
|
||||
# Hardening: no new privileges. ProtectSystem/ProtectHome are NOT set here —
|
||||
# they would break cph's render-cache extraction (~/.cache/cph/) and the
|
||||
# bwrap user-namespace setup. The OS-level sandbox (ADR-0018) is the hard
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
# PLATFORM_DEPLOY_PORT optional, defaults to 22
|
||||
# PLATFORM_DEPLOY_BASE optional, defaults to /srv/curriculum-project-hub
|
||||
# PLATFORM_DEPLOY_HEALTH_URL optional, defaults to http://127.0.0.1:8788/api/healthz
|
||||
# The target host must have Node.js, npm, rsync, PostgreSQL, systemd, bubblewrap
|
||||
# (`bwrap`, for the ADR-0018 agent sandbox), and a writable $PLATFORM_DEPLOY_BASE.
|
||||
# The target host must have Node.js 20+, npm, rsync, PostgreSQL client tools
|
||||
# (`pg_isready`), systemd, bubblewrap (`bwrap`, for the ADR-0018 agent sandbox),
|
||||
# and a compatible `cph` binary named by platform.env. The service installer
|
||||
# provisions the dedicated non-root identity and persistent directories.
|
||||
# Use a dedicated `deploy` user that has passwordless sudo for systemctl and
|
||||
# writing /etc/systemd/system/cph-hub.service through deploy/install_service.sh.
|
||||
set -euo pipefail
|
||||
|
||||
+215
-33
@@ -1,76 +1,258 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install the cph-hub systemd service on a host. Idempotent.
|
||||
#
|
||||
# Prerequisites already on the host: Node.js, npm, PostgreSQL, systemd.
|
||||
# The Hub repo is expected at HUB_DIR (default /srv/curriculum-project-hub/hub)
|
||||
# with `npm ci` + `npm run build` already run by deploy_platform.sh.
|
||||
# Provision and install the cph-hub systemd service on a supported Linux host.
|
||||
# The script is idempotent: existing identities/directories are validated and
|
||||
# never silently rewritten. An absent environment file is seeded completely,
|
||||
# then the script exits EX_CONFIG so no unit is installed with placeholders.
|
||||
#
|
||||
# Usage:
|
||||
# sudo BASE=/srv/curriculum-project-hub bash deploy/install_service.sh
|
||||
# sudo BASE=/srv/curriculum-project-hub \
|
||||
# HUB_DIR=/srv/curriculum-project-hub/hub \
|
||||
# bash deploy/install_service.sh
|
||||
set -euo pipefail
|
||||
|
||||
BASE="${BASE:-/srv/curriculum-project-hub}"
|
||||
HUB_DIR="${HUB_DIR:-$BASE/hub}"
|
||||
SERVICE_USER="${SERVICE_USER:-cph-hub}"
|
||||
SERVICE_GROUP="${SERVICE_GROUP:-$SERVICE_USER}"
|
||||
SERVICE_HOME="${SERVICE_HOME:-/var/lib/cph-hub/home}"
|
||||
STATE_DIR="${STATE_DIR:-/var/lib/cph-hub/state}"
|
||||
CACHE_DIR="${CACHE_DIR:-/var/cache/cph-hub}"
|
||||
WORKSPACE_ROOT="${WORKSPACE_ROOT:-/var/lib/cph-hub/workspaces}"
|
||||
HOST="${HOST:-127.0.0.1}"
|
||||
PORT="${PORT:-8788}"
|
||||
ENV_FILE="${ENV_FILE:-$BASE/.secrets/platform.env}"
|
||||
SYSTEMD_UNIT_DIR="${SYSTEMD_UNIT_DIR:-/etc/systemd/system}"
|
||||
NODE_BIN="${NODE_BIN:-$(command -v node || true)}"
|
||||
BWRAP_BIN="${BWRAP_BIN:-$(command -v bwrap || true)}"
|
||||
PG_ISREADY_BIN="${PG_ISREADY_BIN:-$(command -v pg_isready || true)}"
|
||||
RUNUSER_BIN="${RUNUSER_BIN:-$(command -v runuser || true)}"
|
||||
SETPRIV_BIN="${SETPRIV_BIN:-$(command -v setpriv || true)}"
|
||||
CPH_BIN_DEFAULT="${CPH_BIN_DEFAULT:-/usr/local/bin/cph}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PREFLIGHT_JS="$HUB_DIR/dist/deployment/preflight-cli.js"
|
||||
RUNTIME_PATH="$(dirname "$NODE_BIN"):$(dirname "$BWRAP_BIN"):$(dirname "$SETPRIV_BIN"):/usr/local/bin:/usr/bin:/bin"
|
||||
|
||||
if [ -z "$NODE_BIN" ]; then
|
||||
echo "node must be on PATH, or set NODE_BIN." >&2
|
||||
fail() {
|
||||
echo "[install] error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "required command is missing: $1"
|
||||
}
|
||||
|
||||
require_safe_unit_value() {
|
||||
local label="$1"
|
||||
local value="$2"
|
||||
if [[ ! "$value" =~ ^[A-Za-z0-9_./:@+-]+$ ]]; then
|
||||
fail "$label contains characters that cannot be rendered safely into the systemd unit: $value"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
fail "must run as root (use sudo)"
|
||||
fi
|
||||
if [ ! -d "$HUB_DIR" ]; then
|
||||
echo "HUB_DIR not found: $HUB_DIR (set HUB_DIR or clone the repo there)." >&2
|
||||
exit 1
|
||||
if [[ ! "$SERVICE_USER" =~ ^[a-z_][a-z0-9_-]*[$]?$ ]]; then
|
||||
fail "invalid SERVICE_USER: $SERVICE_USER"
|
||||
fi
|
||||
if [ ! -f "$HUB_DIR/dist/server.js" ]; then
|
||||
echo "build missing: run 'npm ci && npm run build' in $HUB_DIR first." >&2
|
||||
exit 1
|
||||
if [[ ! "$SERVICE_GROUP" =~ ^[a-z_][a-z0-9_-]*[$]?$ ]]; then
|
||||
fail "invalid SERVICE_GROUP: $SERVICE_GROUP"
|
||||
fi
|
||||
if [ ! -f "$SCRIPT_DIR/cph-hub.service" ]; then
|
||||
echo "service template missing: $SCRIPT_DIR/cph-hub.service" >&2
|
||||
exit 1
|
||||
if [ "$SERVICE_USER" = "root" ] || [ "$SERVICE_GROUP" = "root" ]; then
|
||||
fail "the Hub service identity must not be root"
|
||||
fi
|
||||
|
||||
# Seed the env file if absent. Secrets stay on the server, never in the repo.
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
install -d -m 0700 "$(dirname "$ENV_FILE")"
|
||||
for command in getent groupadd useradd install stat systemctl id; do
|
||||
require_command "$command"
|
||||
done
|
||||
for pair in \
|
||||
"NODE_BIN:$NODE_BIN" \
|
||||
"BWRAP_BIN:$BWRAP_BIN" \
|
||||
"PG_ISREADY_BIN:$PG_ISREADY_BIN"; do
|
||||
label="${pair%%:*}"
|
||||
value="${pair#*:}"
|
||||
[ -n "$value" ] || fail "$label is required and its executable was not found on PATH"
|
||||
[ -x "$value" ] || fail "$label is not executable: $value"
|
||||
done
|
||||
[ -n "$RUNUSER_BIN" ] || fail "RUNUSER_BIN is required and runuser was not found on PATH"
|
||||
[ -x "$RUNUSER_BIN" ] || fail "RUNUSER_BIN is not executable: $RUNUSER_BIN"
|
||||
[ -n "$SETPRIV_BIN" ] || fail "SETPRIV_BIN is required and setpriv was not found on PATH"
|
||||
[ -x "$SETPRIV_BIN" ] || fail "SETPRIV_BIN is not executable: $SETPRIV_BIN"
|
||||
|
||||
[ -d "$HUB_DIR" ] || fail "HUB_DIR not found: $HUB_DIR"
|
||||
[ -f "$HUB_DIR/dist/server.js" ] || fail "Hub build missing: $HUB_DIR/dist/server.js"
|
||||
[ -f "$PREFLIGHT_JS" ] || fail "deployment preflight missing from Hub build: $PREFLIGHT_JS"
|
||||
[ -f "$HUB_DIR/dist/deployment/service-runtime-probe.js" ] || fail "service runtime probe missing from Hub build"
|
||||
[ -f "$HUB_DIR/node_modules/prisma/build/index.js" ] || fail "production dependencies missing: run npm ci in $HUB_DIR"
|
||||
[ -f "$HUB_DIR/prisma/schema.prisma" ] || fail "Prisma schema missing: $HUB_DIR/prisma/schema.prisma"
|
||||
[ -f "$SCRIPT_DIR/cph-hub.service" ] || fail "service template missing: $SCRIPT_DIR/cph-hub.service"
|
||||
|
||||
for pair in \
|
||||
"BASE:$BASE" \
|
||||
"HUB_DIR:$HUB_DIR" \
|
||||
"SERVICE_HOME:$SERVICE_HOME" \
|
||||
"STATE_DIR:$STATE_DIR" \
|
||||
"CACHE_DIR:$CACHE_DIR" \
|
||||
"WORKSPACE_ROOT:$WORKSPACE_ROOT" \
|
||||
"ENV_FILE:$ENV_FILE" \
|
||||
"SYSTEMD_UNIT_DIR:$SYSTEMD_UNIT_DIR" \
|
||||
"NODE_BIN:$NODE_BIN" \
|
||||
"BWRAP_BIN:$BWRAP_BIN" \
|
||||
"RUNUSER_BIN:$RUNUSER_BIN" \
|
||||
"SETPRIV_BIN:$SETPRIV_BIN" \
|
||||
"RUNTIME_PATH:$RUNTIME_PATH"; do
|
||||
require_safe_unit_value "${pair%%:*}" "${pair#*:}"
|
||||
done
|
||||
|
||||
if [ -L "$ENV_FILE" ]; then
|
||||
fail "environment file must not be a symlink: $ENV_FILE"
|
||||
fi
|
||||
if [ ! -e "$ENV_FILE" ]; then
|
||||
install -d -o root -g root -m 0700 "$(dirname "$ENV_FILE")"
|
||||
umask 077
|
||||
cat >"$ENV_FILE" <<EOF
|
||||
# cph-hub production configuration. Required blanks must be filled before
|
||||
# rerunning install_service.sh; failed preflight never installs the unit.
|
||||
NODE_ENV=production
|
||||
DATABASE_URL=postgresql://paradigm:change-me@127.0.0.1:5432/paradigm
|
||||
# Claude Code SDK auth via OpenRouter's Anthropic Skin (ADR-0017).
|
||||
# ANTHROPIC_AUTH_TOKEN = your OpenRouter API key; ANTHROPIC_API_KEY must be empty.
|
||||
DATABASE_URL=
|
||||
ANTHROPIC_BASE_URL=https://openrouter.ai/api
|
||||
ANTHROPIC_AUTH_TOKEN=
|
||||
ANTHROPIC_API_KEY=
|
||||
FEISHU_APP_ID=
|
||||
FEISHU_APP_SECRET=
|
||||
FEISHU_BOT_OPEN_ID=
|
||||
PORT=$PORT
|
||||
CPH_BIN=$CPH_BIN_DEFAULT
|
||||
HOST=$HOST
|
||||
PORT=$PORT
|
||||
HUB_PROJECT_WORKSPACE_ROOT=$WORKSPACE_ROOT
|
||||
HUB_PUBLIC_BASE_URL=
|
||||
HUB_SESSION_SECRET=
|
||||
HUB_AGENT_MAX_TURNS=25
|
||||
HUB_FEISHU_LISTENER_ENABLED=true
|
||||
CPH_SANDBOX_EXTRA_DENY_READ=$ENV_FILE
|
||||
EOF
|
||||
chmod 0600 "$ENV_FILE"
|
||||
echo "Seeded $ENV_FILE — edit it to fill in ANTHROPIC_AUTH_TOKEN and Feishu creds, then re-run."
|
||||
exit 0
|
||||
echo "[install] seeded complete production configuration: $ENV_FILE" >&2
|
||||
echo "[install] fill every required blank, install cph at CPH_BIN, then rerun" >&2
|
||||
exit 78
|
||||
fi
|
||||
[ -f "$ENV_FILE" ] || fail "environment file is not a regular file: $ENV_FILE"
|
||||
|
||||
# Render the unit with concrete paths.
|
||||
UNIT=/etc/systemd/system/cph-hub.service
|
||||
PREFLIGHT_ARGS=(
|
||||
"$NODE_BIN" "$PREFLIGHT_JS"
|
||||
--env-file "$ENV_FILE"
|
||||
--node-bin "$NODE_BIN"
|
||||
--runuser-bin "$RUNUSER_BIN"
|
||||
--setpriv-bin "$SETPRIV_BIN"
|
||||
--service-user "$SERVICE_USER"
|
||||
--base-dir "$BASE"
|
||||
--hub-dir "$HUB_DIR"
|
||||
--service-home "$SERVICE_HOME"
|
||||
--state-dir "$STATE_DIR"
|
||||
--cache-dir "$CACHE_DIR"
|
||||
--workspace-root "$WORKSPACE_ROOT"
|
||||
--bwrap-bin "$BWRAP_BIN"
|
||||
--pg-isready-bin "$PG_ISREADY_BIN"
|
||||
)
|
||||
|
||||
# Required configuration, path isolation, binaries and PostgreSQL must pass
|
||||
# before identity/directory provisioning or unit installation begins.
|
||||
"${PREFLIGHT_ARGS[@]}"
|
||||
|
||||
NOLOGIN_SHELL=""
|
||||
for candidate in /usr/sbin/nologin /sbin/nologin /usr/bin/false /bin/false; do
|
||||
if [ -x "$candidate" ]; then
|
||||
NOLOGIN_SHELL="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
[ -n "$NOLOGIN_SHELL" ] || fail "no non-login shell found"
|
||||
|
||||
group_entry="$(getent group "$SERVICE_GROUP" || true)"
|
||||
if [ -z "$group_entry" ]; then
|
||||
groupadd --system "$SERVICE_GROUP"
|
||||
group_entry="$(getent group "$SERVICE_GROUP" || true)"
|
||||
[ -n "$group_entry" ] || fail "created service group cannot be resolved: $SERVICE_GROUP"
|
||||
fi
|
||||
IFS=: read -r resolved_group _ SERVICE_GID _ <<<"$group_entry"
|
||||
[ "$resolved_group" = "$SERVICE_GROUP" ] || fail "service group resolved to unexpected name: $resolved_group"
|
||||
[[ "$SERVICE_GID" =~ ^[0-9]+$ ]] || fail "service group has invalid gid: $SERVICE_GID"
|
||||
[ "$SERVICE_GID" -ne 0 ] || fail "service group must not have gid 0"
|
||||
|
||||
passwd_entry="$(getent passwd "$SERVICE_USER" || true)"
|
||||
if [ -z "$passwd_entry" ]; then
|
||||
useradd \
|
||||
--system \
|
||||
--gid "$SERVICE_GROUP" \
|
||||
--home-dir "$SERVICE_HOME" \
|
||||
--shell "$NOLOGIN_SHELL" \
|
||||
--no-create-home \
|
||||
"$SERVICE_USER"
|
||||
passwd_entry="$(getent passwd "$SERVICE_USER" || true)"
|
||||
[ -n "$passwd_entry" ] || fail "created service user cannot be resolved: $SERVICE_USER"
|
||||
fi
|
||||
IFS=: read -r resolved_user _ SERVICE_UID user_gid _ user_home user_shell <<<"$passwd_entry"
|
||||
[ "$resolved_user" = "$SERVICE_USER" ] || fail "service user resolved to unexpected name: $resolved_user"
|
||||
[[ "$SERVICE_UID" =~ ^[0-9]+$ ]] || fail "service user has invalid uid: $SERVICE_UID"
|
||||
[ "$SERVICE_UID" -ne 0 ] || fail "service user must not have uid 0"
|
||||
[ "$user_gid" = "$SERVICE_GID" ] || fail "service user primary gid $user_gid does not match $SERVICE_GROUP ($SERVICE_GID)"
|
||||
[ "$user_home" = "$SERVICE_HOME" ] || fail "service user home is $user_home; expected $SERVICE_HOME"
|
||||
case "$user_shell" in
|
||||
/usr/sbin/nologin | /sbin/nologin | /usr/bin/false | /bin/false) ;;
|
||||
*) fail "service user must have a non-login shell; found $user_shell" ;;
|
||||
esac
|
||||
service_gids="$(id -G "$SERVICE_USER")" || fail "cannot resolve groups for service user $SERVICE_USER"
|
||||
for service_gid in $service_gids; do
|
||||
if [ "$service_gid" != "$SERVICE_GID" ]; then
|
||||
fail "service user $SERVICE_USER must not have supplementary groups; found gid $service_gid in: $service_gids"
|
||||
fi
|
||||
done
|
||||
|
||||
provision_directory() {
|
||||
local path="$1"
|
||||
if [ -L "$path" ]; then
|
||||
fail "service directory must not be a symlink: $path"
|
||||
fi
|
||||
if [ -e "$path" ]; then
|
||||
[ -d "$path" ] || fail "service path exists but is not a directory: $path"
|
||||
actual="$(stat -c '%u:%g:%a' -- "$path")"
|
||||
expected="$SERVICE_UID:$SERVICE_GID:750"
|
||||
[ "$actual" = "$expected" ] || fail "service directory $path has $actual; expected $expected"
|
||||
return
|
||||
fi
|
||||
install -d -o "$SERVICE_UID" -g "$SERVICE_GID" -m 0750 "$path"
|
||||
}
|
||||
|
||||
provision_directory "$SERVICE_HOME"
|
||||
provision_directory "$STATE_DIR"
|
||||
provision_directory "$CACHE_DIR"
|
||||
provision_directory "$WORKSPACE_ROOT"
|
||||
|
||||
# Resolve every provisioned path again and verify uid/gid/mode before writing
|
||||
# the unit. This catches symlink or ownership changes between the two phases.
|
||||
"${PREFLIGHT_ARGS[@]}" --service-uid "$SERVICE_UID" --service-gid "$SERVICE_GID"
|
||||
|
||||
UNIT="$SYSTEMD_UNIT_DIR/cph-hub.service"
|
||||
install -d -o root -g root -m 0755 "$SYSTEMD_UNIT_DIR"
|
||||
TMP_UNIT="$(mktemp)"
|
||||
trap 'rm -f "$TMP_UNIT"' EXIT
|
||||
sed \
|
||||
-e "s|__SERVICE_USER__|$SERVICE_USER|g" \
|
||||
-e "s|__SERVICE_GROUP__|$SERVICE_GROUP|g" \
|
||||
-e "s|__SERVICE_HOME__|$SERVICE_HOME|g" \
|
||||
-e "s|__STATE_DIR__|$STATE_DIR|g" \
|
||||
-e "s|__CACHE_DIR__|$CACHE_DIR|g" \
|
||||
-e "s|__WORKSPACE_ROOT__|$WORKSPACE_ROOT|g" \
|
||||
-e "s|__HUB_DIR__|$HUB_DIR|g" \
|
||||
-e "s|__BASE_DIR__|$BASE|g" \
|
||||
-e "s|__ENV_FILE__|$ENV_FILE|g" \
|
||||
-e "s|__NODE_BIN__|$NODE_BIN|g" \
|
||||
-e "s|__BWRAP_BIN__|$BWRAP_BIN|g" \
|
||||
-e "s|__RUNTIME_PATH__|$RUNTIME_PATH|g" \
|
||||
"$SCRIPT_DIR/cph-hub.service" >"$TMP_UNIT"
|
||||
install -m 0644 "$TMP_UNIT" "$UNIT"
|
||||
rm -f "$TMP_UNIT"
|
||||
install -o root -g root -m 0644 "$TMP_UNIT" "$UNIT"
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable cph-hub.service
|
||||
echo "Installed cph-hub.service. Start with: systemctl start cph-hub"
|
||||
echo "Check health: curl http://127.0.0.1:$PORT/api/healthz"
|
||||
echo "[install] installed cph-hub.service for $SERVICE_USER:$SERVICE_GROUP"
|
||||
echo "[install] home=$SERVICE_HOME state=$STATE_DIR cache=$CACHE_DIR workspaces=$WORKSPACE_ROOT"
|
||||
echo "[install] start with: systemctl start cph-hub.service"
|
||||
|
||||
Reference in New Issue
Block a user