commit 86204e08364f15ce119d7e4dbbd257afbbadd462 Author: OpenClaw Date: Sat Apr 11 00:16:39 2026 +0200 Automate local OpenClaw Traefik setup and add docs diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c74947 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# openclaw-deploy-scripts + +Deployment and backup scripts for services on this host. + +## Highlights + +- Docker and local deployment helpers under `/opt/deploy` +- Shared Traefik helpers in `common.sh` +- Local OpenClaw deployment now supports automatic Traefik wiring +- Companion how-to docs live under `how-to/` + +## Docs + +- `how-to/traefik-openclaw.md` +- `how-to/mattermost-openclaw.md` +- `how-to/gitea-openclaw-bootstrap.md` + +## Notes + +These scripts are evolving toward unattended rebuilds. diff --git a/deploy-local-openclaw.sh b/deploy-local-openclaw.sh new file mode 100755 index 0000000..84cc289 --- /dev/null +++ b/deploy-local-openclaw.sh @@ -0,0 +1,296 @@ +#!/usr/bin/env bash +# ============================================================================== +# OpenClaw Local (Bare-Metal) Deployment Script +# ============================================================================== +# Installs OpenClaw directly on the host via npm. +# Source: https://docs.openclaw.ai/install +# +# Usage: deploy-local-openclaw.sh +# deploy-local-openclaw.sh --domain oc.an2.io +# deploy-local-openclaw.sh --remove [--purge] [--yes] +# ============================================================================== + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common.sh" + +# --- Parse arguments --- +parse_args "$@" + +# --- Service config --- +SERVICE_NAME="openclaw" +BASE_DIR="/opt/${SERVICE_NAME}" +UNIT_NAME="${SERVICE_NAME}" + +# --- Handle --remove --- +if [[ "$ARG_REMOVE" == "1" ]]; then + require_root + + echo "" + echo -e "${YELLOW}══════════════════════════════════════════════${NC}" + echo -e "${YELLOW} Removing ${SERVICE_NAME} (local)${NC}" + echo -e "${YELLOW}══════════════════════════════════════════════${NC}" + if [[ "$ARG_PURGE" == "1" ]]; then + echo -e " Mode: ${RED}Purge (delete all data)${NC}" + else + echo -e " Mode: Safe (keep data)" + fi + echo -e " Data: ${BASE_DIR}" + echo -e " Systemd: ${UNIT_NAME}.service" + echo -e "${YELLOW}══════════════════════════════════════════════${NC}" + echo "" + + if [[ "$ARG_PURGE" == "1" ]] && [[ -d "$BASE_DIR" ]]; then + backup_script="${SCRIPT_DIR}/backup-${SERVICE_NAME}.sh" + [[ -f "$backup_script" ]] && warn "Back up first? ${backup_script}" + echo "" + fi + + unit_file="/etc/systemd/system/${UNIT_NAME}.service" + if [[ -f "$unit_file" ]]; then + systemctl stop "${UNIT_NAME}.service" > /dev/null 2>&1 || true + systemctl disable "${UNIT_NAME}.service" > /dev/null 2>&1 || true + rm -f "$unit_file" + systemctl daemon-reload + ok "Removed systemd unit: ${UNIT_NAME}.service" + fi + + remove_traefik_dynamic_config "$SERVICE_NAME" + + if [[ "$ARG_PURGE" == "1" ]]; then + echo "" + warn "This will permanently delete: $BASE_DIR" + if [[ "$ARG_YES" != "1" ]]; then + read -rp "Are you sure? [y/N] " confirm + if [[ "${confirm,,}" != "y" ]]; then + info "Purge cancelled." + exit 0 + fi + fi + rm -rf "$BASE_DIR" + rm -rf "$HOME/.openclaw" 2>/dev/null + ok "Purged: $BASE_DIR" + fi + + echo "" + echo -e "${GREEN}══════════════════════════════════════════════${NC}" + echo -e "${GREEN} ${SERVICE_NAME} removed${NC}" + echo -e "${GREEN}══════════════════════════════════════════════${NC}" + if [[ "$ARG_PURGE" != "1" ]]; then + echo -e " Data preserved at ${BASE_DIR}" + echo -e " To purge: $0 --remove --purge" + fi + echo -e " To redeploy: $0" + echo -e "${GREEN}══════════════════════════════════════════════${NC}" + echo "" + exit 0 +fi + +# --- Domain resolution --- +if [[ -n "$ARG_DOMAIN" ]]; then + DOMAIN="$ARG_DOMAIN" +elif [[ -f "${BASE_DIR}/.env" ]] && grep -q '^DOMAIN=' "${BASE_DIR}/.env"; then + DOMAIN="$(grep '^DOMAIN=' "${BASE_DIR}/.env" | cut -d= -f2)" +else + DOMAIN="oc.an2.io" +fi + +# --- Detect current state for banner --- +install_mode="Fresh install" +if [[ -f "${BASE_DIR}/.env" ]]; then + install_mode="Re-run (preserving data)" +fi + +service_status="Not running" +if systemctl is-active --quiet "${UNIT_NAME}.service" 2>/dev/null; then + service_status="Running" +fi + +# --- Print deployment plan --- +echo "" +echo -e "${CYAN}══════════════════════════════════════════════${NC}" +echo -e "${CYAN} Deploying OpenClaw (Local)${NC}" +echo -e "${CYAN}══════════════════════════════════════════════${NC}" +echo -e " Domain: ${DOMAIN}" +echo -e " Data: ${BASE_DIR}/data/" +echo -e " Service: ${service_status}" +echo -e " Mode: ${install_mode}" +echo -e "${CYAN}══════════════════════════════════════════════${NC}" +echo "" + +# --- Check if already deployed and running --- +if [[ -f "${BASE_DIR}/.env" ]] && \ + systemctl is-active --quiet "${UNIT_NAME}.service" 2>/dev/null; then + echo -e "${GREEN} OpenClaw is already running — nothing to do.${NC}" + echo "" + echo -e " Health: curl -fsS http://127.0.0.1:18789/healthz" + echo -e " Logs: journalctl -u ${UNIT_NAME}.service -f" + echo -e " Backup: ${SCRIPT_DIR}/backup-openclaw.sh" + echo -e " Remove: $0 --remove [--purge]" + echo "" + exit 0 +fi + +# --- Infrastructure --- +require_root +detect_os + +# --- Install Node.js 22+ --- +install_nodejs_22() { + if command -v node &>/dev/null; then + local node_major + node_major="$(node --version | sed 's/^v//' | cut -d. -f1)" + if (( node_major >= 22 )); then + ok "Node.js $(node --version) already installed." + return 0 + fi + warn "Node.js $(node --version) is too old (need 22+). Upgrading..." + fi + + info "Installing Node.js 22..." + case "$DISTRO_FAMILY" in + debian) + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - > /dev/null 2>&1 + apt-get install -y -qq nodejs > /dev/null 2>&1 + ;; + fedora) + curl -fsSL https://rpm.nodesource.com/setup_22.x | bash - > /dev/null 2>&1 + dnf install -y -q nodejs > /dev/null 2>&1 + ;; + esac + ok "Node.js $(node --version) installed." +} + +# Install base prerequisites +case "$DISTRO_FAMILY" in + debian) + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y -qq curl openssl jq git ca-certificates > /dev/null 2>&1 + ;; + fedora) + dnf install -y -q curl openssl jq git ca-certificates > /dev/null 2>&1 + ;; +esac + +install_nodejs_22 + +# --- Backup restore check --- +if check_and_restore_backup "$SERVICE_NAME"; then + saved_domain="$DOMAIN" + # shellcheck source=/dev/null + source "${BASE_DIR}/.env" + DOMAIN="$saved_domain" + ok "Backup restored." +else + # --- Fresh install --- + if [[ -f "${BASE_DIR}/.env" ]]; then + saved_domain="$DOMAIN" + # shellcheck source=/dev/null + source "${BASE_DIR}/.env" + DOMAIN="$saved_domain" + info "Existing installation found. Updating..." + npm update -g openclaw 2>/dev/null || true + else + info "Installing OpenClaw via npm..." + npm install -g openclaw@latest + + mkdir -p "${BASE_DIR}/data" + + # Redirect ~/.openclaw to our data dir + rm -rf "$HOME/.openclaw" 2>/dev/null + ln -sfn "${BASE_DIR}/data" "$HOME/.openclaw" + + # --- Generate .env --- + cat > "${BASE_DIR}/.env" < "/etc/systemd/system/${UNIT_NAME}.service" < /dev/null 2>&1 +systemctl restart "${UNIT_NAME}.service" +ok "Systemd unit installed and started: ${UNIT_NAME}.service" + +# --- Wait for health --- +info "Waiting for OpenClaw to become healthy (up to 60s)..." +elapsed=0 +while (( elapsed < 60 )); do + if curl -fsS http://127.0.0.1:18789/ > /dev/null 2>&1; then + ok "OpenClaw is responding." + break + fi + sleep 5 + (( elapsed += 5 )) +done +if (( elapsed >= 60 )); then + warn "OpenClaw did not respond within 60s. Check: journalctl -u ${UNIT_NAME}.service" +fi + +# --- Configure gateway for Traefik/browser access --- +OPENCLAW_CONFIG_JSON="${BASE_DIR}/data/openclaw.json" +if [[ -f "$OPENCLAW_CONFIG_JSON" ]]; then + python3 - < /home/user/.openclaw/extensions/mattermost.disabled-20260410-230312 +- Removed stale plugin install metadata from /home/user/.openclaw/openclaw.json: + - plugins.installs.mattermost +- Removed stale disabled bundled plugin config warning: + - plugins.entries.huggingface + +Mattermost config in use +- baseUrl: https://mm.an2.io +- commands.native: true +- commands.nativeSkills: true +- callbackPath: /api/channels/mattermost/command +- callbackUrl: http://159.69.76.190:18789/api/channels/mattermost/command +- interactions.allowedSourceIps: ["172.20.0.3", "172.20.0.0/16", "127.0.0.1", "::1"] + +What fixed it +- Added the @claw bot to a Mattermost team +- After reconnect/restart, OpenClaw successfully registered slash commands + +Successful log indicators +- mattermost: connected as @claw +- mattermost: registered command /oc_status +- mattermost: registered command /oc_model +- mattermost: registered command /oc_models +- mattermost: slash commands activated for account default (20 commands) +- mattermost: slash commands registered (20 commands across 1 teams, callback=http://159.69.76.190:18789/api/channels/mattermost/command) + +Important reminder +- The bot must belong to at least one team for native slash command registration to work. +- The bot should also be added to the channels where you want it to interact. +- If native slash commands fail again, first check team membership before debugging callback URLs. + +Useful checks +- curl -sS -H 'Authorization: Bearer ' https://mm.an2.io/api/v4/users/me +- curl -sS -H 'Authorization: Bearer ' https://mm.an2.io/api/v4/users//teams +- grep -Ei 'mattermost|slash|callback' /tmp/openclaw-gateway-run.log + +If slash commands break again +1. Confirm the bot is still in a team +2. Confirm callbackUrl is reachable from the Mattermost server +3. Check for old leftover manually installed overrides in ~/.openclaw/extensions/ +4. Restart gateway and re-read Mattermost startup logs + +Button callback note +- Native slash commands now work. +- Button callback test initially failed with: mattermost interaction: rejected callback source remote=172.20.0.3 +- Updated channels.mattermost.interactions.allowedSourceIps to include: + - 172.20.0.3 + - 172.20.0.0/16 + - 127.0.0.1 + - ::1 +- After the allowlist update, button callbacks worked end-to-end. + +Elevated exec from Mattermost +- Added tools.elevated.enabled = true +- Added tools.elevated.allowFrom.mattermost = ["63rzn4hbijnrmjbomxxugntg9h"] +- This allows elevated exec only from the specified Mattermost source/account instead of opening it broadly. +- Gateway restart is required for this change to take full effect. + +Provider routing note for elevated exec +- Elevated exec checks for this session path were evaluated under provider=webchat, not provider=mattermost. +- Because of that, tools.elevated.allowFrom needed both: + - mattermost: ["63rzn4hbijnrmjbomxxugntg9h"] + - webchat: ["63rzn4hbijnrmjbomxxugntg9h"] +- Gateway restart is required after changing these allowFrom rules. + +Elevated exec verification +- After adding both provider allowlists and restarting the gateway, elevated exec from the Mattermost session path worked. +- Confirmed in-session test: sudo -n true -> SUDO_OK +- This verified that elevated exec was allowed for the effective provider path used by that session. diff --git a/how-to/traefik-openclaw.md b/how-to/traefik-openclaw.md new file mode 100644 index 0000000..16e7d64 --- /dev/null +++ b/how-to/traefik-openclaw.md @@ -0,0 +1,46 @@ +# OpenClaw behind Traefik +======================= + +What I changed +- Changed OpenClaw gateway bind in /home/user/.openclaw/openclaw.json from loopback to lan +- Added Traefik dynamic config at /opt/traefik/dynamic/openclaw.yml +- Target upstream is http://host.docker.internal:18789 +- Added gateway.controlUi.allowedOrigins entry for https://oc.an2.io + +Why +- Traefik runs in Docker. +- The Traefik container already has host.docker.internal mapped to the Docker host gateway. +- OpenClaw was only listening on 127.0.0.1:18789, which the container could not reach. +- Binding OpenClaw to lan makes it listen on the host network so Traefik can reach it through the host gateway. +- The Control UI loaded through Traefik uses browser origin https://oc.an2.io, so that exact origin must be listed in gateway.controlUi.allowedOrigins. + +Traefik config summary +- Router name: openclaw +- Entry point: websecure +- TLS cert resolver: letsencrypt +- Host rule: oc.an2.io +- Service upstream: http://host.docker.internal:18789 +- Dynamic file: /opt/traefik/dynamic/openclaw.yml + +OpenClaw config summary +- File: /home/user/.openclaw/openclaw.json +- gateway.bind: lan +- gateway.controlUi.allowedOrigins includes: + - http://localhost:18789 + - http://127.0.0.1:18789 + - https://oc.an2.io + +Notes +- https://localhost:18789 is not the correct public URL because OpenClaw on port 18789 speaks plain HTTP. +- TLS is terminated by Traefik on port 443 and then proxied to http://host.docker.internal:18789. +- If the browser shows "origin not allowed", check gateway.controlUi.allowedOrigins first. + +Useful checks +- ss -ltnp | grep 18789 +- curl http://localhost:18789/ +- curl -k https://oc.an2.io/ +- sudo docker logs --tail 100 traefik + +If needed +- Restart OpenClaw gateway after config changes +- Traefik dynamic config should hot-reload automatically, no Traefik restart is usually needed