<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Steady Pace]]></title><description><![CDATA[Sharing my ups and downs while building a long-term, sustainable digital business—creating things that matter and genuinely help people. No chasing quick wins.]]></description><link>https://aarroyo.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 08 Sep 2026 05:18:16 GMT</lastBuildDate><atom:link href="https://aarroyo.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The Complete Guide to Setting Up OpenClaw on Hetzner VPS (2026)]]></title><description><![CDATA[From zero to a fully operational AI agent with Telegram, web search, browser automation, and X/Twitter integration — for under $10/month.

Why This Guide Exists
Most OpenClaw tutorials cover the basic]]></description><link>https://aarroyo.hashnode.dev/the-complete-guide-to-setting-up-openclaw-on-hetzner-vps-2026</link><guid isPermaLink="true">https://aarroyo.hashnode.dev/the-complete-guide-to-setting-up-openclaw-on-hetzner-vps-2026</guid><category><![CDATA[openclaw]]></category><category><![CDATA[Hetzner]]></category><dc:creator><![CDATA[Anderson Arroyo]]></dc:creator><pubDate>Tue, 07 Apr 2026 21:51:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6595493ef74d591fefc5ef9c/e9818efc-e267-4b06-9dc1-00910a0336fd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>From zero to a fully operational AI agent with Telegram, web search, browser automation, and X/Twitter integration — for under $10/month.</p>
</blockquote>
<h2>Why This Guide Exists</h2>
<p>Most OpenClaw tutorials cover the basics but skip the real-world problems: permission errors, config mismatches, Docker gotchas, and the "pairing required" errors that make you want to throw your laptop. This guide covers everything — including the mistakes — so you don't have to figure them out yourself.</p>
<p><strong>What you'll have by the end:</strong></p>
<ul>
<li>A 24/7 AI agent running on a Hetzner VPS</li>
<li>Telegram integration (chat with your agent from your phone)</li>
<li>Web search (SearXNG — free, unlimited, self-hosted)</li>
<li>Browser automation (Chromium for web scraping and research)</li>
<li>X/Twitter integration (post, reply, search via xurl)</li>
<li>Text-to-speech (Microsoft TTS — free)</li>
<li>Tailscale HTTPS access (no SSH tunnel needed)</li>
<li>GitHub workspace backup</li>
<li>Secure, production-ready configuration</li>
</ul>
<p><strong>Cost:</strong> ~$7-10/month (Hetzner VPS only)</p>
<p><strong>Time:</strong> ~2 hours (including inevitable troubleshooting)</p>
<p><strong>Pro tip:</strong> Use Claude Code (or any AI coding assistant) to streamline this process. I built this entire setup in a single session with Claude helping me debug every issue in real-time.</p>
<hr />
<h2>Table of Contents</h2>
<ol>
<li><a href="#1-choose-your-vps">Choose Your VPS</a></li>
<li><a href="#2-initial-server-setup">Initial Server Setup</a></li>
<li><a href="#3-clone-and-configure-openclaw">Clone and Configure OpenClaw</a></li>
<li><a href="#4-docker-compose-customization">Docker Compose Customization</a></li>
<li><a href="#5-configuration-file">Configuration File (openclaw.json)</a></li>
<li><a href="#6-build-and-launch">Build and Launch</a></li>
<li><a href="#7-access-tailscale-vs-ssh-tunnel">Access: Tailscale vs SSH Tunnel</a></li>
<li><a href="#8-telegram-channel-setup">Telegram Channel Setup</a></li>
<li><a href="#9-model-provider-setup">Model Provider Setup (OpenAI Codex OAuth)</a></li>
<li><a href="#10-fallback-model">Fallback Model (Gemini Flash)</a></li>
<li><a href="#11-web-search">Web Search (SearXNG + Codex Native)</a></li>
<li><a href="#12-browser-automation">Browser Automation (Chromium)</a></li>
<li><a href="#13-text-to-speech">Text-to-Speech</a></li>
<li><a href="#14-x-twitter-integration">X/Twitter Integration (xurl)</a></li>
<li><a href="#15-github-workspace-backup">GitHub Workspace Backup</a></li>
<li><a href="#16-google-workspace">Google Workspace (Calendar, Gmail, Drive)</a></li>
<li><a href="#17-security-hardening">Security Hardening</a></li>
<li><a href="#18-skills-and-tools">Skills &amp; Tools</a></li>
<li><a href="#19-mobile-access">Mobile Access (Termius)</a></li>
<li><a href="#20-maintenance-and-updates">Maintenance &amp; Updates</a></li>
<li><a href="#21-common-errors-and-fixes">Common Errors &amp; Fixes</a></li>
</ol>
<hr />
<h2>1. Choose Your VPS</h2>
<h3>Why Hetzner?</h3>
<ul>
<li>Cheapest reliable VPS in Europe (~€4-7/month)</li>
<li>Great uptime and network</li>
<li>Easy scaling (upgrade RAM without losing data)</li>
</ul>
<h3>Recommended Specs</h3>
<table>
<thead>
<tr>
<th>Spec</th>
<th>Minimum</th>
<th>Recommended</th>
</tr>
</thead>
<tbody><tr>
<td>RAM</td>
<td>4 GB</td>
<td><strong>8 GB</strong> (if using Chromium browser)</td>
</tr>
<tr>
<td>CPU</td>
<td>2 vCPU</td>
<td>2+ vCPU</td>
</tr>
<tr>
<td>Storage</td>
<td>40 GB</td>
<td>40+ GB</td>
</tr>
<tr>
<td>OS</td>
<td>Ubuntu 24.04</td>
<td>Ubuntu 24.04</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>Important:</strong> Before choosing a location, check server availability at <a href="https://radar.iodev.org/">radar.iodev.org</a>. Pick a location with good availability — if you need to scale up/down later, low-availability locations will block you. I got stuck and couldn't rescale for hours because my location had no availability.</p>
</blockquote>
<h3>Why 8 GB RAM?</h3>
<p>With 4 GB I hit memory limits during Docker builds (exit code 137 = OOM killed) and couldn't run Chromium + SearXNG + the gateway simultaneously. With 8 GB, everything runs comfortably with ~5 GB free.</p>
<hr />
<h2>2. Initial Server Setup</h2>
<h3>SSH into your VPS</h3>
<pre><code class="language-bash">ssh root@YOUR_VPS_IP
</code></pre>
<h3>Install Docker</h3>
<pre><code class="language-bash">apt-get update
apt-get install -y git curl ca-certificates
curl -fsSL https://get.docker.com | sh
</code></pre>
<h3>Install Tailscale (optional but recommended)</h3>
<pre><code class="language-bash">curl -fsSL https://tailscale.com/install.sh | sh
tailscale up
</code></pre>
<p>Open the auth URL in your browser to join the VPS to your tailnet.</p>
<h3>Install GitHub CLI</h3>
<pre><code class="language-bash">apt install -y gh
gh auth login
</code></pre>
<h3>Create persistent directories</h3>
<pre><code class="language-bash">mkdir -p /root/.openclaw/workspace
chown -R 1000:1000 /root/.openclaw
</code></pre>
<blockquote>
<p><strong>Why uid 1000?</strong> The OpenClaw container runs as user <code>node</code> (uid 1000), not root. All mounted volumes must be owned by uid 1000 or the gateway will crash with <code>EACCES: permission denied</code>.</p>
</blockquote>
<hr />
<h2>3. Clone and Configure OpenClaw</h2>
<pre><code class="language-bash">git clone https://github.com/openclaw/openclaw.git
cd openclaw
git checkout v2026.4.2  # Use stable release, not main HEAD
</code></pre>
<blockquote>
<p><strong>Lesson learned:</strong> I initially used <code>main</code> branch and hit stack overflow errors in all plugins. Always use a stable release tag.</p>
</blockquote>
<h3>Create your .env file</h3>
<pre><code class="language-bash">cat &gt; .env &lt;&lt; 'EOF'
OPENCLAW_IMAGE=openclaw:latest
OPENCLAW_GATEWAY_TOKEN=GENERATE_WITH_openssl_rand_-hex_32
OPENCLAW_GATEWAY_BIND=lan
OPENCLAW_GATEWAY_PORT=18789

OPENCLAW_CONFIG_DIR=/root/.openclaw
OPENCLAW_WORKSPACE_DIR=/root/.openclaw/workspace

TELEGRAM_BOT_TOKEN=YOUR_TELEGRAM_BOT_TOKEN
GEMINI_API_KEY=YOUR_GEMINI_API_KEY
SEARXNG_BASE_URL=http://searxng:8080

GOG_KEYRING_PASSWORD=GENERATE_WITH_openssl_rand_-hex_32
TZ=UTC
EOF
</code></pre>
<p>Generate strong tokens:</p>
<pre><code class="language-bash">openssl rand -hex 32
</code></pre>
<hr />
<h2>4. Docker Compose Customization</h2>
<p>The default <code>docker-compose.yml</code> needs several modifications. Here's what to change and why:</p>
<h3>Key modifications:</h3>
<ol>
<li><strong>Add <code>build</code> directive</strong> — the default image doesn't exist in any registry; you build locally</li>
<li><strong>Install Chromium</strong> — <code>OPENCLAW_INSTALL_BROWSER: "1"</code> build arg</li>
<li><strong>Bind ports to loopback</strong> — <code>127.0.0.1:</code> prefix for security</li>
<li><strong>Add environment variables</strong> — Telegram, Gemini, SearXNG</li>
<li><strong>Mount persistent volumes</strong> — SSH keys, git config, xurl tokens</li>
<li><strong>Add SearXNG service</strong> — free, unlimited web search</li>
<li><strong>Add <code>--allow-unconfigured</code></strong> — lets gateway start without <code>gateway.mode=local</code></li>
</ol>
<h3>Complete docker-compose.yml (gateway service):</h3>
<pre><code class="language-yaml">services:
  openclaw-gateway:
    image: ${OPENCLAW_IMAGE:-openclaw:local}
    build:
      context: .
      args:
        OPENCLAW_INSTALL_BROWSER: "1"
    environment:
      HOME: /home/node
      TERM: xterm-256color
      OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN:-}
      NODE_ENV: production
      TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
      GEMINI_API_KEY: ${GEMINI_API_KEY:-}
      SEARXNG_BASE_URL: http://searxng:8080
      TZ: ${OPENCLAW_TZ:-UTC}
    volumes:
      - ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw
      - ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace
      - /root/.agent-ssh:/home/node/.ssh
      - /root/.agent-gitconfig/.gitconfig:/home/node/.gitconfig:ro
      - /root/.agent-xurl/.xurl:/home/node/.xurl
    ports:
      - "127.0.0.1:${OPENCLAW_GATEWAY_PORT:-18789}:18789"
      - "127.0.0.1:${OPENCLAW_BRIDGE_PORT:-18790}:18790"
    init: true
    restart: unless-stopped
    command:
      [
        "node", "dist/index.js", "gateway",
        "--bind", "${OPENCLAW_GATEWAY_BIND:-lan}",
        "--port", "18789",
        "--allow-unconfigured",
      ]
    healthcheck:
      test: ["CMD", "node", "-e",
        "fetch('http://127.0.0.1:18789/healthz').then((r)=&gt;process.exit(r.ok?0:1)).catch(()=&gt;process.exit(1))"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 20s

  searxng:
    image: docker.io/searxng/searxng:latest
    restart: unless-stopped
    volumes:
      - searxng-data:/var/cache/searxng/
    environment:
      - SEARXNG_BASE_URL=http://searxng:8080/

volumes:
  searxng-data:
</code></pre>
<h3>Baking custom CLI tools into the image</h3>
<p>If you need tools like <code>xurl</code> (X/Twitter), <code>clawhub</code> (skill discovery), or <code>summarize</code> (URL/video summarization), add them to the Dockerfile before the <code>USER node</code> line:</p>
<pre><code class="language-dockerfile"># Custom agent CLI tools
RUN npm i -g @xdevplatform/xurl clawhub @steipete/summarize
</code></pre>
<hr />
<h2>5. Configuration File</h2>
<p>The gateway config lives at <code>/root/.openclaw/openclaw.json</code> (mounted inside the container at <code>/home/node/.openclaw/openclaw.json</code>).</p>
<blockquote>
<p><strong>Critical:</strong> The file must be named <code>openclaw.json</code>, NOT <code>config.json</code>. I wasted 30 minutes on this.</p>
</blockquote>
<blockquote>
<p><strong>Critical:</strong> After editing from the host as root, always run:</p>
<pre><code class="language-bash">chown 1000:1000 /root/.openclaw/openclaw.json
</code></pre>
<p>Otherwise the container (uid 1000) can't read it.</p>
</blockquote>
<h3>Complete openclaw.json:</h3>
<pre><code class="language-json">{
  "gateway": {
    "mode": "local",
    "controlUi": {
      "allowedOrigins": [
        "http://127.0.0.1:18789",
        "http://localhost:18789",
        "https://YOUR-HOSTNAME.tailnet-name.ts.net"
      ],
      "allowInsecureAuth": true
    },
    "auth": {
      "mode": "token",
      "token": "SAME_TOKEN_AS_YOUR_ENV_FILE",
      "rateLimit": {
        "maxAttempts": 10,
        "windowMs": 60000,
        "lockoutMs": 300000
      }
    },
    "port": 18789,
    "bind": "loopback",
    "tailscale": {
      "mode": "off",
      "resetOnExit": false
    },
    "nodes": {
      "denyCommands": []
    }
  },
  "channels": {
    "telegram": {
      "enabled": true,
      "dmPolicy": "pairing",
      "allowFrom": ["YOUR_TELEGRAM_USER_ID"],
      "defaultTo": "YOUR_TELEGRAM_USER_ID",
      "execApprovals": {
        "enabled": true,
        "target": "dm"
      },
      "groups": {
        "*": { "requireMention": true }
      }
    }
  },
  "agents": {
    "defaults": {
      "workspace": "/home/node/.openclaw/workspace",
      "model": {
        "primary": "openai-codex/gpt-5.4",
        "fallbacks": ["google/gemini-3-flash-preview"]
      }
    }
  },
  "messages": {
    "tts": {
      "auto": "inbound",
      "provider": "microsoft",
      "maxTextLength": 3000,
      "summaryModel": "google/gemini-3-flash-preview",
      "providers": {
        "microsoft": {
          "voice": "en-US-MichelleNeural",
          "lang": "en-US"
        }
      }
    }
  },
  "session": {
    "dmScope": "per-channel-peer"
  },
  "tools": {
    "profile": "full",
    "web": {
      "search": {
        "enabled": true,
        "openaiCodex": {
          "enabled": true,
          "mode": "cached",
          "contextSize": "high"
        }
      }
    }
  }
}
</code></pre>
<h3>Token mismatch warning</h3>
<p>The <code>openclaw onboard</code> wizard writes its own gateway token to <code>openclaw.json</code>. If it's different from your <code>.env</code> token, CLI commands, subagents, and cron jobs will fail. Always sync them:</p>
<pre><code class="language-bash"># Check token in config
docker compose exec openclaw-gateway cat /home/node/.openclaw/openclaw.json | grep token

# Compare with .env
grep OPENCLAW_GATEWAY_TOKEN .env
</code></pre>
<hr />
<h2>6. Build and Launch</h2>
<pre><code class="language-bash">docker compose build    # First build takes 5-10 minutes
docker compose up -d openclaw-gateway searxng
</code></pre>
<h3>Verify:</h3>
<pre><code class="language-bash"># Wait ~30 seconds for startup, then:
curl http://127.0.0.1:18789/healthz
# Should return: {"ok":true,"status":"live"}

docker compose ps
# All services should show "Up"

docker logs openclaw-openclaw-gateway-1 2&gt;&amp;1 | grep "listening"
# Should show: [gateway] listening on ws://0.0.0.0:18789
</code></pre>
<hr />
<h2>7. Access: Tailscale vs SSH Tunnel</h2>
<h3>Option A: Tailscale Serve (recommended)</h3>
<p>No SSH tunnel needed. HTTPS built-in.</p>
<pre><code class="language-bash">tailscale serve --bg http://127.0.0.1:18789
</code></pre>
<blockquote>
<p><strong>Important:</strong> Use <code>http://</code> not <code>https+insecure://</code> — the gateway serves HTTP, not HTTPS. Using the wrong protocol gives you a 502 error.</p>
</blockquote>
<p>Add the Tailscale origin to your <code>openclaw.json</code>:</p>
<pre><code class="language-json">"allowedOrigins": [
  "http://127.0.0.1:18789",
  "https://YOUR-HOSTNAME.tailnet-name.ts.net"
]
</code></pre>
<p>Access from any device on your tailnet: <code>https://YOUR-HOSTNAME.tailnet-name.ts.net/</code></p>
<h3>Option B: SSH Tunnel (fallback)</h3>
<pre><code class="language-bash">ssh -N -L 18789:127.0.0.1:18789 root@YOUR_VPS_IP
</code></pre>
<p>Then open <code>http://127.0.0.1:18789/</code> in your browser.</p>
<blockquote>
<p><strong>Note:</strong> With SSH tunnel, you need <code>"allowInsecureAuth": true</code> because you're accessing via HTTP (the tunnel encrypts the transport, but the browser sees HTTP).</p>
</blockquote>
<h3>Device Pairing</h3>
<p>Every new browser/device needs to be "paired" (approved):</p>
<pre><code class="language-bash">docker compose exec openclaw-gateway node dist/index.js devices list
docker compose exec openclaw-gateway node dist/index.js devices approve --latest
</code></pre>
<hr />
<h2>8. Telegram Channel Setup</h2>
<h3>Create your bot</h3>
<ol>
<li>Open <code>@BotFather</code> in Telegram</li>
<li><code>/newbot</code> → follow prompts → copy the bot token</li>
<li>Add token to your <code>.env</code> file</li>
</ol>
<h3>Configure in openclaw.json</h3>
<p>The key settings:</p>
<ul>
<li><code>dmPolicy: "pairing"</code> — unknown users get a pairing code you must approve</li>
<li><code>execApprovals.enabled: true</code> — the agent asks your permission before running commands</li>
<li><code>allowFrom</code> + <code>defaultTo</code> — <strong>required for exec approvals to work</strong> (needs your Telegram user ID)</li>
<li><code>groups.*: { requireMention: true }</code> — bot only responds when mentioned in groups</li>
</ul>
<h3>Find your Telegram User ID</h3>
<p>Send a message to your bot, then check:</p>
<pre><code class="language-bash">docker compose logs openclaw-gateway | grep "sendMessage.*chat="
</code></pre>
<h3>Approve your user</h3>
<p>After sending a DM to the bot:</p>
<pre><code class="language-bash">docker compose exec openclaw-gateway node dist/index.js pairing list telegram
docker compose exec openclaw-gateway node dist/index.js pairing approve telegram CODE
</code></pre>
<h3>BotFather settings (for groups)</h3>
<ul>
<li><code>/setprivacy</code> → Disable (so bot sees all group messages)</li>
<li>Remove and re-add bot to groups after changing privacy</li>
</ul>
<hr />
<h2>9. Model Provider Setup (OpenAI Codex OAuth)</h2>
<p>OpenAI Codex lets you use your ChatGPT subscription (no separate API billing):</p>
<pre><code class="language-bash">docker compose exec -it openclaw-gateway node dist/index.js onboard --auth-choice openai-codex
</code></pre>
<p>This opens an OAuth flow — paste the URL in your browser, authorize, and return the code.</p>
<hr />
<h2>10. Fallback Model (Gemini Flash)</h2>
<p>If OpenAI goes down, your agent is dead without a fallback. Gemini Flash is free (with limits):</p>
<ol>
<li>Get API key at <a href="https://aistudio.google.com/apikey">aistudio.google.com/apikey</a></li>
<li>Add <code>GEMINI_API_KEY=your-key</code> to <code>.env</code></li>
<li>Config: <code>"fallbacks": ["google/gemini-3-flash-preview"]</code></li>
</ol>
<blockquote>
<p><strong>Note:</strong> The config key is <code>"fallbacks"</code> (array), not <code>"fallback"</code> (string). This tripped me up.</p>
</blockquote>
<hr />
<h2>11. Web Search</h2>
<h3>Dual-layer approach:</h3>
<ol>
<li><strong>Codex Native Search</strong> — built into OpenAI Codex, works automatically for your primary model</li>
<li><strong>SearXNG</strong> — self-hosted, free, unlimited, works for fallback model and provides Google+Bing+DuckDuckGo combined results</li>
</ol>
<p>SearXNG runs as a Docker service alongside your gateway (see docker-compose above). No API keys needed.</p>
<blockquote>
<p><strong>SearXNG requires OpenClaw v2026.4.1+</strong> — earlier versions don't have the bundled plugin.</p>
</blockquote>
<hr />
<h2>12. Browser Automation (Chromium)</h2>
<p>Build with <code>OPENCLAW_INSTALL_BROWSER: "1"</code> in docker-compose build args. Adds ~300MB to image.</p>
<p>Use cases:</p>
<ul>
<li>Navigate competitor websites</li>
<li>Take screenshots of search results</li>
<li>Extract content from pages</li>
<li>Automate web tasks</li>
</ul>
<hr />
<h2>13. Text-to-Speech</h2>
<p>Microsoft TTS is free (no API key), but you need to <strong>explicitly enable the Microsoft plugin</strong> — otherwise you'll get "no provider registered" errors even with the correct TTS config.</p>
<h3>Step 1: Enable the plugin</h3>
<pre><code class="language-bash">docker compose exec openclaw-gateway openclaw config set plugins.entries.microsoft.enabled true
docker compose restart openclaw-gateway
</code></pre>
<h3>Step 2: Configure TTS in openclaw.json</h3>
<pre><code class="language-json">"messages": {
  "tts": {
    "auto": "inbound",
    "provider": "microsoft",
    "providers": {
      "microsoft": {
        "voice": "en-US-MichelleNeural",
        "lang": "en-US"
      }
    }
  }
}
</code></pre>
<ul>
<li><code>"inbound"</code> = responds with audio only when you send audio</li>
<li>For Spanish: <code>"voice": "es-MX-DaliaNeural"</code></li>
<li>For always audio: <code>"auto": "always"</code></li>
</ul>
<blockquote>
<p><strong>Common gotcha:</strong> The TTS config alone is not enough. You must also set <code>plugins.entries.microsoft.enabled: true</code> or the Microsoft speech provider won't register and TTS will silently fail.</p>
</blockquote>
<hr />
<h2>14. X/Twitter Integration (xurl)</h2>
<h3>Setup</h3>
<ol>
<li><p>Create a free X Developer account at <a href="https://developer.x.com">developer.x.com</a></p>
<ul>
<li>App type: <strong>Automated App or Bot</strong></li>
<li>Permissions: <strong>Read and Write</strong></li>
<li>Callback URL: <code>http://localhost:8080/callback</code></li>
<li>Free tier: 500 posts/month (plenty for personal brand)</li>
</ul>
</li>
<li><p>Get your OAuth 1.0a tokens (API Key, Secret, Access Token, Access Token Secret)</p>
</li>
<li><p>Authenticate inside the container:</p>
</li>
</ol>
<pre><code class="language-bash">docker compose exec -it openclaw-gateway xurl auth oauth1 \
  --consumer-key YOUR_API_KEY \
  --consumer-secret YOUR_API_SECRET \
  --access-token YOUR_ACCESS_TOKEN \
  --token-secret YOUR_TOKEN_SECRET
</code></pre>
<blockquote>
<p><strong>Use OAuth 1.0a, not OAuth 2.0</strong> for Docker setups. OAuth 2.0 requires a localhost callback that doesn't work inside containers without port forwarding.</p>
</blockquote>
<ol>
<li>Verify: <code>docker compose exec openclaw-gateway xurl whoami</code></li>
</ol>
<h3>Persistence</h3>
<p>Mount the xurl tokens as a volume:</p>
<pre><code class="language-yaml">volumes:
  - /root/.agent-xurl/.xurl:/home/node/.xurl
</code></pre>
<hr />
<h2>15. GitHub Workspace Backup</h2>
<h3>Create a dedicated agent GitHub account</h3>
<p>Don't use your personal account — create a separate one (e.g., <code>your-agent-dev</code>) for:</p>
<ul>
<li>Security isolation</li>
<li>Clear audit trail</li>
<li>Easy revocation</li>
</ul>
<h3>SSH key setup (inside container)</h3>
<pre><code class="language-bash">docker compose exec openclaw-gateway ssh-keygen -t ed25519 -C "agent@email.com" -f /home/node/.ssh/id_ed25519 -N ""
docker compose exec openclaw-gateway cat /home/node/.ssh/id_ed25519.pub
</code></pre>
<p>Add the public key to GitHub (Settings → SSH keys).</p>
<h3>Persist SSH keys</h3>
<p>Mount as volume:</p>
<pre><code class="language-yaml">volumes:
  - /root/.agent-ssh:/home/node/.ssh
  - /root/.agent-gitconfig/.gitconfig:/home/node/.gitconfig:ro
</code></pre>
<h3>Connect workspace to GitHub</h3>
<pre><code class="language-bash">docker compose exec -w /home/node/.openclaw/workspace openclaw-gateway \
  git remote set-url origin git@github.com:YOUR_AGENT_USER/your-workspace-repo.git
</code></pre>
<hr />
<h2>16. Google Workspace (Calendar, Gmail, Drive)</h2>
<p>Give your agent access to your Google Calendar, Gmail, Drive, Sheets, and Docs using <code>gog</code> (Google Workspace CLI).</p>
<h3>Install gog</h3>
<p>Add to your Dockerfile (before <code>USER node</code>):</p>
<pre><code class="language-dockerfile">RUN curl -fsSL https://github.com/steipete/gogcli/releases/download/v0.12.0/gogcli_0.12.0_linux_amd64.tar.gz | tar -xz -C /usr/local/bin gog &amp;&amp; chmod +x /usr/local/bin/gog
</code></pre>
<h3>Create Google OAuth credentials</h3>
<ol>
<li>Go to <a href="https://console.cloud.google.com">console.cloud.google.com</a></li>
<li>Create a new project (e.g., "openclaw-agent")</li>
<li>Enable APIs: <strong>Google Calendar API</strong>, <strong>Gmail API</strong>, <strong>Google Drive API</strong>, <strong>Google Sheets API</strong>, <strong>Google Docs API</strong></li>
<li>Go to <strong>APIs &amp; Services</strong> → <strong>Credentials</strong> → <strong>Create Credentials</strong> → <strong>OAuth Client ID</strong></li>
<li>Type: <strong>Desktop App</strong> (not Web Application — web clients fail with dynamic port mismatches)</li>
<li>Download <code>client_secret_XXXX.json</code></li>
</ol>
<h3>OAuth consent screen</h3>
<p>Your app will be in <strong>Testing</strong> mode by default. Add your personal email as a <strong>Test user</strong> (OAuth consent screen → Test users), otherwise you'll get a 403 <code>access_denied</code> error.</p>
<blockquote>
<p><strong>Use OAuth, not Service Accounts.</strong> Service accounts need Google Workspace admin + domain-wide delegation — overkill and dangerous for personal use. OAuth is designed for this.</p>
</blockquote>
<h3>Upload credentials to VPS</h3>
<pre><code class="language-bash">scp client_secret_XXXX.json root@YOUR_VPS_IP:/root/.openclaw/client_secret.json
chown 1000:1000 /root/.openclaw/client_secret.json
chmod 600 /root/.openclaw/client_secret.json
</code></pre>
<h3>Authenticate (headless VPS flow)</h3>
<p>The <code>--remote</code> flag is key — it gives you a URL to open in your local browser instead of trying to open a browser on the VPS.</p>
<pre><code class="language-bash"># Step 1: Register credentials
docker compose exec -it openclaw-gateway gog auth credentials /home/node/.openclaw/client_secret.json

# Step 2: Start OAuth flow (generates URL)
docker compose exec -it openclaw-gateway gog auth add YOUR_EMAIL@gmail.com --services calendar,gmail,drive,sheets,docs --remote --step 1
</code></pre>
<p>Open the URL in your browser, authorize, then copy the <strong>full callback URL</strong> from your browser's address bar. Then:</p>
<pre><code class="language-bash"># Step 3: Complete OAuth with the callback URL
docker compose exec -it openclaw-gateway gog auth add YOUR_EMAIL@gmail.com --services calendar,gmail,drive,sheets,docs --remote --step 2 --auth-url "PASTE_FULL_CALLBACK_URL_HERE"
</code></pre>
<blockquote>
<p><strong>Use your personal email</strong>, not the agent's email. The whole point is to give the agent access to <strong>your</strong> calendar and emails.</p>
</blockquote>
<h3>Verify</h3>
<pre><code class="language-bash">docker compose exec -e GOG_ACCOUNT=YOUR_EMAIL@gmail.com openclaw-gateway gog auth list
docker compose exec -e GOG_ACCOUNT=YOUR_EMAIL@gmail.com openclaw-gateway gog calendar events primary --from \((date -u +%Y-%m-%dT00:00:00Z) --to \)(date -u -d "+7 days" +%Y-%m-%dT23:59:59Z)
</code></pre>
<h3>Persistence</h3>
<p>Add these to your <code>docker-compose.yml</code>:</p>
<pre><code class="language-yaml">environment:
  GOG_KEYRING_PASSWORD: ${GOG_KEYRING_PASSWORD:-}
  GOG_ACCOUNT: YOUR_EMAIL@gmail.com

volumes:
  - /root/.agent-gog:/home/node/.config/gogcli
</code></pre>
<p>Create the persistent directory:</p>
<pre><code class="language-bash">mkdir -p /root/.agent-gog &amp;&amp; chown 1000:1000 /root/.agent-gog
</code></pre>
<blockquote>
<p><strong>Common gotcha:</strong> gog stores config in <code>/home/node/.config/gogcli/</code> (not <code>.config/gog/</code>). Mount to the correct path or auth will fail.</p>
</blockquote>
<h3>Usage</h3>
<p>Ask your agent in Telegram:</p>
<ul>
<li>"What's on my calendar this week?"</li>
<li>"Create a meeting tomorrow at 10am called Team Sync"</li>
<li>"Search my emails from the last 3 days"</li>
<li>"Create a Google Doc with a draft for my blog post"</li>
</ul>
<p>The <code>gog</code> skill is bundled with OpenClaw — no skill installation needed.</p>
<hr />
<h2>17. Security Hardening</h2>
<h3>Run the security audit</h3>
<pre><code class="language-bash">docker compose exec openclaw-gateway openclaw security audit
</code></pre>
<h3>Key fixes:</h3>
<ol>
<li><strong>Config file permissions</strong> (critical):</li>
</ol>
<pre><code class="language-bash">chmod 600 /root/.openclaw/openclaw.json
chown 1000:1000 /root/.openclaw/openclaw.json
</code></pre>
<ol>
<li><strong>Auth rate limiting</strong> — protects against brute force:</li>
</ol>
<pre><code class="language-json">"rateLimit": {
  "maxAttempts": 10,
  "windowMs": 60000,
  "lockoutMs": 300000
}
</code></pre>
<ol>
<li><p><strong>Loopback port binding</strong> — <code>127.0.0.1:</code> prefix prevents direct internet exposure</p>
</li>
<li><p><strong>Exec approvals</strong> — agent asks permission before running commands</p>
</li>
<li><p><strong><code>allowFrom</code></strong> — whitelist your Telegram user ID</p>
</li>
</ol>
<hr />
<h2>18. Skills &amp; Tools</h2>
<h3>Built-in (no setup needed)</h3>
<ul>
<li><code>skill-creator</code> — create custom skills</li>
<li><code>github</code> — GitHub operations via gh CLI</li>
<li><code>weather</code> — weather data (no API key)</li>
</ul>
<h3>Recommended installs (add to Dockerfile)</h3>
<pre><code class="language-dockerfile">RUN npm i -g @xdevplatform/xurl clawhub @steipete/summarize
</code></pre>
<ul>
<li><strong>xurl</strong> — X/Twitter posting and engagement</li>
<li><strong>clawhub</strong> — discover and install community skills</li>
<li><strong>summarize</strong> — summarize URLs, YouTube videos, podcasts</li>
</ul>
<h3>Useful bundled skills</h3>
<ul>
<li><code>blogwatcher</code> — monitor competitor blogs</li>
<li><code>video-frames</code> — extract frames from videos (needs ffmpeg)</li>
<li><code>nano-pdf</code> — edit PDFs</li>
<li><code>session-logs</code> — search conversation history</li>
</ul>
<hr />
<h2>19. Mobile Access (Termius)</h2>
<p>Use <a href="https://termius.com">Termius</a> for SSH from Android/iOS:</p>
<ol>
<li>Generate a biometric key (fingerprint/Face ID)</li>
<li>Add the public key to your VPS:</li>
</ol>
<pre><code class="language-bash">echo "YOUR_PUBLIC_KEY" &gt;&gt; /root/.ssh/authorized_keys
</code></pre>
<ol>
<li>Each device needs its own key — biometric keys don't sync between devices</li>
</ol>
<hr />
<h2>20. Maintenance &amp; Updates</h2>
<h3>Update OpenClaw</h3>
<pre><code class="language-bash">cd /root/openclaw
git fetch --tags
git checkout v2026.X.Y  # Latest stable tag
docker compose build --no-cache
docker compose down &amp;&amp; docker compose up -d
</code></pre>
<h3>After editing openclaw.json from host</h3>
<p>Always fix ownership:</p>
<pre><code class="language-bash">chown 1000:1000 /root/.openclaw/openclaw.json
</code></pre>
<p>Or edit from inside the container to avoid this:</p>
<pre><code class="language-bash">docker compose exec openclaw-gateway openclaw config set &lt;key&gt; &lt;value&gt;
</code></pre>
<hr />
<h2>21. Common Errors &amp; Fixes</h2>
<h3>"non-loopback Control UI requires gateway.controlUi.allowedOrigins"</h3>
<p>Add your access URL to <code>allowedOrigins</code> in <code>openclaw.json</code>. The gateway blocks browsers from origins not in the list.</p>
<h3>"pairing required" on Control UI login</h3>
<p>Your browser needs to be approved as a device:</p>
<pre><code class="language-bash">docker compose exec openclaw-gateway node dist/index.js devices approve --latest
</code></pre>
<h3>"exec approval is required, but chat exec approvals are not enabled"</h3>
<p>You need <code>allowFrom</code> with your Telegram user ID AND <code>execApprovals.enabled: true</code>. Without a resolved approver, exec approvals silently disable themselves.</p>
<h3>"EACCES: permission denied" on openclaw.json</h3>
<p>The file is owned by root but the container runs as uid 1000:</p>
<pre><code class="language-bash">chown 1000:1000 /root/.openclaw/openclaw.json
</code></pre>
<h3>"fatal: could not open .git/COMMIT_EDITMSG: Permission denied"</h3>
<p>Same ownership issue in the workspace <code>.git/</code> directory:</p>
<pre><code class="language-bash">chown -R 1000:1000 /root/.openclaw/workspace/.git/
</code></pre>
<h3>Config not loading / "Invalid input"</h3>
<ul>
<li>Config file must be named <code>openclaw.json</code> (not <code>config.json</code>)</li>
<li><code>"fallbacks"</code> is an array: <code>["model"]</code> not <code>"model"</code></li>
<li>After editing, do <code>docker compose down &amp;&amp; docker compose up -d</code> (not just restart)</li>
</ul>
<h3>Build killed (exit code 137)</h3>
<p>Out of memory. Upgrade to 8 GB RAM or add swap:</p>
<pre><code class="language-bash">fallocate -l 2G /swapfile &amp;&amp; chmod 600 /swapfile &amp;&amp; mkswap /swapfile &amp;&amp; swapon /swapfile
</code></pre>
<h3>Tailscale serve gives 502</h3>
<p>You used <code>https+insecure://</code> but the gateway serves HTTP:</p>
<pre><code class="language-bash">tailscale serve --https=443 off
tailscale serve --bg http://127.0.0.1:18789
</code></pre>
<h3>Stack overflow errors in plugins</h3>
<p>You're on an unstable commit. Use a release tag:</p>
<pre><code class="language-bash">git checkout v2026.4.2
docker compose build --no-cache
</code></pre>
<hr />
<h2>Resources</h2>
<ul>
<li><a href="https://docs.openclaw.ai/install/hetzner">Official Hetzner Setup Guide</a></li>
<li><a href="https://docs.openclaw.ai/install/docker-vm-runtime">Docker VM Runtime Guide</a></li>
<li><a href="https://docs.openclaw.ai/gateway/configuration-reference">Gateway Configuration Reference</a></li>
<li><a href="https://docs.openclaw.ai/channels/telegram">Telegram Channel Docs</a></li>
<li><a href="https://docs.openclaw.ai/tools/skills">Skills Documentation</a></li>
<li><a href="https://radar.iodev.org/">Hetzner Availability Radar</a></li>
<li><a href="https://github.com/xdevplatform/xurl">xurl GitHub</a></li>
</ul>
<hr />
<h2>Final Setup Checklist</h2>
<ul>
<li> Hetzner VPS provisioned (8 GB RAM recommended)</li>
<li> Docker + Tailscale installed</li>
<li> OpenClaw cloned on stable release tag</li>
<li> <code>.env</code> configured with strong tokens</li>
<li> <code>docker-compose.yml</code> customized</li>
<li> <code>openclaw.json</code> created with correct permissions (uid 1000, mode 600)</li>
<li> Image built with Chromium (<code>OPENCLAW_INSTALL_BROWSER=1</code>)</li>
<li> Gateway healthy (<code>/healthz</code> returns ok)</li>
<li> Tailscale serve configured (HTTPS access)</li>
<li> Device paired in Control UI</li>
<li> Telegram bot created and paired</li>
<li> Model provider authenticated (OpenAI Codex OAuth)</li>
<li> Fallback model configured (Gemini Flash)</li>
<li> SearXNG running and connected</li>
<li> TTS configured (Microsoft, free)</li>
<li> xurl authenticated for X/Twitter</li>
<li> SSH keys + git config mounted as volumes</li>
<li> Workspace connected to GitHub</li>
<li> Google Workspace authenticated (Calendar, Gmail, Drive, Sheets, Docs)</li>
<li> Security audit clean (0 critical)</li>
</ul>
<hr />
<p><em>Built from real experience setting up OpenClaw from scratch. Every error in this guide is one I actually hit and solved.</em></p>
]]></content:encoded></item></channel></rss>