Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ Every stack starts from the same [standard setup](documentation/standard-setup.m
| **Nanote** | Note-taking app that stores notes as Markdown files in folders. | [Details](services/nanote) |
| **NextExplorer** | Web file manager for browsing, uploading, and editing files in one folder on your server. | [Details](services/next-explorer) |
| **Open WebUI** | Chat interface for AI models, through Ollama or OpenAI-compatible APIs. | [Details](services/open-webui) |
| **OpenClaw** | Personal AI assistant that connects agents to your chat apps. | [Details](services/openclaw) |
| **Paperless-ngx** | Document management system that turns scans and files into a searchable archive. | [Details](services/paperless) |
| **Pingvin Share** | Archived. File sharing platform with links that can expire. | [Details](services/pingvin-share) |
| **Radicale** | CalDAV and CardDAV server that syncs calendars, to-do lists, and contacts. | [Details](services/radicale) |
Expand Down
30 changes: 30 additions & 0 deletions services/openclaw/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#URL=https://github.com/tailscale-dev/ScaleTail
# To run a second copy, copy the folder under another name and change SERVICE.

# Service Configuration
# Service name (e.g., adguard). Used as hostname in Tailscale and for container naming (app-${SERVICE}).
SERVICE=openclaw
# Docker image URL from container registry (e.g., adguard/adguardhome:latest).
IMAGE_URL=ghcr.io/openclaw/openclaw:latest

# Network Configuration
# Port to expose to local network. Uncomment the "ports:" section in compose.yaml to enable.
SERVICEPORT=18789
# Preferred DNS server for Tailscale. Uncomment the "dns:" section in compose.yaml to enable.
DNS_SERVER=9.9.9.9

# Tailscale Configuration
# Auth key from https://console.tailscale.com/admin/settings/keys. See: https://tailscale.com/kb/1085/auth-keys#generate-an-auth-key for instructions.
TS_AUTHKEY=

# Time Zone setting for containers. See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
TZ=Europe/Amsterdam

# Any Container environment variables are declared below. See https://docs.docker.com/compose/how-tos/environment-variables/

# Required: the token that signs in to the OpenClaw web interface. Generate it with: openssl rand -hex 32
OPENCLAW_GATEWAY_TOKEN=

# Optional: model provider keys. Also uncomment the matching line in compose.yaml.
#OPENAI_API_KEY=
#ANTHROPIC_API_KEY=
51 changes: 51 additions & 0 deletions services/openclaw/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# OpenClaw

[OpenClaw](https://openclaw.ai) is a self-hosted personal AI assistant. Its Gateway runs the agents, connects chat channels, and serves the Control UI web interface.

This stack runs OpenClaw with a Tailscale sidecar, as described in [the standard setup](../../documentation/standard-setup.md).

## At a glance

| Item | Value |
| ------------- | ------------------------------------ |
| Web interface | `https://openclaw.<tailnet>.ts.net` |
| Service port | `18789` |
| Image | `ghcr.io/openclaw/openclaw:latest` |
| Data | `./openclaw-data/` |

## Before you start

- Set `OPENCLAW_GATEWAY_TOKEN` in `.env`. It is the token that signs in to the web interface. Generate it with `openssl rand -hex 32`. Compose stops with an error until it is set.
- Create the data folder before the first start, so that it is owned by you and not by root:

```sh
mkdir -p openclaw-data/state
```

The image runs as user `node` (uid 1000). If your user has another uid, run `sudo chown -R 1000:1000 openclaw-data` too. Without this, Docker creates the folder as root and the application restarts in a loop, logging `GatewayLockError: failed to acquire gateway state ownership`.

## Deviations from the standard setup

- **Gateway token.** OpenClaw refuses connections without authentication, so `OPENCLAW_GATEWAY_TOKEN` is required.
- **Sidecar instead of OpenClaw's own Tailscale mode.** OpenClaw can manage Tailscale Serve itself, but it runs the `tailscale` command on the host. The image contains no Tailscale, so `gateway.tailscale.mode` stays `off` and the sidecar provides Serve. For the same reason, signing in with the Tailscale identity (`gateway.auth.allowTailscale`) does not work, and you sign in with the token.
- **Trusted proxy and start without setup.** Tailscale Serve connects to the Gateway from `127.0.0.1`. Without `gateway.trustedProxies`, the Gateway answers `403 proxy_attribution_required`. The command therefore sets `gateway.trustedProxies` to `127.0.0.1` and `gateway.mode` to `local` each time the container starts, so the Gateway runs without `openclaw onboard` first. Any other process in the shared network namespace is trusted the same way, which is why the token stays required. It binds with `--bind lan`, as the upstream Docker setup does.
- **Hardening.** The application drops `NET_RAW` and `NET_ADMIN` and sets `no-new-privileges`, as upstream's `docker-compose.yml` does.
- **No CLI container.** Upstream's compose file has a separate `openclaw-cli` container. Run the CLI in the application container instead, for example `docker exec app-openclaw node dist/index.js <command>`.
- **Gateway port on the Tailnet.** The Gateway also listens on `18789` without HTTPS, reachable at the Tailscale IP address of the device, when your Tailnet policy allows it. It requires the token.

## First run

1. Open the web interface and enter the value of `OPENCLAW_GATEWAY_TOKEN` in the settings.
2. Each new browser connects as a new device, and the Gateway asks you to approve it once. List the request and approve it:

```sh
docker exec app-openclaw node dist/index.js devices list
docker exec app-openclaw node dist/index.js devices approve <request-id>
```

3. Add a model provider, for example with an API key, before you chat with an agent. See the [OpenClaw documentation](https://docs.openclaw.ai).

## Links

- [OpenClaw documentation](https://docs.openclaw.ai)
- [OpenClaw source code](https://github.com/openclaw/openclaw)
82 changes: 82 additions & 0 deletions services/openclaw/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
configs:
ts-serve:
content: |
{"TCP":{"443":{"HTTPS":true}},
"Web":{"$${TS_CERT_DOMAIN}:443":
{"Handlers":{"/":
{"Proxy":"http://127.0.0.1:18789"}}}},
"AllowFunnel":{"$${TS_CERT_DOMAIN}:443":false}}

services:
# Make sure you have updated/checked the .env file with the correct variables.
# Every variable used in this file must be defined there, unless it has a default such as ${VAR:-value}.
# Tailscale Sidecar Configuration
tailscale:
image: tailscale/tailscale:latest # Image to be used
container_name: tailscale-${SERVICE} # Name for local container management
hostname: ${SERVICE} # Name used within your Tailscale environment
environment:
- TS_AUTHKEY=${TS_AUTHKEY}
- TS_STATE_DIR=/var/lib/tailscale
- TS_SERVE_CONFIG=/config/serve.json # Tailscale Serve configuration to expose the web interface on your local Tailnet - remove this line if not required
- TS_USERSPACE=false
- TS_ENABLE_HEALTH_CHECK=true # Enable healthcheck endpoint: "/healthz"
- TS_LOCAL_ADDR_PORT=127.0.0.1:41234 # The <addr>:<port> for the healthz endpoint
#- TS_ACCEPT_DNS=true # Uncomment only if the service must resolve MagicDNS names - this replaces Docker DNS, so Compose service names no longer resolve
- TS_AUTH_ONCE=true
configs:
- source: ts-serve
target: /config/serve.json
volumes:
- ./config:/config # Config folder used to store Tailscale files - you may need to change the path
- ./ts/state:/var/lib/tailscale # Tailscale requirement - you may need to change the path
devices:
- /dev/net/tun:/dev/net/tun # Network configuration for Tailscale to work
cap_add:
- net_admin # Tailscale requirement
#ports:
# - 0.0.0.0:${SERVICEPORT}:${SERVICEPORT} # Binding the service port to the local network - may be removed if only exposure to your Tailnet is required
# If any DNS issues arise, use your preferred DNS provider by uncommenting the config below
#dns:
# - ${DNS_SERVER}
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:41234/healthz"] # Check Tailscale has a Tailnet IP and is operational
interval: 1m # How often to perform the check
timeout: 10s # Time to wait for the check to succeed
retries: 3 # Number of retries before marking as unhealthy
start_period: 10s # Time to wait before starting health checks
restart: always

# Application
application:
image: ${IMAGE_URL} # Image to be used
network_mode: service:tailscale # Sidecar configuration to route the service through Tailscale
container_name: app-${SERVICE} # Name for local container management
environment: # Variables are declared in .env file.
- TZ=${TZ}
- OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN:?Set OPENCLAW_GATEWAY_TOKEN in .env}
- OPENCLAW_GATEWAY_PORT=18789
# Model provider keys are optional. To use one, set it in .env and uncomment the matching line below.
#- OPENAI_API_KEY=${OPENAI_API_KEY}
#- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
# Tailscale Serve reaches the gateway from 127.0.0.1, so that address is a trusted proxy. Without it the gateway answers 403 proxy_attribution_required.
# gateway.mode=local lets the gateway start without running "openclaw onboard" first.
command:
- sh
- -c
- |
node dist/index.js config set --batch-json '[{"path":"gateway.mode","value":"local"},{"path":"gateway.trustedProxies","value":["127.0.0.1"]}]'
exec node dist/index.js gateway --bind lan --port 18789
init: true
cap_drop:
- NET_RAW
- NET_ADMIN
security_opt:
- no-new-privileges:true
volumes:
- ./${SERVICE}-data/state:/home/node/.openclaw # Settings, workspace and sessions - owned by uid 1000
depends_on:
tailscale:
condition: service_healthy
# Healthcheck: defined by the image (node dist/docker-healthcheck.js, which checks /healthz), so this file does not override it.
restart: always
Loading