release v0.1.5

This commit is contained in:
gpuk-release
2026-09-17 00:02:06 +00:00
parent 406d457b8d
commit 983733003b
4 changed files with 524 additions and 128 deletions
+237 -35
View File
@@ -104,6 +104,72 @@ install_binary() { # [local-path]
gen_secret() { head -c 32 /dev/urandom | base64 | tr '+/' '-_' | tr -d '='; }
# The host ports a leftover app container is reached on ("ui mtls inference"),
# read off the container itself with the precedence the controller applies
# (core/published-ports.ts): host networking moves the listeners (GPUK_PORT,
# GPUK_MTLS_PORT, GPUK_LISTEN_ADDR); anything else keeps the image's fixed
# listeners and publishes them (GPUK_PUBLIC_*, then the port bindings). Empty
# when there is no such container or no docker.
leftover_container_ports() { # $1 = container name
command -v docker >/dev/null 2>&1 || return 0
# Same format string as install.sh's container_facts — one reading, two scripts.
_f=$(docker inspect -f '{{.State.Status}}|{{.HostConfig.NetworkMode}}|{{range $p, $b := .HostConfig.PortBindings}}{{range $b}}{{$p}}={{.HostPort}} {{end}}{{end}}{{"\n"}}{{range .Config.Env}}{{.}}{{"\n"}}{{end}}' "$1" 2>/dev/null) \
|| return 0
[ -n "$_f" ] || return 0
_head=$(printf '%s\n' "$_f" | head -1)
_r=${_head#*|}; _net=${_r%%|*}; _bind=${_r#*|}
_env() { printf '%s\n' "$_f" | sed -n "s/^$1=//p" | head -1; }
_pub() { printf '%s\n' "$_bind" | tr ' ' '\n' | sed -n "s/^$1\/tcp=//p" | head -1; }
if [ "$_net" = "host" ]; then
_ui=$(_env GPUK_PORT); _mtls=$(_env GPUK_MTLS_PORT)
_inf=$(_env GPUK_PROXY_PUBLIC_PORT)
[ -n "$_inf" ] || { _la=$(_env GPUK_LISTEN_ADDR); _inf=${_la##*:}; }
else
_ui=$(_env GPUK_PUBLIC_PORT); [ -n "$_ui" ] || _ui=$(_pub 8080)
_mtls=$(_env GPUK_PUBLIC_MTLS_PORT); [ -n "$_mtls" ] || _mtls=$(_pub 8443)
_inf=$(_env GPUK_PROXY_PUBLIC_PORT); [ -n "$_inf" ] || _inf=$(_pub 8200)
fi
printf '%s %s %s' "${_ui:-8080}" "${_mtls:-8443}" "${_inf:-8200}"
}
# Who holds a port: "process", "process in container NAME", or "". The process's
# cgroup names its container (host networking); a bridged publication is found
# as the container's port mapping in `docker ps` (the host-side holder is
# docker-proxy, which says nothing by itself). Same reading as install.sh.
port_owner() { # $1 = ss|netstat, $2 = port
_proc=""; _pid=""
case "$1" in
ss)
_line=$(ss -ltnpH "sport = :$2" 2>/dev/null | head -1)
_proc=$(printf '%s' "$_line" | sed -n 's/.*users:((\"\([^"]*\)\".*/\1/p')
_pid=$(printf '%s' "$_line" | sed -n 's/.*pid=\([0-9]*\).*/\1/p')
;;
netstat)
_field=$(netstat -ltnp 2>/dev/null \
| awk -v p="$2" '{n=split($4,a,":"); if (a[n]==p) {print $NF; exit}}')
case "$_field" in
*/*) _pid=${_field%%/*}; _proc=${_field#*/} ;;
*) _proc="$_field" ;;
esac
;;
esac
case "$_pid" in *[!0-9]*|"") _pid="" ;; esac
_ctr=""
if command -v docker >/dev/null 2>&1; then
if [ -n "$_pid" ] && [ -r "/proc/$_pid/cgroup" ]; then
_cid=$(sed -n 's#.*docker[-/]\([0-9a-f]\{64\}\).*#\1#p' "/proc/$_pid/cgroup" 2>/dev/null | head -1)
[ -z "$_cid" ] || _ctr=$(docker inspect -f '{{.Name}}' "$_cid" 2>/dev/null | sed 's|^/||')
fi
[ -n "$_ctr" ] || _ctr=$(docker ps --format '{{.Names}} {{.Ports}}' 2>/dev/null \
| awk -v p=":$2->" 'index($0, p) { print $1; exit }')
fi
if [ -n "$_ctr" ]; then
printf '%s' "${_proc:-a process} in container $_ctr"
else
printf '%s' "$_proc"
fi
}
seed_secrets() { # DATA_ROOT
_sd="$1/secrets"
mkdir -p "$_sd"; chmod 0700 "$_sd"
@@ -191,7 +257,7 @@ write_caddyfile() {
#
# The app already runs with GPUK_HSTS=true and GPUK_SESSION_COOKIE_SECURE=true
# (set by the public profile). What does NOT go through this proxy:
# - the worker mTLS channel (:8443): workers pin the controller CA and must
# - the worker mTLS channel (:$MTLS_PORT): workers pin the controller CA and must
# reach it DIRECTLY — terminating it here would break the pin.
# - worker<->worker data transfers (:8300): LAN-only by contract (OPS-68).
@@ -205,7 +271,7 @@ $DOMAIN {
#
# inference.$DOMAIN {
# encode zstd gzip
# reverse_proxy localhost:8200
# reverse_proxy localhost:$INFERENCE_PORT
# }
CADDY
chmod 0644 "$DATA_ROOT/caddy/Caddyfile"
@@ -242,8 +308,14 @@ hint_existing_caches() {
cmd_install() {
IMAGE=""; MODE="worker"; CLUSTER="default"; DATA_ROOT="/var/lib/gpu-kitchen"
CACHE_DIR=""; NETWORK="host"; CONTROLLER_URL=""; BIN_SRC=""; HEALTH_PORT="8001"
# 1337, not 8080: kept in lockstep with install.sh's default (INS-01).
ENROLL_TOKEN=""; HTTP_PORT="1337"; PROFILE=""; DOMAIN=""; DRY_RUN=0
# 1337, not 8080: kept in lockstep with install.sh's default (INS-01). The
# worker channel and the inference endpoint move the same way (INS-46).
ENROLL_TOKEN=""; HTTP_PORT="1337"; MTLS_PORT="8443"; INFERENCE_PORT="8200"
# Worker data plane (OPS-68): the daemon reads GPUK_DATA_PORT,
# GPUK_MODEL_TRANSFER_PORT and GPUK_HANDOVER_DATA_PORT from worker.env and
# announces the first two to the controller, so moving them here is complete.
DATA_PORT="8300"; TRANSFER_PORT="8301"; HANDOVER_PORT="8302"
PROFILE=""; DOMAIN=""; DRY_RUN=0
while [ $# -gt 0 ]; do
case "$1" in
--image) IMAGE="$2"; shift 2 ;;
@@ -261,6 +333,11 @@ cmd_install() {
--enroll-token|--token) ENROLL_TOKEN="$2"; shift 2 ;;
--health-port) HEALTH_PORT="$2"; shift 2 ;;
--http-port) HTTP_PORT="$2"; shift 2 ;;
--mtls-port) MTLS_PORT="$2"; shift 2 ;;
--inference-port) INFERENCE_PORT="$2"; shift 2 ;;
--data-port) DATA_PORT="$2"; shift 2 ;;
--transfer-port) TRANSFER_PORT="$2"; shift 2 ;;
--handover-port) HANDOVER_PORT="$2"; shift 2 ;;
--binary) BIN_SRC="$2"; shift 2 ;;
--dry-run) DRY_RUN=1; shift ;;
*) die "unknown install option: $1" ;;
@@ -279,6 +356,26 @@ cmd_install() {
if [ "$MODE" = "worker" ] && [ -n "$PROFILE" ]; then
die "--profile applies only to --mode controller; workers do not have an installation profile"
fi
for _pv in "$HTTP_PORT" "$MTLS_PORT" "$INFERENCE_PORT" "$HEALTH_PORT" \
"$DATA_PORT" "$TRANSFER_PORT" "$HANDOVER_PORT"; do
case "$_pv" in
''|*[!0-9]*) die "not a port number: '$_pv'" ;;
esac
[ "$_pv" -ge 1 ] && [ "$_pv" -le 65535 ] || die "port out of range: $_pv"
done
if [ "$HTTP_PORT" = "$MTLS_PORT" ] || [ "$HTTP_PORT" = "$INFERENCE_PORT" ] || [ "$MTLS_PORT" = "$INFERENCE_PORT" ]; then
die "--http-port, --mtls-port and --inference-port must differ (got $HTTP_PORT, $MTLS_PORT, $INFERENCE_PORT)"
fi
# The worker's four listeners (health + data plane) must differ too; the
# handover port is the data port's temporary twin (WRK-173), never the same.
_seen=""
for _pv in "$HEALTH_PORT" "$DATA_PORT" "$TRANSFER_PORT" "$HANDOVER_PORT"; do
case " $_seen " in
*" $_pv "*) die "--health-port, --data-port, --transfer-port and --handover-port must differ (got $HEALTH_PORT, $DATA_PORT, $TRANSFER_PORT, $HANDOVER_PORT)" ;;
esac
_seen="$_seen $_pv"
done
case "$PROFILE" in
""|homelab|studio|enterprise|public) ;;
*) die "--profile must be homelab, studio, enterprise or public (got '$PROFILE')" ;;
@@ -319,32 +416,54 @@ cmd_install() {
if [ "$DRY_RUN" -eq 1 ]; then
[ "$MODE" != "controller" ] || CLAIM_CODE_AVAILABLE=1
echo "==> dry run: no file, service or container was changed"
[ ! -f "$MANIFEST" ] \
|| echo "==> dry run: $MANIFEST exists — this render would be MERGED into it, controller-owned settings kept"
if [ "$MODE" = "worker" ]; then render_worker_manifest; else render_controller_manifest; fi
[ -z "$DOMAIN" ] || echo "==> dry run: would write $DATA_ROOT/caddy/Caddyfile for $DOMAIN"
return 0
fi
# ── Port conflicts (INS-46) — the mutator's own guard ────────────────────────
# install.sh's preflight already checks these, and on a terminal it can offer
# an alternative port. gpuk is the actual mutator and contributors call it
# DIRECTLY, so it re-checks and refuses, non-interactively. Same helpers as
# install.sh (both scripts ship standalone from the channel). A listener owned
# by an existing install is not a conflict: a manifest on disk means the
# re-run is the update path, and every checked port is then our own.
# install.sh's preflight already checks these, and on a terminal it offers an
# alternative port. gpuk is the actual mutator and contributors call it
# DIRECTLY, so it re-checks and refuses, non-interactively, naming the flag
# that moves the port. Same helpers as install.sh (both scripts ship standalone
# from the channel). A listener owned by an existing install is not a conflict:
# a manifest on disk means the re-run is the update path, and every checked
# port is then our own; without a manifest, a leftover app container still
# holds OUR ports — apply() renames and stops it before the new one starts.
if [ ! -f "$MANIFEST" ]; then
_port_tool=""
if command -v ss >/dev/null 2>&1; then _port_tool="ss"
elif command -v netstat >/dev/null 2>&1; then _port_tool="netstat"; fi
_port_tool="${GPUK_PORT_CHECK_TOOL:-}"
case "$_port_tool" in
""|ss|netstat) ;;
*) die "GPUK_PORT_CHECK_TOOL must be ss or netstat (got '$_port_tool')" ;;
esac
if [ -z "$_port_tool" ]; then
if command -v ss >/dev/null 2>&1; then _port_tool="ss"
elif command -v netstat >/dev/null 2>&1; then _port_tool="netstat"; fi
fi
_ours=""
if [ "$MODE" = "controller" ]; then
_ours=$(leftover_container_ports gpu-kitchen)
[ -z "$_ours" ] || echo "==> existing app container gpu-kitchen found (ports $_ours): the install replaces it"
fi
if [ -z "$_port_tool" ]; then
echo "==> warning: cannot check for port conflicts (no ss or netstat)"
else
if [ "$MODE" = "controller" ]; then
set -- "$HTTP_PORT" 8443 8200
set -- "$HTTP_PORT:UI:--http-port" \
"$MTLS_PORT:worker channel:--mtls-port" \
"$INFERENCE_PORT:inference endpoint:--inference-port"
else
# Worker data-plane ports (OPS-68) plus the local health listener.
set -- "$HEALTH_PORT" 8300 8301 8302
set -- "$HEALTH_PORT:worker health:--health-port" \
"$DATA_PORT:worker data plane:--data-port" \
"$TRANSFER_PORT:model transfers:--transfer-port" \
"$HANDOVER_PORT:handover data plane:--handover-port"
fi
for _p in "$@"; do
for _spec in "$@"; do
_p=${_spec%%:*}; _rest=${_spec#*:}; _label=${_rest%%:*}; _flag=${_rest#*:}
case " $_ours " in *" $_p "*) continue ;; esac
_busy=1
case "$_port_tool" in
ss) [ -n "$(ss -ltnH "sport = :$_p" 2>/dev/null)" ] || _busy=0 ;;
@@ -354,7 +473,11 @@ cmd_install() {
|| _busy=0
;;
esac
[ "$_busy" -eq 0 ] || die "port $_p is already in use. Free it first, then run the install again."
[ "$_busy" -eq 0 ] && continue
_owner=$(port_owner "$_port_tool" "$_p")
_hint="Free it first, then run the install again."
[ -z "$_flag" ] || _hint="Pass $_flag <p> to choose another port, or free it first."
die "port $_p ($_label) is already in use by ${_owner:-an unknown process}. $_hint"
done
fi
fi
@@ -378,6 +501,9 @@ cmd_install() {
{
echo "GPUK_CLUSTER=$CLUSTER"
echo "GPUK_WORKER_HEALTH_PORT=$HEALTH_PORT"
echo "GPUK_DATA_PORT=$DATA_PORT"
echo "GPUK_MODEL_TRANSFER_PORT=$TRANSFER_PORT"
echo "GPUK_HANDOVER_DATA_PORT=$HANDOVER_PORT"
echo "GPUK_MACHINE_ID_FILE=$MACHINE_ID_FILE"
echo "NODE_DISPLAY_NAME=$(hostname)"
[ -n "$CONTROLLER_URL" ] && echo "GPUK_CONTROLLER_URLS=$CONTROLLER_URL"
@@ -392,7 +518,7 @@ cmd_install() {
echo "GPUK_CLUSTER=$CLUSTER"
echo "GPUK_WORKER_HEALTH_PORT=$HEALTH_PORT"
echo "GPUK_MACHINE_ID_FILE=$MACHINE_ID_FILE"
echo "GPUK_CONTROLLER_URLS=wss://127.0.0.1:8443"
echo "GPUK_CONTROLLER_URLS=wss://127.0.0.1:$MTLS_PORT"
echo "GPUK_SELF_ENROLL_FILE=$SELF_ENROLL_FILE"
echo "NODE_DISPLAY_NAME=$(hostname)"
} > "$WORKER_ENV"
@@ -439,7 +565,7 @@ cmd_install() {
# No unix socket any more: workerd reconciles the app container in-process from
# the on-disk manifest (there is no backend to relay through on the very first
# boot). Steady-state updates go through the daemon over WS.
"$BIN_DEST" apply || die "apply failed. Check: gpuk logs"
GPUK_MANIFEST_PATH="$MANIFEST" "$BIN_DEST" apply || die "apply failed. Check: gpuk logs"
echo
echo "Done. The worker daemon is running and the app container is up."
echo " Status : gpuk status App logs: gpuk logs Daemon: journalctl -u $SERVICE_NAME"
@@ -463,7 +589,36 @@ render_worker_manifest() {
JSON
}
write_worker_manifest() { render_worker_manifest > "$MANIFEST"; }
# Every env key gpuk may ever write into a manifest — conditional ones included.
# On a re-run the merge sets each of them to the fresh value or DELETES it when the
# fresh render no longer carries it (a profile change drops GPUK_HSTS); any other
# key was pushed by the controller and is kept. Keep this list in step with
# render_controller_manifest.
INSTALL_ENV_KEYS="NODE_ENV,GPUK_MODE,GPUK_CLUSTER,GPUK_SELF_ENROLL_FILE,NODE_DISPLAY_NAME,HF_HOME,GPUK_DATA_ROOT,GPUK_INSTALL_PROFILE,GPUK_HSTS,GPUK_SESSION_COOKIE_SECURE,GPUK_BOOTSTRAP_MUST_CHANGE,BACKEND_DOCKER_NETWORK,GPUK_PORT,GPUK_PUBLIC_PORT,GPUK_MTLS_PORT,GPUK_PUBLIC_MTLS_PORT,GPUK_LISTEN_ADDR,GPUK_PROXY_PUBLIC_PORT"
# Write the manifest — or, when one exists, MERGE into it (INS-03). Re-running the
# installer is the update path, and the manifest is not ours alone: since the first
# install the controller has patched it (extra cache disks, shared origin, eviction,
# LED binary, ports moved from Settings -> Network…). Rewriting it from flags would
# silently undo all of that, so the fresh render goes through the daemon's own
# `install-manifest`, which only replaces what the installer owns (image, mode,
# cluster, network, data root, ports, secret references, the primary cache disk,
# the env keys above) and keeps the rest. The first write is the render as-is.
write_manifest() { # $1 = render function
if [ -f "$MANIFEST" ]; then
"$1" > "$MANIFEST.new"
chmod 0600 "$MANIFEST.new"
GPUK_MANIFEST_PATH="$MANIFEST" "$BIN_DEST" install-manifest \
--from "$MANIFEST.new" --own-env "$INSTALL_ENV_KEYS" >/dev/null \
|| { rm -f "$MANIFEST.new"; die "could not merge the new settings into $MANIFEST"; }
rm -f "$MANIFEST.new"
echo "==> merged into the existing $MANIFEST (controller-owned settings kept)"
else
"$1" > "$MANIFEST"
fi
}
write_worker_manifest() { write_manifest render_worker_manifest; }
# Controller manifest: the declarative app-container description workerd applies.
render_controller_manifest() {
@@ -490,18 +645,25 @@ render_controller_manifest() {
[ "$NETWORK" = "host" ] || EXTRA_ENV="$EXTRA_ENV,\"BACKEND_DOCKER_NETWORK\":\"$(json_str "$NETWORK")\""
# Published != bound (INS-43). On a bridged network the container keeps the image's
# FIXED listeners — nginx 8080, worker mTLS 8443 — and --http-port only moves the HOST
# side of the publication; Settings -> Network moves it later by patching this same
# manifest, so the container port must never become a variable. With host networking
# nothing is published and the listener itself takes the port.
# FIXED listeners — nginx 8080, worker mTLS 8443, gpuk-proxy 8200 — and the port
# flags only move the HOST side of the publication; Settings -> Network moves it
# later by patching this same manifest, so the container port must never become a
# variable. With host networking nothing is published and the listeners themselves
# take the ports (core/published-ports.ts reads this back with the same rules; the
# proxy port is GPUK_LISTEN_ADDR for the proxy, GPUK_PROXY_PUBLIC_PORT for what the
# backend shows — core/cluster-settings.ts proxyPublicPort).
PORTS_JSON="{}"
if [ "$NETWORK" = "host" ]; then
EXTRA_ENV="$EXTRA_ENV,\"GPUK_PORT\":\"$(json_str "$HTTP_PORT")\""
EXTRA_ENV="$EXTRA_ENV,\"GPUK_MTLS_PORT\":\"$(json_str "$MTLS_PORT")\""
EXTRA_ENV="$EXTRA_ENV,\"GPUK_LISTEN_ADDR\":\"0.0.0.0:$(json_str "$INFERENCE_PORT")\""
else
EXTRA_ENV="$EXTRA_ENV,\"GPUK_PORT\":\"8080\""
EXTRA_ENV="$EXTRA_ENV,\"GPUK_PUBLIC_PORT\":\"$(json_str "$HTTP_PORT")\""
PORTS_JSON="{\"8080\":$HTTP_PORT,\"8200\":8200,\"8443\":8443}"
EXTRA_ENV="$EXTRA_ENV,\"GPUK_PUBLIC_MTLS_PORT\":\"$(json_str "$MTLS_PORT")\""
PORTS_JSON="{\"8080\":$HTTP_PORT,\"8200\":$INFERENCE_PORT,\"8443\":$MTLS_PORT}"
fi
EXTRA_ENV="$EXTRA_ENV,\"GPUK_PROXY_PUBLIC_PORT\":\"$(json_str "$INFERENCE_PORT")\""
cat <<JSON
{
@@ -532,7 +694,7 @@ render_controller_manifest() {
JSON
}
write_controller_manifest() { render_controller_manifest > "$MANIFEST"; }
write_controller_manifest() { write_manifest render_controller_manifest; }
# ── control subcommands ────────────────────────────────────────────────────────
container_name() {
@@ -770,7 +932,7 @@ cmd_enroll() {
# has no app container — `apply` there is a no-op with a clear message.
cmd_apply() {
need_root
"$BIN_DEST" apply
GPUK_MANIFEST_PATH="$MANIFEST" "$BIN_DEST" apply
}
# ── update ─────────────────────────────────────────────────────────────────────
@@ -824,7 +986,7 @@ cmd_update() {
fi
if [ -z "$_want" ]; then
echo "==> cannot reach $CHANNEL_URL — re-applying the pinned $_current"
"$BIN_DEST" apply
GPUK_MANIFEST_PATH="$MANIFEST" "$BIN_DEST" apply
return 0
fi
# Pick the digest matching the installed edition by image basename — the
@@ -849,15 +1011,51 @@ cmd_update() {
else
echo "==> already on $_current — re-pulling and recreating"
fi
"$BIN_DEST" apply
GPUK_MANIFEST_PATH="$MANIFEST" "$BIN_DEST" apply
}
# ── uninstall ──────────────────────────────────────────────────────────────────
# Plain: stop and remove the service, touch nothing else (a pause, reversible by
# `gpuk install`). --purge: everything the installer created goes — the app
# container (and a leftover -old twin), $ETC_DIR with the manifest and the
# enrolled identity, the binary — EXCEPT the data root: database, models and the
# secrets (ENCRYPTION_KEY above all, OPS-04) are the operator's to delete, by
# hand, knowingly. After a purge the next install is a first install (INS-03);
# without it, install.sh sees the manifest and treats the re-run as an update.
cmd_uninstall() {
need_root
_purge=0
while [ $# -gt 0 ]; do
case "$1" in
--purge) _purge=1; shift ;;
*) die "unknown uninstall option: $1 (expected: --purge)" ;;
esac
done
_cn=$(container_name)
_root=$(manifest_data_root)
systemctl disable --now "$SERVICE_NAME" 2>/dev/null || true
rm -f "$UNIT_DEST"; systemctl daemon-reload 2>/dev/null || true
echo "Removed the systemd service. Left in place: $BIN_DEST, $ETC_DIR (incl. identity),"
echo "the data root and any app container. Delete them manually for a full cleanup."
echo "==> removed the systemd service"
if [ "$_purge" -eq 0 ]; then
echo "Left in place: $BIN_DEST, $ETC_DIR (manifest, identity), the data root and any"
echo "app container — 'gpuk install' brings the service back on them."
echo "For a clean slate (everything but the data root): gpuk uninstall --purge"
return 0
fi
if [ -n "$_cn" ] && command -v docker >/dev/null 2>&1; then
for _c in "$_cn" "$_cn-old"; do
docker rm -f "$_c" >/dev/null 2>&1 && echo "==> removed container $_c"
done
fi
for _d in "$ETC_DIR" "$IDENTITY_DIR"; do
case "$_d" in ""|/|/etc|/usr|/var) die "refusing to remove $_d" ;; esac
[ ! -e "$_d" ] || { rm -rf "$_d"; echo "==> removed $_d"; }
done
[ ! -e "$MACHINE_ID_FILE" ] || rm -f "$MACHINE_ID_FILE"
[ ! -e "$BIN_DEST" ] || { rm -f "$BIN_DEST"; echo "==> removed $BIN_DEST"; }
echo
echo "Kept: the data root${_root:+ $_root} — database, models and secrets (ENCRYPTION_KEY)."
echo "A new install over it reuses them. To delete it too, knowingly: rm -rf ${_root:-<data-root>}"
}
usage() {
@@ -865,10 +1063,12 @@ usage() {
gpuk — GPU Kitchen host daemon (gpu-kitchen-worker)
gpuk install --mode controller --image <ref> [--profile homelab|studio|enterprise|public]
[--cluster N] [--cache-dir P]
[--data-root P] [--http-port P] [--network host|bridge|<net>] [--binary <path>]
[--cluster N] [--cache-dir P] [--data-root P]
[--http-port P] [--mtls-port P] [--inference-port P]
[--network host|bridge|<net>] [--binary <path>]
gpuk install --mode worker --controller wss://<host>:<port> --enroll-token gk_enroll_...
[--cluster N] [--cache-dir P] [--binary <path>]
[--health-port P] [--data-port P] [--transfer-port P] [--handover-port P]
gpuk install ... --dry-run Validate inputs and print the manifest without changing the host
gpuk status Service state, enrollment, /health, app container status
gpuk enroll --controller wss://<host>:<port> --token gk_enroll_...
@@ -880,7 +1080,9 @@ gpuk — GPU Kitchen host daemon (gpu-kitchen-worker)
gpuk backup [DIR] (controller) Cold snapshot of the embedded pgdata (stop → tar → restart)
gpuk manifest Print the current manifest
gpuk logs Follow the app container logs (controller) or the daemon journal
gpuk uninstall Remove the systemd service
gpuk uninstall Remove the systemd service, leave everything else in place
gpuk uninstall --purge Also remove the app container, /etc/gpu-kitchen and the binary
(never the data root: database, models, secrets)
Most people never run this directly: the channel's install.sh installs it
(https://repo.byterain.io/gpukitchen/channel/raw/branch/main/install.sh).
@@ -902,7 +1104,7 @@ case "$cmd" in
_img=$(manifest_image)
if [ -n "$_img" ]; then exec docker logs -f "$(container_name)"; else exec journalctl -u "$SERVICE_NAME" -f; fi
;;
uninstall) cmd_uninstall ;;
uninstall) cmd_uninstall "$@" ;;
help|-h|--help) usage ;;
*) usage; exit 1 ;;
esac