Skip to content

Instantly share code, notes, and snippets.

@grafuls
Last active May 18, 2026 10:35
Show Gist options
  • Select an option

  • Save grafuls/833d2bd8e5ec2909caf152c45b46e932 to your computer and use it in GitHub Desktop.

Select an option

Save grafuls/833d2bd8e5ec2909caf152c45b46e932 to your computer and use it in GitHub Desktop.
Openclaw setup
#!/usr/bin/env bash
#
# setup-openclaw.sh
# ------------------------------------------------------------------------------
# Bootstraps an OpenClaw gateway on a fresh Hetzner Cloud VPS running Ubuntu
# 24.04 LTS. Tested on CAX11 (ARM, 4 GB) and CX23 (x86, 4 GB); works on any
# 2 GB+ Ubuntu/Debian box.
# Idempotent: re-running on a partially-configured box is safe.
#
# Usage (as root, on the freshly-provisioned VPS):
#
# Local file:
# sudo bash setup-openclaw.sh
#
# From a gist / raw URL (env vars must go BEFORE the pipe, exported, so the
# sudo'd shell inherits them — see SSH_PUBLIC_KEY note):
# curl -fsSL https://gist.githubusercontent.com/<you>/<id>/raw/setup-openclaw.sh \
# | sudo -E SSH_PUBLIC_KEY="$(cat ~/.ssh/id_ed25519.pub)" bash
#
# Or download then run (simplest, recommended for first run):
# curl -fsSL https://gist.githubusercontent.com/<you>/<id>/raw/setup-openclaw.sh -o s.sh
# sudo SSH_PUBLIC_KEY="$(cat ~/.ssh/id_ed25519.pub)" bash s.sh
#
# After this completes, log in as the created user and run:
# tmux new -s claw
# openclaw onboard --install-daemon
# ------------------------------------------------------------------------------
set -euo pipefail
# ---- Self-reexec when piped (curl | bash) ------------------------------------
# This script uses heredocs (bash <<'INNER') that read from the script source.
# When streamed via a pipe, stdin is the script itself and those heredocs can
# be consumed unreliably. If we detect we're NOT running from a regular file,
# copy ourselves to a temp file and re-exec from there so every heredoc reads
# correctly. Env vars are preserved across the re-exec.
_src="${BASH_SOURCE[0]:-}"
if [[ -z "$_src" || ! -f "$_src" || "$_src" == "/dev/stdin" || "$_src" == "bash" ]]; then
_self="$(mktemp /tmp/setup-openclaw.XXXXXX.sh)"
cat > "$_self"
chmod +x "$_self"
OPENCLAW_SELF_TMP="$_self" exec bash "$_self" "$@"
fi
# If we were re-exec'd from a temp copy, remove it on exit (success or fail).
if [[ -n "${OPENCLAW_SELF_TMP:-}" ]]; then
trap 'rm -f "$OPENCLAW_SELF_TMP"' EXIT
fi
# ---- Configuration (override via env vars) -----------------------------------
: "${CLAW_USER:=claw}" # non-root user that runs the gateway
: "${NODE_MAJOR:=24}" # NodeSource major version (OpenClaw recommends 24)
: "${SSH_PORT:=22}" # change if you've moved sshd
: "${HARDEN_SSH:=yes}" # disable root login + password auth
: "${ENABLE_UFW:=yes}" # firewall: SSH in, everything else denied
: "${PASSWORDLESS_SUDO:=yes}" # convenient on a single-user box; set "no" to require password
: "${INSTALL_OPENWEBUI:=yes}" # install Docker + stage Open WebUI (ChatGPT-style chat UI)
: "${SSH_PUBLIC_KEY:=}" # optional: 'ssh-ed25519 AAAA... you@host' to inject directly
# into claw's authorized_keys. If unset, the script falls back
# to copying /root/.ssh/authorized_keys.
: "${INSTALL_HOMEBREW:=yes}" # install Homebrew (Linuxbrew) for claw, non-interactively
# ---- Pre-flight --------------------------------------------------------------
if [[ $EUID -ne 0 ]]; then
echo "error: must run as root. try: sudo bash $0" >&2
exit 1
fi
if ! grep -qE '^(ID|ID_LIKE)=.*(ubuntu|debian)' /etc/os-release; then
echo "error: this script targets Ubuntu/Debian. detected:" >&2
grep PRETTY_NAME /etc/os-release >&2
exit 1
fi
export DEBIAN_FRONTEND=noninteractive
log() { printf '\n\033[1;36m==>\033[0m %s\n' "$*"; }
# Pre-flight: if we're going to disable root SSH login, make absolutely sure
# claw will have a key to log in with. Refusing now is much better than
# locking you out at the reload step.
if [[ "$HARDEN_SSH" == "yes" ]]; then
has_key=0
[[ -n "$SSH_PUBLIC_KEY" ]] && has_key=1
[[ -s /root/.ssh/authorized_keys ]] && has_key=1
if id -u "$CLAW_USER" >/dev/null 2>&1 \
&& [[ -s "/home/$CLAW_USER/.ssh/authorized_keys" ]]; then
has_key=1
fi
if [[ "$has_key" -eq 0 ]]; then
cat >&2 <<EOF
error: HARDEN_SSH=yes but no SSH key source found for '$CLAW_USER'.
This would lock you out when sshd reloads. Aborting BEFORE any changes.
Pick one:
1. Pass your public key directly:
sudo SSH_PUBLIC_KEY='ssh-ed25519 AAAA... you@host' bash $0
2. Put your key in /root/.ssh/authorized_keys before running.
3. Skip hardening for now:
sudo HARDEN_SSH=no bash $0
(you can re-run with HARDEN_SSH=yes after adding keys to claw)
EOF
exit 1
fi
fi
# ---- 1. System update + base packages ----------------------------------------
log "Updating apt and installing base packages"
apt-get update -y
apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade -y
apt-get install -y \
curl ca-certificates gnupg lsb-release \
build-essential git tmux htop jq \
unattended-upgrades ufw
systemctl enable --now unattended-upgrades.service
# ---- 2. Create non-root user -------------------------------------------------
# Each sub-step is independent and idempotent so a re-run after partial failure
# fills in anything that's missing without clobbering user-added state.
if ! id -u "$CLAW_USER" >/dev/null 2>&1; then
log "Creating user '$CLAW_USER'"
adduser --disabled-password --gecos "" "$CLAW_USER"
else
log "User '$CLAW_USER' already exists"
fi
# Always ensure sudo group membership (no-op if already a member)
usermod -aG sudo "$CLAW_USER"
# Always ensure (or remove) passwordless sudo per current config
if [[ "$PASSWORDLESS_SUDO" == "yes" ]]; then
echo "$CLAW_USER ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/90-$CLAW_USER"
chmod 440 "/etc/sudoers.d/90-$CLAW_USER"
else
rm -f "/etc/sudoers.d/90-$CLAW_USER"
fi
# Always ensure the .ssh directory exists with correct perms
install -d -m 700 -o "$CLAW_USER" -g "$CLAW_USER" "/home/$CLAW_USER/.ssh"
AK="/home/$CLAW_USER/.ssh/authorized_keys"
touch "$AK"
chown "$CLAW_USER:$CLAW_USER" "$AK"
chmod 600 "$AK"
# Ensure SSH_PUBLIC_KEY is present (idempotent: only append if not already there)
if [[ -n "$SSH_PUBLIC_KEY" ]]; then
if ! grep -qxF "$SSH_PUBLIC_KEY" "$AK"; then
echo "$SSH_PUBLIC_KEY" >> "$AK"
log "Added SSH_PUBLIC_KEY to $AK"
fi
fi
# Seed from /root/.ssh/authorized_keys if claw's is still empty
if [[ ! -s "$AK" ]] && [[ -s /root/.ssh/authorized_keys ]]; then
cat /root/.ssh/authorized_keys > "$AK"
chown "$CLAW_USER:$CLAW_USER" "$AK"
chmod 600 "$AK"
log "Seeded $AK from /root/.ssh/authorized_keys"
fi
# Final safety check — if HARDEN_SSH=yes the pre-flight already enforced this,
# but log loudly if claw still has no keys.
if [[ ! -s "$AK" ]]; then
echo "warning: $AK is empty. claw cannot SSH in until a key is added." >&2
fi
# ---- 3. Install Node.js via NodeSource ---------------------------------------
need_node_install=1
if command -v node >/dev/null 2>&1; then
current_major="$(node -v | sed 's/^v//; s/\..*//')"
if [[ "$current_major" -ge "$NODE_MAJOR" ]]; then
need_node_install=0
fi
fi
if [[ "$need_node_install" -eq 1 ]]; then
log "Installing Node.js ${NODE_MAJOR}.x from NodeSource"
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash -
apt-get install -y nodejs
fi
log "Node $(node --version), npm $(npm --version)"
# ---- 4. Optional: Homebrew (Linuxbrew), non-interactively --------------------
# Homebrew's installer refuses to run as root AND forces an interactive sudo
# prompt for the few privileged steps (creating + chowning its prefix). We do
# those privileged steps here as root, then run the installer as claw with
# NONINTERACTIVE=1 so it never needs sudo and never prompts.
if [[ "$INSTALL_HOMEBREW" == "yes" ]]; then
if sudo -u "$CLAW_USER" bash -lc 'command -v brew' >/dev/null 2>&1; then
log "Homebrew already installed for $CLAW_USER, skipping"
else
log "Preparing Linuxbrew prefix and installing Homebrew non-interactively"
# Build deps Homebrew needs on Linux
apt-get install -y procps file
# Create + own the prefix as root so the installer never calls sudo
install -d -m 755 -o "$CLAW_USER" -g "$CLAW_USER" /home/linuxbrew/.linuxbrew
# Run the official installer AS claw, fully non-interactive
sudo -u "$CLAW_USER" -H bash <<'BREW'
set -e
export NONINTERACTIVE=1
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Add brew to the login shell env exactly once. Match ANY existing
# 'brew shellenv' line (with or without a shell arg) so this is robust
# across Homebrew versions and never double-adds on re-run.
SHELLENV='eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"'
if ! grep -qE 'brew shellenv' "$HOME/.bashrc" 2>/dev/null; then
{
echo ''
echo '# --- Homebrew ---'
echo "$SHELLENV"
echo '# --- end Homebrew ---'
} >> "$HOME/.bashrc"
fi
# Make brew usable immediately in this provisioning shell too
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
BREW
log "Homebrew installed for $CLAW_USER ($(sudo -u "$CLAW_USER" bash -lc 'brew --version 2>/dev/null | head -1' || echo '?'))"
fi
fi
# ---- 5. Compile-cache directory (shared, world-writable so any user benefits) -
mkdir -p /var/tmp/openclaw-compile-cache
chown "$CLAW_USER:$CLAW_USER" /var/tmp/openclaw-compile-cache
chmod 755 /var/tmp/openclaw-compile-cache
# ---- 6. Per-user shell env + npm prefix --------------------------------------
log "Configuring user-local npm prefix and shell env for '$CLAW_USER'"
sudo -u "$CLAW_USER" -H bash <<'INNER'
set -e
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
mkdir -p "$HOME/.npm-global"
npm config set prefix "$HOME/.npm-global"
if ! grep -q '# --- OpenClaw env ---' "$HOME/.bashrc" 2>/dev/null; then
cat >> "$HOME/.bashrc" <<'BRC'
# --- OpenClaw env ---
export PATH="$HOME/.npm-global/bin:$PATH"
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
export NODE_COMPILE_CACHE="/var/tmp/openclaw-compile-cache"
export OPENCLAW_NO_RESPAWN=1
# --- end OpenClaw env ---
BRC
fi
# Make the same env visible to systemd user services (which don't read .bashrc)
mkdir -p "$HOME/.config/environment.d"
cat > "$HOME/.config/environment.d/openclaw.conf" <<ENV
NODE_COMPILE_CACHE=/var/tmp/openclaw-compile-cache
OPENCLAW_NO_RESPAWN=1
PATH=%h/.npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ENV
INNER
# ---- 7. Pre-stage a systemd drop-in for the (future) gateway unit ------------
# OpenClaw onboard creates ~/.config/systemd/user/openclaw-gateway.service.
# Drop-in files in <unit>.service.d/ are picked up whenever the unit loads.
log "Pre-staging systemd drop-in for small-VM tuning"
sudo -u "$CLAW_USER" -H bash <<'INNER'
set -e
DROPIN="$HOME/.config/systemd/user/openclaw-gateway.service.d"
mkdir -p "$DROPIN"
cat > "$DROPIN/override.conf" <<'UNIT'
[Service]
Environment=OPENCLAW_NO_RESPAWN=1
Environment=NODE_COMPILE_CACHE=/var/tmp/openclaw-compile-cache
Restart=always
RestartSec=2
TimeoutStartSec=90
UNIT
INNER
# ---- 8. Enable systemd user lingering ----------------------------------------
# Without this, systemctl --user services die when your SSH session ends.
# This is THE most common headless-VPS gotcha for OpenClaw.
log "Enabling systemd linger for '$CLAW_USER'"
loginctl enable-linger "$CLAW_USER"
# Give logind a moment to bring /run/user/<uid> up
sleep 1
# ---- 9. Install OpenClaw via npm ---------------------------------------------
log "Installing openclaw via npm (this can take a few minutes)"
sudo -u "$CLAW_USER" -H bash <<'INNER'
set -e
export PATH="$HOME/.npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export NODE_COMPILE_CACHE="/var/tmp/openclaw-compile-cache"
npm install -g openclaw@latest
which openclaw
openclaw --version || true
INNER
# ---- 10. Optional: Docker + Open WebUI staging ------------------------------
# Open WebUI is a polished, self-hosted ChatGPT-style frontend. It talks to
# OpenClaw's OpenAI-compatible /v1 endpoint over loopback.
#
# We install Docker and pre-pull the image here. The actual config edit
# (enabling chatCompletions) and container start happen via a helper script
# that runs AFTER `openclaw onboard --install-daemon`, because onboarding
# is what creates ~/.openclaw/openclaw.json in the first place.
if [[ "$INSTALL_OPENWEBUI" == "yes" ]]; then
log "Installing Docker for Open WebUI"
if ! command -v docker >/dev/null 2>&1; then
curl -fsSL https://get.docker.com | sh
fi
systemctl enable --now docker
usermod -aG docker "$CLAW_USER"
log "Pre-pulling Open WebUI image (so first 'docker run' is instant)"
docker pull ghcr.io/open-webui/open-webui:main \
|| echo "warning: docker pull failed — helper script will retry at run time"
log "Writing helper /home/$CLAW_USER/enable-openwebui.sh"
cat > "/home/$CLAW_USER/enable-openwebui.sh" <<'HELPER'
#!/usr/bin/env bash
# enable-openwebui.sh
# Run this AFTER `openclaw onboard --install-daemon` has finished.
# - Enables OpenClaw's /v1 chatCompletions endpoint in openclaw.json
# - Restarts the gateway so the change takes effect
# - Starts the Open WebUI container bound to 127.0.0.1:8080
# Re-running is safe (idempotent).
set -euo pipefail
CFG="$HOME/.openclaw/openclaw.json"
if [[ ! -f "$CFG" ]]; then
echo "error: $CFG not found. run 'openclaw onboard --install-daemon' first." >&2
exit 1
fi
# 1. Patch openclaw.json to enable chatCompletions (idempotent)
echo "==> Enabling OpenAI-compatible /v1 endpoint in $CFG"
cp "$CFG" "$CFG.bak.$(date +%s)"
tmp="$(mktemp)"
jq '
.gateway //= {}
| .gateway.http //= {}
| .gateway.http.endpoints //= {}
| .gateway.http.endpoints.chatCompletions //= {}
| .gateway.http.endpoints.chatCompletions.enabled = true
' "$CFG" > "$tmp" && mv "$tmp" "$CFG"
# 2. Restart gateway to pick up the change
echo "==> Restarting openclaw-gateway.service"
systemctl --user restart openclaw-gateway.service
sleep 2
# 3. Start (or restart) the Open WebUI container, bound to loopback only
echo "==> Starting Open WebUI container on 127.0.0.1:8080"
if docker ps -a --format '{{.Names}}' | grep -q '^open-webui$'; then
docker restart open-webui >/dev/null
else
docker run -d \
--name open-webui \
--restart unless-stopped \
-p 127.0.0.1:8080:8080 \
--add-host=host.docker.internal:host-gateway \
-v open-webui:/app/backend/data \
ghcr.io/open-webui/open-webui:main >/dev/null
fi
# 4. Print connection details
TOKEN="$(jq -r '.gateway.auth.token // empty' "$CFG")"
[[ -z "$TOKEN" ]] && TOKEN="(token not set — inspect $CFG manually)"
PUB_IP="$(hostname -I | awk '{print $1}')"
cat <<EOF
================================================================
Open WebUI is running on http://127.0.0.1:8080
================================================================
From your laptop, tunnel the port:
ssh -L 8080:localhost:8080 $(whoami)@${PUB_IP}
Then open http://localhost:8080 and create the admin account.
In Admin Settings -> Connections -> OpenAI -> Add Connection:
URL: http://host.docker.internal:18789/v1
API Key: ${TOKEN}
Pick 'openclaw/default' from the model dropdown and chat.
================================================================
EOF
HELPER
chmod +x "/home/$CLAW_USER/enable-openwebui.sh"
chown "$CLAW_USER:$CLAW_USER" "/home/$CLAW_USER/enable-openwebui.sh"
fi
# Write a self-diagnostic helper that tells the user exactly which step of
# the install + onboarding flow is currently broken. Unconditional — useful
# whether or not Open WebUI is installed.
log "Writing helper /home/$CLAW_USER/claw-status.sh"
cat > "/home/$CLAW_USER/claw-status.sh" <<'STATUS'
#!/usr/bin/env bash
# claw-status.sh — one-shot diagnostic of your OpenClaw setup.
# Tells you exactly which piece is missing or stopped.
set -uo pipefail
ok() { printf ' \033[1;32m\xE2\x9C\x93\033[0m %s\n' "$*"; }
bad() { printf ' \033[1;31m\xE2\x9C\x97\033[0m %s\n' "$*"; }
warn() { printf ' \033[1;33m!\033[0m %s\n' "$*"; }
hint() { printf ' \033[2m\xE2\x86\x92 %s\033[0m\n' "$*"; }
echo
echo "==> OpenClaw status on $(hostname)"
# 1. Onboarding done? (creates ~/.openclaw/openclaw.json)
CFG="$HOME/.openclaw/openclaw.json"
if [[ -f "$CFG" ]]; then
ok "Onboarding complete (config exists at $CFG)"
else
bad "Onboarding not done — no $CFG"
hint "tmux new -s claw"
hint "openclaw onboard --install-daemon"
echo
echo "Onboarding is interactive (asks for model provider, API key, channels)"
echo "and is the one step that can't be scripted. Run it once, then come back."
exit 0
fi
# 2. Gateway service installed and active?
if systemctl --user is-active --quiet openclaw-gateway.service; then
ok "openclaw-gateway.service is active"
elif systemctl --user list-unit-files openclaw-gateway.service >/dev/null 2>&1; then
bad "openclaw-gateway.service exists but is not running"
hint "systemctl --user start openclaw-gateway.service"
hint "journalctl --user -u openclaw-gateway.service -n 50"
else
bad "openclaw-gateway.service not installed"
hint "openclaw onboard --install-daemon (re-run with --install-daemon)"
fi
# 3. Gateway port?
if ss -lntp 2>/dev/null | grep -q ':18789 '; then
ok "Gateway listening on 127.0.0.1:18789"
else
bad "Nothing on 127.0.0.1:18789 — gateway not serving"
hint "journalctl --user -u openclaw-gateway.service -f"
fi
# 4. /v1 chatCompletions endpoint (needed for Open WebUI / OpenAI-compatible clients)
if jq -e '.gateway.http.endpoints.chatCompletions.enabled == true' "$CFG" >/dev/null 2>&1; then
ok "OpenAI-compatible /v1 endpoint enabled"
else
warn "/v1/chatCompletions disabled (Open WebUI cannot connect without this)"
hint "bash ~/enable-openwebui.sh"
fi
# 5. Open WebUI container
if command -v docker >/dev/null 2>&1; then
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^open-webui$'; then
ok "Open WebUI container running on 127.0.0.1:8080"
elif docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^open-webui$'; then
warn "Open WebUI container exists but stopped"
hint "docker start open-webui"
else
warn "Open WebUI container not created"
hint "bash ~/enable-openwebui.sh"
fi
fi
echo
PUB_IP="$(hostname -I | awk '{print $1}')"
echo "If everything above is green, from your laptop:"
echo " ssh -L 18789:localhost:18789 -L 8080:localhost:8080 $(whoami)@${PUB_IP}"
echo "Then open http://localhost:18789 (dashboard) or http://localhost:8080 (Open WebUI)"
echo
STATUS
chmod +x "/home/$CLAW_USER/claw-status.sh"
chown "$CLAW_USER:$CLAW_USER" "/home/$CLAW_USER/claw-status.sh"
# ---- 11. Firewall (gateway stays on loopback; only SSH is exposed) ----------
if [[ "$ENABLE_UFW" == "yes" ]]; then
log "Configuring UFW: allow SSH on ${SSH_PORT}/tcp, deny all other inbound"
ufw default deny incoming
ufw default allow outgoing
ufw allow "${SSH_PORT}/tcp" comment 'SSH'
ufw --force enable
ufw status verbose
fi
# ---- 12. SSH hardening -------------------------------------------------------
if [[ "$HARDEN_SSH" == "yes" ]]; then
log "Hardening sshd: disabling root login + password auth"
install -d -m 755 /etc/ssh/sshd_config.d
cat > /etc/ssh/sshd_config.d/99-openclaw.conf <<EOF
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
EOF
# /run/sshd is sshd's privilege-separation directory. On Ubuntu 24.04+ with
# socket-activated ssh.socket it may not exist until a connection lands,
# which makes `sshd -t` fail. Create it via three redundant paths so this
# never breaks again.
systemd-tmpfiles --create /usr/lib/tmpfiles.d/sshd.conf 2>/dev/null || true
systemctl start ssh.service 2>/dev/null || systemctl start sshd.service 2>/dev/null || true
mkdir -p /run/sshd
chmod 0755 /run/sshd
chown root:root /run/sshd
if [[ ! -d /run/sshd ]]; then
echo "error: /run/sshd could not be created — sshd -t will fail" >&2
exit 1
fi
# Validate config before reloading; if this fails, abort without touching ssh.
sshd -t
systemctl reload ssh 2>/dev/null || systemctl reload sshd 2>/dev/null || true
fi
# ---- Done --------------------------------------------------------------------
PUB_IP="$(curl -fsSL --max-time 3 https://ipv4.icanhazip.com 2>/dev/null \
|| hostname -I | awk '{print $1}')"
OPENWEBUI_STEPS=""
if [[ "$INSTALL_OPENWEBUI" == "yes" ]]; then
OPENWEBUI_STEPS="
6. Enable Open WebUI (ChatGPT-style frontend on http://localhost:8080):
bash ~/enable-openwebui.sh
Then tunnel port 8080 from your laptop:
ssh -L 8080:localhost:8080 ${CLAW_USER}@${PUB_IP}
Open http://localhost:8080 and create your admin account.
The helper prints the OpenAI connection URL + token to paste in.
"
fi
cat <<DONE
================================================================
OpenClaw bootstrap complete on $(hostname)
================================================================
Public IP : ${PUB_IP}
User : ${CLAW_USER}
Node : $(node --version)
OpenClaw : $(sudo -u "$CLAW_USER" bash -c 'PATH="$HOME/.npm-global/bin:$PATH" openclaw --version 2>/dev/null || echo "(installed)"')
Next steps — from your laptop:
1. SSH in as the new user:
ssh ${CLAW_USER}@${PUB_IP}
2. Start tmux (so onboarding survives a flaky SSH connection):
tmux new -s claw
3. Run the interactive onboarder (this is the one step the script can't
automate — it prompts for your model API key, channel choices, etc.):
openclaw onboard --install-daemon
4. Verify everything is healthy in one shot:
bash ~/claw-status.sh
5. Reach the dashboard from your laptop (gateway stays on loopback):
ssh -L 18789:localhost:18789 ${CLAW_USER}@${PUB_IP}
Then open http://localhost:18789 in your browser.
${OPENWEBUI_STEPS}
Backups: snapshot /home/${CLAW_USER}/.openclaw regularly — that
directory holds your SOUL.md files, session JSONL transcripts,
configs and secrets. The VPS itself is disposable; that directory
isn't.
================================================================
DONE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment