https://github.com/eratchev/openclaw-deploy
Hardened single-VPS deployment of OpenClaw AI assistant — Docker Compose, Caddy TLS, Redis, Python execution guardrail, automated S3 backups.
https://github.com/eratchev/openclaw-deploy
ai-assistant docker-compose llm openclaw self-hosted telegram-bot whatsapp
Last synced: 10 days ago
JSON representation
Hardened single-VPS deployment of OpenClaw AI assistant — Docker Compose, Caddy TLS, Redis, Python execution guardrail, automated S3 backups.
- Host: GitHub
- URL: https://github.com/eratchev/openclaw-deploy
- Owner: eratchev
- Created: 2026-03-03T01:00:57.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-05-07T15:42:19.000Z (about 2 months ago)
- Last Synced: 2026-05-07T17:35:45.614Z (about 2 months ago)
- Topics: ai-assistant, docker-compose, llm, openclaw, self-hosted, telegram-bot, whatsapp
- Language: Python
- Size: 770 KB
- Stars: 1
- Watchers: 0
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Security: docs/security-checklist.md
Awesome Lists containing this project
- awesome-openclaw-resources - OpenClaw Deploy - Hardened single-VPS deployment with Docker Compose, Caddy TLS, Redis, Python guardrails, and automated S3 backups. (Open Source Projects / Hosting Platforms)
README
# openclaw-deploy
> Hardened single-VPS deployment of [OpenClaw](https://github.com/openclaw/openclaw) with execution guardrails. Personal assistant + publishable open-source template.
>
> **Repo:** https://github.com/eratchev/openclaw-deploy
## What This Is
One VPS. One Docker Compose. Hardened container, log-driven execution guardrail, Redis session store, TLS via Caddy. Telegram, WhatsApp, Google Calendar, and Brave Search are all optional integrations — the base stack runs without any of them.
Out of the box you get:
- TLS termination via Caddy with automatic Let's Encrypt certificates
- OpenClaw Gateway running as a non-root user with all Linux capabilities dropped, read-only filesystem, and resource limits enforced
- Redis session store isolated to an internal Docker network — unreachable from the internet
- A Python execution guardrail that kills runaway LLM sessions before they burn tokens or abuse tools
- VPS hardening via `scripts/provision.sh` (UFW, SSH key-only auth, Fail2ban, unattended security upgrades)
- Automated daily backups of the `/data` volume to Hetzner Object Storage with configurable retention
- A `Makefile` with commands for bring-up, teardown, logs, backup, and upgrade
## What This Is NOT
- Not multi-tenant
- Not Kubernetes
- Not a managed SaaS
- Not hardened for enterprise (see threat model)
## Prerequisites
- A VPS (Hetzner CX22 or equivalent, ~$5-7/month)
- Ubuntu 24.04 LTS
- A domain name pointing to the VPS
- OpenClaw already set up locally (you need to onboard channels before deploying)
- Docker + Docker Compose (installed by provision.sh)
- **SSH public key loaded on the VPS** — `scripts/provision.sh` disables password authentication. Run `ssh-copy-id user@` before provisioning or you will be locked out.
## Quickstart
**Prerequisites:**
- A VPS running Ubuntu 24.04 (Hetzner CX22 ~$5/mo works well)
- A domain pointing at the VPS IP
- SSH key access: `ssh-copy-id user@`
- A Telegram bot token from [@BotFather](https://t.me/BotFather)
- An [Anthropic API key](https://console.anthropic.com)
**Deploy:**
```bash
git clone https://github.com/eratchev/openclaw-deploy.git
cd openclaw-deploy
make deploy HOST=user@
```
The wizard provisions the VPS, configures everything interactively, and starts the stack. When it finishes, send a message to your bot.
**Add WhatsApp (optional):**
```bash
make pair-whatsapp
```
Renders a QR code in your terminal. Scan with WhatsApp on your phone.
**Check health:**
```bash
make doctor
```
**Deploy code changes** (after every `git push`):
```bash
make push
```
Pulls latest code on the VPS, rebuilds running service containers, updates CLI binaries, and deploys workspace files. Non-interactive — safe to run at any time.
## Security Model
This deployment shifts OpenClaw's execution risk to containment. OpenClaw can execute arbitrary code via skills and tools — the hardening around it prevents that from compromising the host.
See [docs/threat-model.md](docs/threat-model.md) for the full threat model including known gaps. Phase 1 ships with outbound egress unrestricted — read it before deploying.
## Execution Guardrails
A Python watchdog runs inside the container and kills OpenClaw if sessions exceed configurable limits (tool calls, LLM calls, session time, idle timeout). Because OpenClaw has no per-session abort API, a violation kills all sessions — the container restarts automatically.
See [docs/execution-guardrails.md](docs/execution-guardrails.md) for limits and tuning.
## Backups
The `/data` volume (OpenClaw config, credentials, session history) is backed up daily to Hetzner Object Storage. Backups older than `BACKUP_RETAIN_DAYS` (default: 7) are pruned automatically.
**Setup (one-time):**
1. Create a bucket in [Hetzner Object Storage](https://console.hetzner.com) and generate S3 credentials
2. Add the `BACKUP_S3_*` vars to `.env` (see `.env.example`)
3. Install the cron job: `sudo bash scripts/install-backup-cron.sh`
**Manual backup:** `make backup-remote`
Backups run daily at 03:00 UTC. Logs go to `/var/log/openclaw-backup.log`.
## Google Calendar Integration *(optional)*
OpenClaw can read and write your Google Calendar via an MCP proxy that runs on the internal Docker network. All writes go through a policy engine (conflict detection, business hours, rate limits) before touching the Google API.
**One-time setup (local machine):**
```bash
make setup-gcal CLIENT_SECRET=path/to/client_secret.json
```
This generates a Fernet encryption key, runs the Google OAuth browser flow, encrypts the token, copies it to the VPS, updates `.env`, and restarts the calendar-proxy. Requires `client_secret.json` from Google Cloud Console (see below).
**Multiple accounts:** Add a second account with `ACCOUNT=`:
```bash
make setup-gcal CLIENT_SECRET=path/to/client_secret.json ACCOUNT=jobs
```
The agent selects an account via `gcal --account jobs list`. Omitting `--account` uses the default (first entry in `GCAL_ACCOUNTS`).
**Additional `.env` vars (add via `make deploy` or manually):**
```bash
GCAL_USER_TIMEZONE=America/Los_Angeles # your local timezone
GCAL_ALLOWED_CALENDARS=primary # comma-separated calendar IDs
GCAL_WORK_CALENDAR_ID= # optional — requires confirmation for any write
```
Then `make up-calendar` to start the base stack **plus** the calendar-proxy container. (Plain `make up` skips `calendar-proxy` — it only starts when explicitly requested via the `calendar` profile.)
**One-time exec approvals setup** (run after first deploy):
```bash
make setup-approvals
```
This configures the `gcal` and `date` binaries on the exec allowlist so the agent can call them without interactive approval.
See [docs/calendar-proxy.md](docs/calendar-proxy.md) for tuning, health checks, and troubleshooting.
## Gmail Integration *(optional)*
OpenClaw can read, search, and reply to Gmail, and proactively notifies you via Telegram when important emails arrive (scored by Claude AI).
**One-time setup (local machine):**
```bash
make setup-gmail CLIENT_SECRET=path/to/client_secret.json
```
This generates a Fernet encryption key, runs the Google OAuth browser flow (requesting `gmail.readonly`, `gmail.send`, `gmail.modify`), encrypts the token, copies it to the VPS, updates `.env`, registers the `gmail` CLI on the exec approvals allowlist, and starts the service.
Requires `client_secret.json` from Google Cloud Console (same project as Calendar if using both — see below).
**Multiple accounts:** Add a second account with `ACCOUNT=`:
```bash
make setup-gmail CLIENT_SECRET=path/to/client_secret.json ACCOUNT=jobs
```
The agent selects an account via `gmail --account jobs list`. Omitting `--account` uses the default (first entry in `GMAIL_ACCOUNTS`).
**Start:**
```bash
make up-mail
```
**Available agent commands:**
| Command | Description |
|---|---|
| `gmail list` | Show unread inbox (up to 10) |
| `gmail get --thread-id ID` | Fetch full thread |
| `gmail search --query "..."` | Gmail query syntax |
| `gmail reply --thread-id ID --message-id ID --body "..."` | Reply to thread |
| `gmail send --to EMAIL --subject "..." --body "..." --confirmed` | Send new email |
| `gmail mark-read --message-id ID` | Mark as read |
| `gmail --account jobs list` | Use a non-default account |
**Proactive notifications:**
When new emails arrive, the agent scores them for importance using Claude and sends a Telegram summary for anything scoring ≥ 7 (configurable via `GMAIL_IMPORTANCE_THRESHOLD`). Requires `ALERT_TELEGRAM_CHAT_ID` in `.env`.
**Re-auth (if token expires):**
```bash
make setup-gmail CLIENT_SECRET=path/to/client_secret.json
```
Safe to re-run — generates a fresh key and token.
See [docs/superpowers/specs/2026-03-13-gmail-integration-design.md](docs/superpowers/specs/2026-03-13-gmail-integration-design.md) for architecture details.
### Getting `client_secret.json`
Both Calendar and Gmail integrations use the same Google Cloud OAuth flow:
1. Go to [console.cloud.google.com](https://console.cloud.google.com) and create a project (or reuse one).
2. Enable the API(s) you need: **APIs & Services → Library**
- For Calendar: enable **Google Calendar API**
- For Gmail: enable **Gmail API**
3. Create credentials: **APIs & Services → Credentials → Create Credentials → OAuth client ID**
- Application type: **Desktop app**
- Name: anything (e.g. `openclaw`)
4. Download the JSON — that is your `client_secret.json`.
5. Add your Google account as a test user: **OAuth consent screen → Test users → Add**.
You can reuse the same project and the same `client_secret.json` for both Calendar and Gmail.
### Voice Transcription *(optional)*
Automatically transcribes Telegram voice notes via OpenAI Whisper so you can speak to OpenClaw hands-free.
**Setup:**
1. Add to `.env`:
```bash
OPENAI_API_KEY=sk-...
TELEGRAM_TOKEN= # same token as channels.telegram.botToken in openclaw.json
```
2. Configure OpenClaw for webhook mode (required — voice-proxy intercepts incoming webhook POSTs; long-polling cannot be intercepted):
```bash
# Set secret before URL or validation fails
docker compose exec openclaw openclaw config set channels.telegram.webhookSecret
docker compose exec openclaw openclaw config set channels.telegram.webhookUrl https:///telegram-webhook
docker compose exec openclaw openclaw config set channels.telegram.webhookHost 0.0.0.0
```
3. `make up-voice`
4. Send a voice note to your bot — it should reply as if you typed the text.
**Cost:** ~$0.006/min (OpenAI Whisper). Negligible for personal use.
**Rate limit:** 10 voice messages/minute per chat (configurable via `VOICE_RATE_LIMIT_PER_MIN`).
**Troubleshooting:** Check `docker compose logs voice-proxy`. Each voice request logs `status=ok|error|no_api_key|rate_limited|size_exceeded`.
## Brave Search *(optional)*
The agent can search the web using the Brave Search API. Get a free API key at [brave.com/search/api](https://brave.com/search/api), then configure it in the running container:
```bash
docker compose exec openclaw openclaw config set tools.web.search.apiKey
docker compose exec openclaw openclaw config set tools.web.search.provider brave
docker compose exec openclaw openclaw config set tools.web.search.maxResults 5
docker compose restart openclaw
```
## OpenClaw Skills *(optional)*
OpenClaw ships with bundled skills that unlock additional capabilities. Some require external CLI binaries to be installed in the container. Install them in one step:
```bash
make setup-skills # all supported skills
make setup-skills SKILLS="github session-logs" # specific skills only
```
Supported skills and their binaries:
| Skill | Binary | Works out of the box? |
|---|---|---|
| `session-logs` | `jq`, `rg` | ✅ Yes |
| `github` | `gh` | Needs `gh auth login` |
| `spotify-player` | `spogo` | Needs cookie auth (see below) |
| `summarize` | `summarize` | ❌ Not available on Linux |
---
### GitHub skill
After installing, authenticate `gh` inside the container:
```bash
make ssh # or: ssh user@your-vps
sudo docker compose exec -it openclaw gh auth login
```
Follow the prompts. Once authenticated, ask the bot things like "do I have any open PRs?" or "what's the status of my latest CI run?"
---
### Spotify skill
Requires a Spotify Premium account. Uses [spogo](https://github.com/steipete/spogo) — a statically compiled CLI that controls Spotify via the Web API using a browser session cookie. Works with any sign-in method (Google, Apple, Yahoo, email).
**Install:**
```bash
make setup-skills SKILLS=spotify-player
```
**Authenticate:**
1. Open [open.spotify.com](https://open.spotify.com) in Chrome/Firefox while logged in to Spotify
2. Open DevTools → Application → Cookies → `open.spotify.com` → copy the values of **`sp_dc`** and **`sp_t`**
3. Run this on your local machine (substituting the cookie values):
```bash
SP_DC="your-sp_dc-value"
SP_T="your-sp_t-value"
python3 -c "
import json
cookies = [
{'name':'sp_dc','value':'$SP_DC','domain':'.spotify.com','path':'/','expires':'2027-12-31T23:59:59Z','secure':True,'http_only':True},
{'name':'sp_t','value':'$SP_T','domain':'.spotify.com','path':'/','expires':'2027-12-31T23:59:59Z','secure':True,'http_only':False}
]
print(json.dumps(cookies, indent=2))
" > /tmp/spogo_cookies.json
scp /tmp/spogo_cookies.json user@your-vps:/tmp/spogo_cookies.json
ssh user@your-vps "
sudo docker compose -f ~/openclaw-deploy/docker-compose.yml cp \
/tmp/spogo_cookies.json openclaw:/home/node/.openclaw/spogo/cookies/default.json
rm -f /tmp/spogo_cookies.json
"
rm -f /tmp/spogo_cookies.json
```
Verify: `make doctor` or:
```bash
ssh user@your-vps \
"sudo docker compose -f ~/openclaw-deploy/docker-compose.yml exec -T openclaw \
/home/node/.openclaw/bin/spogo auth status"
```
Credentials are stored in the persistent volume. Re-run the auth step if the cookies expire (typically annually).
After that, ask the bot: "play some jazz", "skip this song", "what's playing?"
---
## Agent Workspace *(optional)*
The `workspace/` directory contains the agent's instruction files. These are copied into the container at runtime and control agent behaviour:
- `AGENTS.md` — injected into every system prompt (always active, all sessions)
- `SOUL.md` — agent identity and personality
- `POLICY.md` — safety rules, authority model, guardrails
- `OPERATIONS.md` — execution model and tool usage
- `USER.md` — who the agent is helping (preferences, context)
- `COMMANDS.md` — global commands available in all sessions including groups
- `MEMORY_GUIDE.md` — memory instructions and tool quick-references (operator-owned, redeployed on every `make deploy`)
- `MEMORY.md` — agent-owned long-term memory, loaded in direct/DM sessions only; never overwritten after first deploy
Edit the files locally, then deploy:
```bash
make deploy-workspace
```
> **Telegram groups:** The bot only responds when @mentioned (e.g. `@YourBotName ai update`). This is controlled by `channels.telegram.groupPolicy: open` — change it to `disabled` to block group messages entirely, or configure per-group `requireMention: false` to allow unprefixed commands.
## Upgrading
`make backup-remote && make update`
See [docs/upgrade-path.md](docs/upgrade-path.md).
## Pre-launch Checklist
See [docs/security-checklist.md](docs/security-checklist.md). Run through it before going live.
## Troubleshooting
### Guardrail exits immediately / `pairing required`
**Symptom:** `[entrypoint] guardrail exited (code 0), restarting in 5s...` loops indefinitely. Running `docker compose exec openclaw openclaw logs --json` returns `gateway closed (1008): pairing required`.
**Cause:** OpenClaw config was created on macOS. The gateway pins the CLI device to `darwin` and rejects connections from the Linux container.
**Fix:**
```bash
docker compose exec openclaw node -e "
const fs = require('fs');
const p = '/home/node/.openclaw/devices/paired.json';
const d = JSON.parse(fs.readFileSync(p, 'utf8'));
for (const id of Object.keys(d)) {
if (d[id].clientId === 'cli') d[id].platform = 'linux';
}
fs.writeFileSync(p, JSON.stringify(d, null, 2));
fs.writeFileSync('/home/node/.openclaw/devices/pending.json', '{}');
console.log('done');
"
docker compose restart openclaw
```
### Caddy fails with `unrecognized global option: reverse_proxy`
**Cause:** The `DOMAIN` variable is not visible to Caddy, so `{$DOMAIN}` expands to an empty string and Caddy interprets the site block as the global options block.
**Fix:** Ensure `env_file: - .env` is present under the `caddy` service in `docker-compose.yml` (already included in this repo).
### OpenClaw reports `Missing config`
**Cause:** The `/data` volume is empty. On a fresh deploy this is handled automatically — the entrypoint bootstraps `openclaw.json` from `.env` on first start.
**Fix:** If bootstrap did not run (e.g. the container started before `.env` was written), ensure `.env` has `TELEGRAM_TOKEN` and `DOMAIN` set, then restart:
```bash
make doctor # confirms .env vars
sudo docker compose restart openclaw
```
### Bootstrap fails with `config set` error on first start
**Symptom:** `[entrypoint] ERROR: TELEGRAM_TOKEN is not set`
**Cause:** `.env` is missing a required variable. The bootstrap runs before the gateway and fails fast.
**Fix:** Verify `.env` has `TELEGRAM_TOKEN`, `DOMAIN`, and `ANTHROPIC_API_KEY` set, then restart:
```bash
make doctor # shows which vars are missing
sudo docker compose restart openclaw
```