Fix critical bugs and improve reliability, compatibility, and usability
- Fix Homebrew installation: install sudo, set sudoers permissions, and add transparent root wrapper - Add Debian 12 fallback when Debian 13 template is unavailable - Ensure proper OpenClaw token authentication configuration - Dynamically resolve binary paths in systemd services - Configure SSH root password login - Clean stale VNC locks on startup and prevent screen-blanking - Add --skip-daemon flag to onboarding shortcut to avoid service conflict - Enable nesting and onboot autostart for container - Prompt for VMID and storage pools with auto-detected defaults
This commit is contained in:
25
CLAUDE.md
25
CLAUDE.md
@@ -8,28 +8,27 @@ OpenClaw on Proxmox is a single-script deployment tool (`setup-openclaw-lxc.sh`)
|
||||
|
||||
## Architecture
|
||||
|
||||
The entire project is one ~460-line Bash script with this flow:
|
||||
The entire project is one Bash script with this flow:
|
||||
|
||||
1. **Pre-flight** — Verify Proxmox CLI tools (`pct`, `pvesh`, `pveam`) and root access
|
||||
2. **User prompts** — Collect container password, disk/memory/CPU/resolution settings
|
||||
3. **Auto-detection** — Find next VMID and storage backends via `pvesh` JSON + Python parsing
|
||||
4. **Template management** — Download Debian 13 template if missing
|
||||
5. **Container creation** — Privileged LXC, DHCP networking
|
||||
6. **Package installation** — Node.js 22, Homebrew (as non-root `brewuser`), OpenClaw (npm global), LXQt, TigerVNC, noVNC, Google Chrome, OpenClaw browser extension
|
||||
7. **Configuration** — OpenClaw gateway settings, VNC password/xstartup, noVNC auto-scaling patch, Chrome as default browser
|
||||
8. **Desktop integration** — Desktop shortcuts for onboarding wizard and dashboard
|
||||
9. **Systemd services** — Three auto-starting services: `openclaw-gateway` (port 18789), `vncserver` (:1/5901), `novnc` (port 6080)
|
||||
10. **Verification** — Check all services are active, print connection info
|
||||
1. **Pre-flight** — Verify Proxmox CLI tools (`pct`, `pvesh`, `pveam`), root access, and x86_64 architecture
|
||||
2. **Auto-detection & User prompts** — Detect next VMID and available storages, prompt user with smart defaults
|
||||
3. **Template management** — Check local storage, download Debian 13 or fallback to Debian 12 template
|
||||
4. **Container creation** — Privileged LXC with nesting enabled, DHCP networking, onboot autostart
|
||||
5. **Package installation** — Node.js 22, Homebrew (as non-root `brewuser`), OpenClaw (npm global), LXQt, TigerVNC, noVNC, Google Chrome, OpenClaw browser extension, OpenSSH server
|
||||
6. **Configuration** — OpenClaw gateway settings (`gateway.auth.mode token`), VNC password/xstartup (no blanking), noVNC auto-scaling patch, Chrome as default browser (`--no-sandbox` wrapper), SSH root login
|
||||
7. **Desktop integration** — Desktop shortcuts for onboarding wizard (`--skip-daemon`) and dashboard (pre-filled token)
|
||||
8. **Systemd services** — Three auto-starting services with dynamically detected binaries: `openclaw-gateway` (port 18789), `vncserver` (:1/5901), `novnc` (port 6080)
|
||||
9. **Verification** — Check all services are active, print connection info (direct one-click URLs)
|
||||
|
||||
Key helper: `ct_exec()` wraps `pct exec $VMID -- bash -c` for all in-container commands.
|
||||
|
||||
## Development Notes
|
||||
|
||||
- No build system, test suite, or linter — this is a standalone Bash script
|
||||
- Uses `set -euo pipefail` for strict error handling
|
||||
- Uses `set -euo pipefail` and ERR trap for strict error handling and cleanup tips
|
||||
- All in-container operations use heredoc-embedded config files (systemd units, desktop files, wrapper scripts)
|
||||
- Colorized output via `info()`, `ok()`, `warn()`, `err()`, `fatal()` helper functions
|
||||
- Homebrew is installed under a dedicated `brewuser` account (non-root requirement) with a symlink at `/usr/local/bin/brew` for root access
|
||||
- Homebrew is installed under a dedicated `brewuser` account (non-root requirement) with passwordless sudo (`0440` permission) and a transparent wrapper script at `/usr/local/bin/brew` so root and OpenClaw can run `brew` commands
|
||||
- The noVNC auto-scaling patch modifies `/usr/share/novnc/app/ui.js` in-place via `sed`
|
||||
- Auth token is generated with `openssl rand -hex 16`
|
||||
|
||||
|
||||
32
README.md
32
README.md
@@ -47,10 +47,10 @@ After the script finishes, you'll have a fully configured LXC container with:
|
||||
| What | How to access |
|
||||
|------|---------------|
|
||||
| **OpenClaw Dashboard** | `http://<container-ip>:18789/#token=<your-token>` |
|
||||
| **Remote Desktop** | `http://<container-ip>:6080/vnc.html` |
|
||||
| **Remote Desktop** | `http://<container-ip>:6080/vnc.html?autoconnect=true&resize=scale` |
|
||||
| **SSH** | `ssh root@<container-ip>` |
|
||||
|
||||
The script prints all of this (including the token) at the end.
|
||||
The script prints all of this (including the direct token link) at the end.
|
||||
|
||||
## Desktop Shortcuts
|
||||
|
||||
@@ -58,38 +58,41 @@ Once you open the remote desktop via noVNC, you'll find two shortcuts ready to g
|
||||
|
||||
| Shortcut | What it does |
|
||||
|----------|-------------|
|
||||
| **OpenClaw Setup Wizard** | Opens the interactive onboarding wizard (`openclaw onboard`) to configure your channels, workspace, and skills |
|
||||
| **OpenClaw Setup Wizard** | Opens the interactive onboarding wizard (`openclaw onboard --skip-daemon`) to configure your channels, workspace, and skills |
|
||||
| **OpenClaw Dashboard** | Opens Chrome with the dashboard URL and auth token pre-filled — no manual token entry needed |
|
||||
|
||||
## Requirements
|
||||
|
||||
- Proxmox VE 8.x+
|
||||
- Proxmox VE 8.x+ (x86_64 / amd64)
|
||||
- Root access on the Proxmox host
|
||||
- Internet connectivity (for downloading packages)
|
||||
|
||||
## Configuration
|
||||
|
||||
The script prompts you interactively (all optional except password):
|
||||
The script prompts you interactively (all optional with smart auto-detection except password):
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| Password | *(required)* | Container root password (also used for VNC) |
|
||||
| VMID | *(next free)* | Container ID (auto-detected, customizable) |
|
||||
| Password | *(required)* | Container root password (VNC uses first 8 characters) |
|
||||
| Rootfs storage | *(auto-detected)* | Storage pool for container rootfs (`local-lvm`, `local-zfs`, etc.) |
|
||||
| Template storage | *(auto-detected)* | Storage pool for template cache (`local`, etc.) |
|
||||
| Disk size | 32 GB | Container root filesystem size |
|
||||
| Memory | 4096 MB | RAM allocation |
|
||||
| CPU cores | 4 | Number of CPU cores |
|
||||
| VNC resolution | 1920x1080 | Remote desktop resolution |
|
||||
|
||||
Everything else is auto-detected — VMID, storage, networking (DHCP).
|
||||
Everything else is auto-detected — networking (DHCP bridge `vmbr0`), nesting, and onboot autostart.
|
||||
|
||||
## What the script installs
|
||||
|
||||
All inside the container (nothing is installed on the Proxmox host):
|
||||
|
||||
- **Debian 13** LXC (privileged)
|
||||
- **Debian 13 or 12** LXC (privileged, nested, onboot)
|
||||
- **Node.js 22** + **OpenClaw**
|
||||
- **Homebrew** (for OpenClaw skills)
|
||||
- **LXQt** desktop + **TigerVNC** + **noVNC**
|
||||
- **Google Chrome** (with OpenClaw browser extension)
|
||||
- **Homebrew** (preconfigured with non-root user + transparent root wrapper for OpenClaw skills)
|
||||
- **LXQt** desktop + **TigerVNC** + **noVNC** (with auto-scaling & no screen-blanking)
|
||||
- **Google Chrome** (configured for root with `--no-sandbox` & OpenClaw browser extension)
|
||||
- **Noto Color Emoji** font (for proper terminal rendering)
|
||||
- Three **systemd services**: `openclaw-gateway`, `vncserver`, `novnc`
|
||||
|
||||
@@ -104,11 +107,10 @@ pct status <VMID> # Check container status
|
||||
|
||||
## Notes on the onboarding wizard
|
||||
|
||||
When you run the **OpenClaw Setup Wizard** from the desktop shortcut, you may see two messages that look like errors but are perfectly normal:
|
||||
When you run the **OpenClaw Setup Wizard** from the desktop shortcut:
|
||||
|
||||
**"Gateway service install failed"** — The wizard tries to create its own systemd service for the gateway, but our script already set up `openclaw-gateway.service` and it's already running. This is safe to ignore.
|
||||
|
||||
**"SECURITY ERROR: ws:// to a non-loopback address"** — This is a warning that the gateway uses unencrypted WebSocket over LAN. This is expected for a local setup. If you're on a trusted home or office network, it's completely fine.
|
||||
- The shortcut automatically includes `--skip-daemon` so the wizard won't attempt to reinstall or overwrite the existing systemd service.
|
||||
- **"SECURITY ERROR: ws:// to a non-loopback address"** — This is a standard warning that the gateway uses unencrypted WebSocket over LAN. This is expected for a local setup. If you're on a trusted home or office network, it's completely fine.
|
||||
|
||||
As long as you see your **channels connected**, a **Control UI URL**, and an **agent workspace** listed at the end of the wizard, everything worked.
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
#
|
||||
# setup-openclaw-lxc.sh — Automated OpenClaw AI assistant setup in a Proxmox LXC container
|
||||
#
|
||||
# Run this script directly on the Proxmox host.
|
||||
# It creates a Debian 13 LXC, installs OpenClaw + LXQt desktop + Google Chrome + VNC/noVNC,
|
||||
# Run this script directly on the Proxmox host as root.
|
||||
# It creates a Debian 13/12 LXC, installs OpenClaw + LXQt desktop + Google Chrome + VNC/noVNC,
|
||||
# and prints connection info when done.
|
||||
#
|
||||
|
||||
@@ -23,11 +23,63 @@ warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
err() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
fatal() { err "$@"; exit 1; }
|
||||
|
||||
# Trap unexpected errors to give actionable advice
|
||||
trap 'err "An error occurred on line $LINENO. Container VMID: ${VMID:-none}. To clean up: pct destroy ${VMID:-<vmid>}"' ERR
|
||||
|
||||
# ─── Pre-flight checks ───────────────────────────────────────────────────────
|
||||
command -v pct >/dev/null 2>&1 || fatal "pct not found — this script must run on a Proxmox host."
|
||||
command -v pvesh >/dev/null 2>&1 || fatal "pvesh not found — this script must run on a Proxmox host."
|
||||
command -v pveam >/dev/null 2>&1 || fatal "pveam not found — this script must run on a Proxmox host."
|
||||
[[ $(id -u) -eq 0 ]] || fatal "This script must be run as root."
|
||||
[[ $(uname -m) == "x86_64" ]] || fatal "This script currently supports x86_64 (amd64) architecture only."
|
||||
|
||||
# ─── Auto-detection ──────────────────────────────────────────────────────────
|
||||
info "Detecting next available VMID and storage pools..."
|
||||
|
||||
# Detect next available VMID
|
||||
DETECTED_VMID=$(pvesh get /cluster/nextid 2>/dev/null || true)
|
||||
if [[ -z "$DETECTED_VMID" || ! "$DETECTED_VMID" =~ ^[0-9]+$ ]]; then
|
||||
DETECTED_VMID=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
ids = [r["vmid"] for r in data if "vmid" in r]
|
||||
print(max(ids) + 1 if ids else 100)
|
||||
' 2>/dev/null || echo 100)
|
||||
fi
|
||||
|
||||
# Find template storage (enabled and supports vztmpl content)
|
||||
DETECTED_TMPL_STORAGE=$(pvesh get /storage --output-format json 2>/dev/null \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
stores = json.load(sys.stdin)
|
||||
for s in stores:
|
||||
if s.get("disable", 0) != 1 and s.get("enabled", 1) == 1:
|
||||
if "vztmpl" in s.get("content", ""):
|
||||
print(s["storage"])
|
||||
break
|
||||
' 2>/dev/null || echo "local")
|
||||
|
||||
# Find rootdir storage (prefer local-lvm, local-zfs, local-btrfs, then any with rootdir/images)
|
||||
DETECTED_ROOT_STORAGE=$(pvesh get /storage --output-format json 2>/dev/null \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
stores = json.load(sys.stdin)
|
||||
candidates = []
|
||||
for s in stores:
|
||||
if s.get("disable", 0) != 1 and s.get("enabled", 1) == 1:
|
||||
content = s.get("content", "")
|
||||
if "rootdir" in content or "images" in content:
|
||||
candidates.append(s["storage"])
|
||||
for preferred in ["local-lvm", "local-zfs", "local-btrfs"]:
|
||||
if preferred in candidates:
|
||||
print(preferred)
|
||||
sys.exit(0)
|
||||
if candidates:
|
||||
print(candidates[0])
|
||||
else:
|
||||
print("local-lvm")
|
||||
' 2>/dev/null || echo "local-lvm")
|
||||
|
||||
# ─── User prompts ────────────────────────────────────────────────────────────
|
||||
echo -e "${BOLD}╔═══════════════════════════════════════════════════╗${NC}"
|
||||
@@ -35,10 +87,24 @@ echo -e "${BOLD}║ OpenClaw LXC Setup for Proxmox ║${NC}"
|
||||
echo -e "${BOLD}╚═══════════════════════════════════════════════════╝${NC}"
|
||||
echo
|
||||
|
||||
read -rp "Container VMID [${DETECTED_VMID}]: " VMID_INPUT
|
||||
VMID="${VMID_INPUT:-$DETECTED_VMID}"
|
||||
[[ "$VMID" =~ ^[0-9]+$ ]] || fatal "VMID must be an integer."
|
||||
|
||||
if pct status "$VMID" >/dev/null 2>&1 || qm status "$VMID" >/dev/null 2>&1; then
|
||||
fatal "VMID $VMID is already in use by another container or VM!"
|
||||
fi
|
||||
|
||||
read -rp "Container password: " -s CT_PASSWORD
|
||||
echo
|
||||
[[ -n "$CT_PASSWORD" ]] || fatal "Password cannot be empty."
|
||||
|
||||
read -rp "Rootfs storage [${DETECTED_ROOT_STORAGE}]: " ROOT_STORAGE_INPUT
|
||||
ROOT_STORAGE="${ROOT_STORAGE_INPUT:-$DETECTED_ROOT_STORAGE}"
|
||||
|
||||
read -rp "Template storage [${DETECTED_TMPL_STORAGE}]: " TMPL_STORAGE_INPUT
|
||||
TMPL_STORAGE="${TMPL_STORAGE_INPUT:-$DETECTED_TMPL_STORAGE}"
|
||||
|
||||
read -rp "Disk size in GB [32]: " DISK_SIZE
|
||||
DISK_SIZE="${DISK_SIZE:-32}"
|
||||
|
||||
@@ -52,70 +118,31 @@ read -rp "VNC resolution [1920x1080]: " VNC_RES
|
||||
VNC_RES="${VNC_RES:-1920x1080}"
|
||||
|
||||
echo
|
||||
ok "Will create container $VMID on storage $ROOT_STORAGE (template on $TMPL_STORAGE)"
|
||||
|
||||
# ─── Auto-detect next VMID ───────────────────────────────────────────────────
|
||||
info "Detecting next available VMID..."
|
||||
VMID=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
ids = [r["vmid"] for r in data if "vmid" in r]
|
||||
print(max(ids) + 1 if ids else 100)
|
||||
' 2>/dev/null || echo 100)
|
||||
ok "Will use VMID: $VMID"
|
||||
|
||||
# ─── Auto-detect storage ─────────────────────────────────────────────────────
|
||||
info "Detecting storage..."
|
||||
|
||||
# Find template storage (supports vztmpl content)
|
||||
TMPL_STORAGE=$(pvesh get /storage --output-format json 2>/dev/null \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
stores = json.load(sys.stdin)
|
||||
for s in stores:
|
||||
content = s.get("content", "")
|
||||
if "vztmpl" in content:
|
||||
print(s["storage"])
|
||||
break
|
||||
' 2>/dev/null || echo "local")
|
||||
|
||||
# Find rootdir storage (prefer local-lvm, then any with rootdir)
|
||||
ROOT_STORAGE=$(pvesh get /storage --output-format json 2>/dev/null \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
stores = json.load(sys.stdin)
|
||||
candidates = []
|
||||
for s in stores:
|
||||
content = s.get("content", "")
|
||||
if "rootdir" in content or "images" in content:
|
||||
candidates.append(s["storage"])
|
||||
if "local-lvm" in candidates:
|
||||
print("local-lvm")
|
||||
elif candidates:
|
||||
print(candidates[0])
|
||||
else:
|
||||
print("local-lvm")
|
||||
' 2>/dev/null || echo "local-lvm")
|
||||
|
||||
ok "Template storage: $TMPL_STORAGE"
|
||||
ok "Rootfs storage: $ROOT_STORAGE"
|
||||
|
||||
# ─── Download Debian 13 template ─────────────────────────────────────────────
|
||||
info "Checking for Debian 13 template..."
|
||||
# ─── Download Debian template (Debian 13 or 12) ───────────────────────────────
|
||||
info "Checking for Debian template..."
|
||||
pveam update >/dev/null 2>&1 || true
|
||||
|
||||
TEMPLATE=$(pveam available --section system 2>/dev/null | grep -oP 'debian-13-standard_\S+' | head -1 || true)
|
||||
if [[ -z "$TEMPLATE" ]]; then
|
||||
TEMPLATE=$(pveam available 2>/dev/null | grep -oP 'debian-13-standard_\S+' | head -1 || true)
|
||||
fi
|
||||
[[ -n "$TEMPLATE" ]] || fatal "Could not find Debian 13 template. Check 'pveam available'."
|
||||
# Check if an existing Debian 13 or 12 template is already downloaded on storage
|
||||
TEMPLATE=$(pveam list "$TMPL_STORAGE" 2>/dev/null | grep -oP 'debian-(13|12)-standard_\S+' | head -1 || true)
|
||||
|
||||
if pveam list "$TMPL_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
|
||||
ok "Template already downloaded: $TEMPLATE"
|
||||
if [[ -n "$TEMPLATE" ]]; then
|
||||
ok "Found locally downloaded template: $TEMPLATE"
|
||||
else
|
||||
# Search available templates, prioritizing Debian 13 then Debian 12
|
||||
TEMPLATE=$(pveam available --section system 2>/dev/null | grep -oP 'debian-13-standard_\S+' | head -1 || true)
|
||||
if [[ -z "$TEMPLATE" ]]; then
|
||||
TEMPLATE=$(pveam available --section system 2>/dev/null | grep -oP 'debian-12-standard_\S+' | head -1 || true)
|
||||
fi
|
||||
if [[ -z "$TEMPLATE" ]]; then
|
||||
TEMPLATE=$(pveam available 2>/dev/null | grep -oP 'debian-(13|12)-standard_\S+' | head -1 || true)
|
||||
fi
|
||||
[[ -n "$TEMPLATE" ]] || fatal "Could not find Debian 13 or Debian 12 template in 'pveam available'."
|
||||
|
||||
info "Downloading $TEMPLATE to $TMPL_STORAGE..."
|
||||
pveam download "$TMPL_STORAGE" "$TEMPLATE"
|
||||
ok "Template downloaded."
|
||||
ok "Template downloaded: $TEMPLATE"
|
||||
fi
|
||||
|
||||
# ─── Create the LXC container ────────────────────────────────────────────────
|
||||
@@ -128,6 +155,8 @@ pct create "$VMID" "${TMPL_STORAGE}:vztmpl/${TEMPLATE}" \
|
||||
--cores "$CORES" \
|
||||
--net0 name=eth0,bridge=vmbr0,ip=dhcp \
|
||||
--unprivileged 0 \
|
||||
--features nesting=1 \
|
||||
--onboot 1 \
|
||||
--start 0
|
||||
ok "Container $VMID created."
|
||||
|
||||
@@ -147,16 +176,23 @@ ct_exec() {
|
||||
info "Waiting for DHCP lease (up to 30s)..."
|
||||
CT_IP=""
|
||||
for i in $(seq 1 30); do
|
||||
CT_IP=$(ct_exec "hostname -I 2>/dev/null" | awk '{print $1}' || true)
|
||||
if [[ -n "$CT_IP" && "$CT_IP" != "127.0.0.1" ]]; then
|
||||
CT_IP=$(ct_exec "hostname -I 2>/dev/null" | grep -oP '\b(?:\d{1,3}\.){3}\d{1,3}\b' | grep -v '^127\.' | head -n1 || true)
|
||||
if [[ -n "$CT_IP" ]]; then
|
||||
break
|
||||
fi
|
||||
CT_IP=""
|
||||
sleep 1
|
||||
done
|
||||
[[ -n "$CT_IP" ]] || warn "Could not detect container IP — continuing anyway."
|
||||
[[ -n "$CT_IP" ]] || warn "Could not detect container IPv4 — continuing anyway."
|
||||
ok "Container IP: ${CT_IP:-unknown}"
|
||||
|
||||
info "Verifying internet connectivity inside container..."
|
||||
for i in $(seq 1 15); do
|
||||
if ct_exec "curl -s --connect-timeout 2 https://deb.debian.org >/dev/null 2>&1 || ping -c 1 -W 2 1.1.1.1 >/dev/null 2>&1"; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# ─── Install packages inside the container ────────────────────────────────────
|
||||
info "Updating packages and installing prerequisites..."
|
||||
ct_exec "
|
||||
@@ -170,10 +206,19 @@ ct_exec "
|
||||
export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
|
||||
|
||||
apt-get upgrade -y 2>&1 | grep -v 'Failed to write'
|
||||
apt-get install -y curl ca-certificates gnupg git 2>&1 | grep -v 'Failed to write'
|
||||
apt-get install -y curl ca-certificates gnupg git sudo openssh-server 2>&1 | grep -v 'Failed to write'
|
||||
"
|
||||
ok "Prerequisites installed."
|
||||
|
||||
# ─── Configure SSH for root access ───────────────────────────────────────────
|
||||
info "Configuring SSH for root password login..."
|
||||
ct_exec "
|
||||
mkdir -p /etc/ssh/sshd_config.d
|
||||
echo 'PermitRootLogin yes' > /etc/ssh/sshd_config.d/permit-root-login.conf
|
||||
systemctl restart ssh 2>/dev/null || systemctl restart sshd 2>/dev/null || true
|
||||
"
|
||||
ok "SSH configured."
|
||||
|
||||
info "Installing Node.js 22..."
|
||||
ct_exec "
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
@@ -185,18 +230,38 @@ ok "Node.js installed."
|
||||
info "Installing Homebrew (required for OpenClaw skills)..."
|
||||
ct_exec "
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get install -y build-essential procps file 2>&1 | grep -v -E 'Failed to write|Permission denied'
|
||||
apt-get install -y build-essential procps file sudo 2>&1 | grep -v -E 'Failed to write|Permission denied'
|
||||
|
||||
# Homebrew must be installed as a non-root user
|
||||
# Create dedicated brewuser and configure passwordless sudo
|
||||
useradd -m -s /bin/bash brewuser 2>/dev/null || true
|
||||
mkdir -p /etc/sudoers.d
|
||||
echo 'brewuser ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/brewuser
|
||||
chmod 0440 /etc/sudoers.d/brewuser
|
||||
|
||||
# Pre-create /home/linuxbrew with correct ownership
|
||||
mkdir -p /home/linuxbrew/.linuxbrew
|
||||
chown -R brewuser:brewuser /home/linuxbrew
|
||||
|
||||
# Install Homebrew as brewuser
|
||||
su - brewuser -c 'NONINTERACTIVE=1 /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"' 2>&1 | tail -5
|
||||
|
||||
# Make brew available system-wide for root
|
||||
# Configure brew environment for brewuser
|
||||
echo 'eval \"\$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)\"' >> /home/brewuser/.bashrc
|
||||
echo 'eval \"\$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)\"' >> /home/brewuser/.profile
|
||||
|
||||
# Configure brew environment for root
|
||||
echo '[ -x /home/linuxbrew/.linuxbrew/bin/brew ] && eval \"\$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)\"' >> /root/.bashrc
|
||||
ln -sf /home/linuxbrew/.linuxbrew/bin/brew /usr/local/bin/brew
|
||||
|
||||
# Safe wrapper so root and OpenClaw can seamlessly execute brew without root errors
|
||||
cat > /usr/local/bin/brew << 'BREWWRAPPER'
|
||||
#!/bin/bash
|
||||
if [[ \$(id -u) -eq 0 ]]; then
|
||||
exec sudo -u brewuser -i /home/linuxbrew/.linuxbrew/bin/brew \"\$@\"
|
||||
else
|
||||
exec /home/linuxbrew/.linuxbrew/bin/brew \"\$@\"
|
||||
fi
|
||||
BREWWRAPPER
|
||||
chmod +x /usr/local/bin/brew
|
||||
"
|
||||
ok "Homebrew installed."
|
||||
|
||||
@@ -208,7 +273,7 @@ info "Installing LXQt, TigerVNC, noVNC (this takes a few minutes)..."
|
||||
ct_exec "
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
|
||||
apt-get install -y lxqt openbox lxterminal tigervnc-standalone-server novnc websockify dbus-x11 fonts-noto-color-emoji 2>&1 \
|
||||
apt-get install -y lxqt openbox lxterminal tigervnc-standalone-server tigervnc-common novnc websockify dbus-x11 fonts-noto-color-emoji x11-xserver-utils 2>&1 \
|
||||
| grep -v -E 'Failed to write|Failed to send reload|Permission denied|Cannot set LC_'
|
||||
"
|
||||
ok "Desktop environment installed."
|
||||
@@ -230,29 +295,39 @@ ct_exec "
|
||||
openclaw config set gateway.mode local
|
||||
openclaw config set gateway.bind lan
|
||||
openclaw config set gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback true
|
||||
openclaw config set gateway.auth.mode token
|
||||
openclaw config set gateway.auth.token '${AUTH_TOKEN}'
|
||||
"
|
||||
ok "OpenClaw configured (token: $AUTH_TOKEN)."
|
||||
|
||||
info "Installing OpenClaw browser extension..."
|
||||
ct_exec "openclaw browser extension install 2>&1 | tail -5 || true"
|
||||
EXTENSION_DIR=$(ct_exec "openclaw browser extension path 2>/dev/null" || echo "/root/.openclaw/browser/extension")
|
||||
ok "OpenClaw browser extension installed."
|
||||
|
||||
# ─── Configure VNC ───────────────────────────────────────────────────────────
|
||||
info "Configuring VNC..."
|
||||
ct_exec "
|
||||
mkdir -p /root/.config/tigervnc
|
||||
echo '${CT_PASSWORD}' | vncpasswd -f > /root/.config/tigervnc/passwd
|
||||
|
||||
# Safely set VNC password via stdin to prevent shell escape issues with special characters
|
||||
printf '%s\n' "$CT_PASSWORD" | pct exec "$VMID" -- sh -c '
|
||||
mkdir -p /root/.config/tigervnc /root/.vnc /etc/tigervnc
|
||||
vncpasswd -f > /root/.config/tigervnc/passwd
|
||||
chmod 600 /root/.config/tigervnc/passwd
|
||||
"
|
||||
ln -sf /root/.config/tigervnc/passwd /root/.vnc/passwd
|
||||
echo ":1=root" > /etc/tigervnc/vncserver.users
|
||||
'
|
||||
|
||||
ct_exec "cat > /root/.config/tigervnc/xstartup << 'XSTARTUP'
|
||||
#!/bin/bash
|
||||
unset SESSION_MANAGER
|
||||
unset DBUS_SESSION_BUS_ADDRESS
|
||||
xset s off
|
||||
xset s noblank
|
||||
xset -dpms
|
||||
exec dbus-launch --exit-with-session startlxqt
|
||||
XSTARTUP
|
||||
chmod +x /root/.config/tigervnc/xstartup"
|
||||
chmod +x /root/.config/tigervnc/xstartup
|
||||
ln -sf /root/.config/tigervnc/xstartup /root/.vnc/xstartup"
|
||||
|
||||
# Configure lxterminal with dark theme
|
||||
ct_exec "
|
||||
@@ -323,9 +398,11 @@ ct_exec "
|
||||
exec /usr/bin/google-chrome-stable --no-sandbox \"\$@\"
|
||||
WRAPPER
|
||||
chmod +x /usr/local/bin/google-chrome
|
||||
ln -sf /usr/local/bin/google-chrome /usr/local/bin/google-chrome-stable
|
||||
|
||||
update-alternatives --set x-www-browser /usr/bin/google-chrome-stable 2>/dev/null || true
|
||||
update-alternatives --set gnome-www-browser /usr/bin/google-chrome-stable 2>/dev/null || true
|
||||
update-alternatives --install /usr/bin/x-www-browser x-www-browser /usr/local/bin/google-chrome 200 2>/dev/null || true
|
||||
update-alternatives --set x-www-browser /usr/local/bin/google-chrome 2>/dev/null || true
|
||||
update-alternatives --set gnome-www-browser /usr/local/bin/google-chrome 2>/dev/null || true
|
||||
xdg-mime default google-chrome.desktop x-scheme-handler/http 2>/dev/null || true
|
||||
xdg-mime default google-chrome.desktop x-scheme-handler/https 2>/dev/null || true
|
||||
xdg-mime default google-chrome.desktop text/html 2>/dev/null || true
|
||||
@@ -367,14 +444,14 @@ StartupNotify=true
|
||||
SHORTCUT
|
||||
chmod +x /root/Desktop/terminal.desktop"
|
||||
|
||||
# OpenClaw Onboarding wizard (runs in terminal)
|
||||
# OpenClaw Onboarding wizard (runs in terminal with --skip-daemon)
|
||||
ct_exec "cat > /root/Desktop/openclaw-onboard.desktop << 'SHORTCUT'
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=OpenClaw Setup Wizard
|
||||
Comment=Run the OpenClaw onboarding wizard to configure your AI assistant
|
||||
Exec=lxterminal --title='OpenClaw Onboarding' -e sh -c 'openclaw onboard; exec bash'
|
||||
Exec=lxterminal --title='OpenClaw Onboarding' -e sh -c 'openclaw onboard --skip-daemon; exec bash'
|
||||
Icon=utilities-terminal
|
||||
Terminal=false
|
||||
Categories=Utility;
|
||||
@@ -382,7 +459,7 @@ StartupNotify=true
|
||||
SHORTCUT
|
||||
chmod +x /root/Desktop/openclaw-onboard.desktop"
|
||||
|
||||
# OpenClaw Dashboard (opens in Chrome with token)
|
||||
# OpenClaw Dashboard (opens in Chrome with token pre-filled)
|
||||
ct_exec "cat > /root/Desktop/openclaw-dashboard.desktop << SHORTCUT
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
@@ -402,17 +479,24 @@ ok "Desktop shortcuts created."
|
||||
# ─── Create systemd services ─────────────────────────────────────────────────
|
||||
info "Creating systemd services..."
|
||||
|
||||
ct_exec "cat > /etc/systemd/system/openclaw-gateway.service << 'SVC'
|
||||
OPENCLAW_BIN=$(ct_exec "command -v openclaw" || echo "/usr/bin/openclaw")
|
||||
WEBSOCKIFY_BIN=$(ct_exec "command -v websockify" || echo "/usr/bin/websockify")
|
||||
VNC_BIN=$(ct_exec "command -v vncserver" || echo "/usr/bin/vncserver")
|
||||
|
||||
ct_exec "cat > /etc/systemd/system/openclaw-gateway.service << SVC
|
||||
[Unit]
|
||||
Description=OpenClaw Gateway
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/bin/openclaw gateway run --bind lan --auth token
|
||||
ExecStart=${OPENCLAW_BIN} gateway run --bind lan --auth token --token ${AUTH_TOKEN}
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=HOME=/root
|
||||
Environment=NODE_ENV=production
|
||||
Environment=OPENCLAW_GATEWAY_TOKEN=${AUTH_TOKEN}
|
||||
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
WorkingDirectory=/root
|
||||
|
||||
[Install]
|
||||
@@ -427,9 +511,9 @@ After=network.target
|
||||
[Service]
|
||||
Type=forking
|
||||
Environment=HOME=/root
|
||||
ExecStartPre=/bin/sh -c \"/usr/bin/vncserver -kill :1 > /dev/null 2>&1 || :\"
|
||||
ExecStart=/usr/bin/vncserver :1 -geometry ${VNC_RES} -depth 24 -localhost yes
|
||||
ExecStop=/usr/bin/vncserver -kill :1
|
||||
ExecStartPre=/bin/sh -c \"${VNC_BIN} -kill :1 >/dev/null 2>&1 || :; rm -f /tmp/.X1-lock /tmp/.X11-unix/X1; mkdir -p /tmp/.X11-unix; chmod 1777 /tmp/.X11-unix\"
|
||||
ExecStart=${VNC_BIN} :1 -geometry ${VNC_RES} -depth 24 -localhost yes
|
||||
ExecStop=${VNC_BIN} -kill :1
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
@@ -437,7 +521,7 @@ RestartSec=5
|
||||
WantedBy=multi-user.target
|
||||
SVC"
|
||||
|
||||
ct_exec "cat > /etc/systemd/system/novnc.service << 'SVC'
|
||||
ct_exec "cat > /etc/systemd/system/novnc.service << SVC
|
||||
[Unit]
|
||||
Description=noVNC WebSocket Proxy
|
||||
After=vncserver.service
|
||||
@@ -445,7 +529,7 @@ Requires=vncserver.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/bin/websockify --web=/usr/share/novnc/ 6080 localhost:5901
|
||||
ExecStart=${WEBSOCKIFY_BIN} --web=/usr/share/novnc/ 6080 localhost:5901
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
@@ -474,7 +558,7 @@ for svc in openclaw-gateway vncserver novnc; do
|
||||
done
|
||||
|
||||
# Re-detect IP in case it changed
|
||||
CT_IP=$(ct_exec "hostname -I 2>/dev/null" | awk '{print $1}' || echo "unknown")
|
||||
CT_IP=$(ct_exec "hostname -I 2>/dev/null" | grep -oP '\b(?:\d{1,3}\.){3}\d{1,3}\b' | grep -v '^127\.' | head -n1 || echo "unknown")
|
||||
|
||||
# ─── Print connection info ────────────────────────────────────────────────────
|
||||
echo
|
||||
@@ -485,17 +569,22 @@ echo
|
||||
echo -e " ${BOLD}Container ID:${NC} $VMID"
|
||||
echo -e " ${BOLD}Container IP:${NC} $CT_IP"
|
||||
echo
|
||||
echo -e " ${BOLD}OpenClaw Dashboard:${NC}"
|
||||
echo -e " http://${CT_IP}:18789/"
|
||||
echo -e " Auth token: ${CYAN}${AUTH_TOKEN}${NC}"
|
||||
echo -e " ${BOLD}OpenClaw Dashboard (Direct Link):${NC}"
|
||||
echo -e " ${CYAN}http://${CT_IP}:18789/#token=${AUTH_TOKEN}${NC}"
|
||||
echo -e " Auth token: ${AUTH_TOKEN}"
|
||||
echo
|
||||
echo -e " ${BOLD}noVNC (remote desktop):${NC}"
|
||||
echo -e " http://${CT_IP}:6080/vnc.html"
|
||||
echo -e " ${BOLD}noVNC Remote Desktop (Auto-connect):${NC}"
|
||||
echo -e " ${CYAN}http://${CT_IP}:6080/vnc.html?autoconnect=true&resize=scale${NC}"
|
||||
if [[ ${#CT_PASSWORD} -gt 8 ]]; then
|
||||
echo -e " ${YELLOW}Note on VNC password:${NC} VNC protocol uses first 8 characters: ${BOLD}${CT_PASSWORD:0:8}${NC}"
|
||||
else
|
||||
echo -e " VNC password: (same as container password)"
|
||||
fi
|
||||
echo
|
||||
echo -e " ${BOLD}Browser Extension:${NC}"
|
||||
echo -e " Directory: ${CYAN}${EXTENSION_DIR}${NC}"
|
||||
echo -e " Open Chrome → chrome://extensions → Enable Developer Mode"
|
||||
echo -e " Click 'Load unpacked' and select the OpenClaw extension directory"
|
||||
echo -e " Click 'Load unpacked' and select the directory above"
|
||||
echo
|
||||
echo -e " ${BOLD}SSH:${NC}"
|
||||
echo -e " ssh root@${CT_IP}"
|
||||
|
||||
Reference in New Issue
Block a user