Last active
May 18, 2026 10:35
-
-
Save grafuls/833d2bd8e5ec2909caf152c45b46e932 to your computer and use it in GitHub Desktop.
Openclaw setup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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