Install and configure a fully operational Dockerized OpenClaw instance on macOS from scratch. Includes browser pairing, Discord channel setup, and optional G...
---
name: openclaw-docker-setup
description: Install and configure a fully operational Dockerized OpenClaw instance on macOS from scratch. Includes browser pairing, Discord channel setup, and optional Gmail/Google Drive integration. Use when user asks to "install openclaw docker", "set up dockerized openclaw", "openclaw in docker", or "isolated openclaw instance".
triggers:
- install openclaw docker
- set up dockerized openclaw
- openclaw in docker
- isolated openclaw instance
- dockerized openclaw
- run openclaw in docker
metadata: {"openclaw": {"os": ["darwin"]}}
---
# openclaw-docker-setup
Install a fully isolated, production-ready OpenClaw instance inside Docker on macOS.
One session, zero to running. All common pitfalls are handled inline.
**Supports multiple instances on the same machine.** Each instance gets a unique name and port.
**What you end up with:**
- A named container running and auto-restarting
- Persistent data via named Docker volumes (survives container recreation)
- Dashboard accessible at http://127.0.0.1:YOUR_PORT/
- Discord channel configured and responding
- (Optional) Gmail working via Himalaya (supports attachments); Google Drive via gog
---
## Step 0: Pick Your Instance Name and Port
Run this auto-detect script. It scans existing OpenClaw containers, finds the next free port, and suggests a name. Confirm or override.
```bash
# Auto-detect existing instances and suggest next available name+port
python3 - << 'AUTODETECT'
import subprocess, re
# Find all running openclaw containers
result = subprocess.run(
["docker", "ps", "-a", "--format", "{{.Names}} {{.Ports}}"],
capture_output=True, text=True
)
existing = {}
for line in result.stdout.strip().splitlines():
parts = line.split(' ')
name = parts[0]
ports = parts[1] if len(parts) > 1 else ""
if 'openclaw' in name.lower() or '18789' in ports:
m = re.search(r'0\.0\.0\.0:(\d+)->18789', ports)
port = int(m.group(1)) if m else None
existing[name] = port
# Find next free port starting from 19002
used_ports = set(p for p in existing.values() if p)
port = 19002
while port in used_ports:
port += 1
# Suggest name
count = len(existing) + 1
names = ["openclaw-main", "openclaw-work", "openclaw-demo", "openclaw-test", "openclaw-lab"]
suggested_name = names[min(count - 1, len(names) - 1)]
print("\n=== Existing OpenClaw instances ===")
if existing:
for n, p in existing.items():
print(f" {n} → port {p}")
else:
print(" (none found)")
print(f"\n=== Suggested for new instance ===")
print(f" INSTANCE={suggested_name}")
print(f" HOST_PORT={port}")
print(f"\nTo accept, run:")
print(f" export INSTANCE={suggested_name}")
print(f" export HOST_PORT={port}")
print(f"\nTo override, replace the values and run the export commands with your chosen values.")
AUTODETECT
```
Review the output, then set your variables:
```bash
# Accept suggestion (paste the export lines from the output above)
export INSTANCE=openclaw-main
export HOST_PORT=19002
# Or override with your own values:
export INSTANCE=openclaw-demo
export HOST_PORT=19003
```
**All subsequent commands in this guide use `$INSTANCE` and `$HOST_PORT`.** Keep this terminal session open, or re-export the variables if you open a new one.
**Multiple instances example:**
| Instance | Host port | Purpose |
|----------|-----------|---------|
| `openclaw-main` | 19002 | Primary personal assistant |
| `openclaw-demo` | 19003 | Public demo / lecture |
| `openclaw-work` | 19004 | Work projects |
Each instance has its own volumes (`$INSTANCE-data`, `$INSTANCE-home`) — data is fully isolated.
---
## Prerequisites
- macOS (Darwin) — Intel or Apple Silicon
- Docker Desktop installed and running (or Docker Engine + CLI)
- Claude Code CLI installed on the host if using a Claude Max/Pro subscription (`claude` command available). Skip if using a raw API key.
Verify Docker is running:
```bash
docker --version
docker ps
```
Both commands must succeed before continuing.
---
## Step 1: Pull the Image
The official image is on GitHub Container Registry (GHCR), **not Docker Hub**.
```bash
docker pull ghcr.io/openclaw/openclaw:latest
```
> **Pitfall:** `openclaw/openclaw` on Docker Hub does not exist. Always use `ghcr.io/openclaw/openclaw:latest`.
**Success:** Pull completes without error and `docker images | grep openclaw` shows the image.
---
## Step 2: Generate a Claude Setup Token
**Skip this step if you have a raw Anthropic API key** — you will pass it via `-e ANTHROPIC_API_KEY=sk-ant-api03-...` in Step 3 instead.
If you have a Claude Max or Pro subscription, generate a setup token on the host:
```bash
claude setup-token
```
Copy the token (format: `sk-ant-oat01-...`). You will paste it into the container in Step 5.
> **Pitfall:** This is a **setup token**, not an API key. The two are different. A setup token lets the container authenticate using your subscription. An API key charges per token.
---
## Step 3: Launch the Container
Use the `$INSTANCE` and `$HOST_PORT` variables you set in Step 0. Do not reduce the memory — 512 MB and 1024 MB are insufficient and cause crash loops.
```bash
docker run -d \
--name $INSTANCE \
--restart unless-stopped \
-p $HOST_PORT:18789 \
-m 2048m \
--cpus=2 \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt no-new-privileges \
-v ${INSTANCE}-data:/app/data \
-v ${INSTANCE}-home:/home/node \
-e NODE_OPTIONS="--max-old-space-size=1024" \
ghcr.io/openclaw/openclaw:latest
```
If using a raw API key instead of a setup token, add `-e ANTHROPIC_API_KEY=sk-ant-api03-...` before the image name.
Wait 10 seconds, then verify:
```bash
docker ps --filter name=$INSTANCE
```
**Success:** Status shows `Up X seconds` and the container is not restarting.
> **Pitfall — OOM crash loop:** If the container keeps restarting, check logs:
> `docker logs --tail 20 $INSTANCE`
> If you see `JavaScript heap out of memory`, the container needs `-m 2048m` AND `-e NODE_OPTIONS="--max-old-space-size=1024"`. Recreate with the full command above.
> **Pitfall — port conflict:** If the port is in use, you chose the wrong `HOST_PORT` in Step 0. Re-run the conflict check: `lsof -i :$HOST_PORT`. Pick a free port and relaunch.
---
## Step 4: Configure the Gateway
The gateway binds to `127.0.0.1` (loopback) inside the container by default. Docker port-forwarding sends traffic to the container's network interface, not its loopback. You must switch to LAN mode.
### 4a. Set bind to LAN
```bash
docker exec $INSTANCE node /app/openclaw.mjs config set gateway.bind lan
```
### 4b. Set allowed origins
Non-loopback bind requires explicitly allowed origins or the gateway refuses to start:
```bash
docker exec $INSTANCE node /app/openclaw.mjs config set \
gateway.controlUi.allowedOrigins '["http://127.0.0.1:$HOST_PORT"]' --json
```
### 4c. Restart to apply
```bash
docker restart $INSTANCE
```
Wait 10 seconds, then verify:
```bash
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:$HOST_PORT/
```
**Success:** Returns `200`.
> **Pitfall — crash after setting LAN bind:** If you set `gateway.bind lan` but forget the `allowedOrigins` step, the container crash-loops with `non-loopback Control UI requires gateway.controlUi.allowedOrigins`. Run step 4b, then restart.
---
## Step 5: Register Authentication
> **Important:** Do NOT paste your token directly in the command line — it would be stored in shell history. The command below prompts interactively.
```bash
docker exec -it $INSTANCE node /app/openclaw.mjs models auth paste-token --provider anthropic
```
When prompted, paste the setup token from Step 2 (or your API key if using one).
**Success:**
```
Updated ~/.openclaw/openclaw.json
Auth profile: anthropic:manual (anthropic/token)
```
> **Pitfall — `openclaw` command not found:** The CLI binary is NOT installed as a global command in this image. Always use `node /app/openclaw.mjs` inside the container:
> ```bash
> docker exec $INSTANCE node /app/openclaw.mjs <command>
> ```
> For interactive commands (pasting tokens, TTY prompts), add `-it`:
> ```bash
> docker exec -it $INSTANCE node /app/openclaw.mjs <command>
> ```
---
## Step 6: Verify Authentication and Gateway
```bash
# Check model auth
docker exec $INSTANCE node /app/openclaw.mjs models status
```
**Success:** Output includes `Providers w/ OAuth/tokens (1): anthropic (1)`
```bash
# Check gateway
docker exec $INSTANCE node /app/openclaw.mjs gateway status
```
**Success:** Output includes `RPC probe: ok`
---
## Step 7: Access the Dashboard and Pair Your Browser
### Get the gateway auth token
The CLI redacts secrets in output. Read the raw config file instead:
```bash
docker exec $INSTANCE cat /home/node/.openclaw/openclaw.json
```
Find `gateway.auth.token` in the output. Copy it.
### Open the dashboard
Open http://127.0.0.1:$HOST_PORT/ in your browser. Enter the gateway token to log in.
The browser will show "Pairing required." This is expected on first access.
### Approve the pairing request
```bash
docker exec $INSTANCE node /app/openclaw.mjs devices list
```
Find the pending request ID, then approve it:
```bash
docker exec $INSTANCE node /app/openclaw.mjs devices approve <REQUEST_ID>
```
Refresh the browser.
**Success:** Dashboard loads and shows the OpenClaw interface.
---
## Step 8: Configure Discord
### Create a Discord bot
1. Go to https://discord.com/developers/applications
2. Click **New Application** — name it (e.g. "OpenClaw Isolated")
3. Go to **Bot** → set a username → click **Reset Token** → copy the token
4. Under **Privileged Gateway Intents**, enable:
- **Message Content Intent** (required)
- **Server Members Intent** (recommended)
5. Go to **OAuth2 > URL Generator**:
- Scopes: `bot` AND `applications.commands` (both required)
- Bot Permissions: View Channels, Send Messages, Read Message History, Embed Links, Attach Files
6. Copy the generated URL, open it in a browser, and add the bot to your server
> **Pitfall — slash commands say "not authorized":** If you invite the bot without `applications.commands`, slash commands will not work. Re-authorize using this URL (replace `BOT_CLIENT_ID`):
> ```
> https://discord.com/oauth2/authorize?client_id=BOT_CLIENT_ID&scope=bot+applications.commands&permissions=274877991936
> ```
### Collect Discord IDs
Enable **Developer Mode**: Discord → User Settings → Advanced → Developer Mode.
- Right-click your server icon → **Copy Server ID**
- Right-click your own avatar → **Copy User ID**
### Configure Discord in the container
Replace `YOUR_DISCORD_BOT_TOKEN`, `YOUR_SERVER_ID`, and user IDs with real values.
```bash
# Enable Discord
docker exec $INSTANCE node /app/openclaw.mjs config set \
channels.discord.enabled true --json
# Set bot token
docker exec $INSTANCE node /app/openclaw.mjs config set \
channels.discord.token '"YOUR_DISCORD_BOT_TOKEN"' --json
# Set access policy to allowlist
docker exec $INSTANCE node /app/openclaw.mjs config set \
channels.discord.groupPolicy '"allowlist"' --json
# Configure the guild with authorized users
docker exec $INSTANCE node /app/openclaw.mjs config set \
channels.discord.guilds \
'{"YOUR_SERVER_ID":{"requireMention":false,"users":["USER_ID_1","USER_ID_2"]}}' \
--json
# Restart to apply
docker restart $INSTANCE
```
### Verify Discord is connected
```bash
docker exec $INSTANCE node /app/openclaw.mjs channels status --probe
```
**Success:** Output includes `Discord default: enabled, configured, running, ... works`
Confirm users resolved:
```bash
docker logs --tail 10 $INSTANCE
```
Look for: `[discord] channel users resolved: USER_ID_1→USER_ID_1`
> **Pitfall — guilds config fails with "expected record, received array":** Always set the full `guilds` object at once. Setting individual guild keys fails:
> ```bash
> # WRONG
> docker exec $INSTANCE node /app/openclaw.mjs config set \
> 'channels.discord.guilds.SERVER_ID' '{"requireMention":false}' --json
>
> # CORRECT
> docker exec $INSTANCE node /app/openclaw.mjs config set \
> channels.discord.guilds '{"SERVER_ID":{"requireMention":false,"users":["UID"]}}' --json
> ```
> **Pitfall — adding users later:** `config set guilds` replaces the entire object. Always include ALL existing user IDs when adding new ones.
---
## Optional: Gmail Integration
**Skip this section if you do not need Gmail.** The rest of the setup works without it.
See `references/gmail-setup.md` for the complete guide (uses Himalaya — the only reliable option for attachment download).
The key insight: OAuth browser auth does NOT work inside Docker because the callback URL points to localhost inside the container, which the host browser cannot reach. The solution is to authenticate on the host Mac first, then copy tokens into the container.
---
## Optional: Google Drive Integration
**Skip this section if you do not need Google Drive.** Google Drive uses gog (gogcli) — independent of the Gmail/Himalaya setup. You can set up Drive without setting up Gmail.
See `references/google-drive-setup.md` for the complete guide.
---
## Maintenance Reference
### Daily operations
```bash
# Stop the container
docker stop $INSTANCE
# Start the container
docker start $INSTANCE
# View logs
docker logs --tail 50 $INSTANCE
# Monitor resource usage
docker stats --no-stream $INSTANCE
```
### Update OpenClaw to a new version
```bash
docker pull ghcr.io/openclaw/openclaw:latest
docker stop $INSTANCE
docker rm $INSTANCE
# Re-run the same launch command from Step 3
# Named volumes retain all config, auth, and workspace data
docker run -d \
--name $INSTANCE \
--restart unless-stopped \
-p $HOST_PORT:18789 \
-m 2048m \
--cpus=2 \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt no-new-privileges \
-v ${INSTANCE}-data:/app/data \
-v ${INSTANCE}-home:/home/node \
-e NODE_OPTIONS="--max-old-space-size=1024" \
ghcr.io/openclaw/openclaw:latest
```
### Transfer files
```bash
# Host to container
docker cp /path/to/local/file.txt $INSTANCE:/home/node/.openclaw/workspace/
# Container to host
docker cp $INSTANCE:/home/node/.openclaw/workspace/file.txt ~/Desktop/
# Backup entire .openclaw directory
docker exec $INSTANCE tar -czf - -C /home/node .openclaw \
> ~/Desktop/${INSTANCE}-backup.tar.gz
# Restore from backup
cat ~/Desktop/${INSTANCE}-backup.tar.gz | \
docker exec -i $INSTANCE tar -xzf - -C /home/node
```
### Completely remove everything
```bash
docker rm -f $INSTANCE
docker volume rm ${INSTANCE}-data ${INSTANCE}-home
```
**Warning:** This permanently deletes all config, auth, and workspace data. Backup first.
---
## Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| `pull access denied for openclaw/openclaw` | Wrong image registry | Use `ghcr.io/openclaw/openclaw:latest` |
| Container keeps restarting | OOM crash | Use `-m 2048m` + `-e NODE_OPTIONS="--max-old-space-size=1024"` |
| `curl 127.0.0.1:$HOST_PORT` returns 000 or connection refused | Gateway on loopback | Set `gateway.bind lan` + `allowedOrigins`, restart |
| Container crash-loops after setting LAN bind | Missing `allowedOrigins` | Set `gateway.controlUi.allowedOrigins`, restart |
| `exec: "openclaw": executable file not found` | No global CLI binary | Use `node /app/openclaw.mjs` |
| Dashboard shows "Pairing required" | Browser not approved | `devices list` then `devices approve <ID>` |
| `config get gateway.auth.token` returns `__OPENCLAW_REDACTED__` | CLI redacts secrets | `cat /home/node/.openclaw/openclaw.json` |
| Discord slash commands say "not authorized" | Missing `applications.commands` scope or user not in allowlist | Re-authorize bot; check `guilds` config |
| Data gone after container recreation | Data on ephemeral overlay | Mount `/home/node` as named volume (the launch command above already does this) |
For full pitfall details, see `references/pitfalls.md`.
---
## Container Reference
| Setting | Value |
|---------|-------|
| Container name | `$INSTANCE` |
| Image | `ghcr.io/openclaw/openclaw:latest` |
| Host port | `19002` |
| Container port | `18789` |
| Dashboard URL | http://127.0.0.1:$HOST_PORT/ |
| Memory limit | 2048 MB |
| CPU limit | 1 core |
| Node.js heap | 1024 MB |
| Restart policy | `unless-stopped` |
| CLI prefix inside container | `node /app/openclaw.mjs` |
| Volume | Mounted at | Contains |
|--------|-----------|----------|
| `${INSTANCE}-home` | `/home/node` | Config, auth, workspace |
| `${INSTANCE}-data` | `/app/data` | App-level data |
---
## Do You Need to Be at Your Mac?
**Short answer: Yes for initial setup. No for ongoing use.**
### Why initial setup requires your Mac
Three steps require direct Mac access (physical or SSH with port forwarding):
| Step | Why Mac access is needed |
|------|--------------------------|
| **Step 2: Claude setup token** | `claude setup-token` opens a browser auth flow on `localhost`. Cannot be done remotely without a browser on the Mac. Skip if using a raw API key — that can be set from anywhere. |
| **Step 4: Browser pairing** | The OpenClaw dashboard runs at `http://127.0.0.1:$HOST_PORT/` — only reachable from the Mac. You must open a browser there to pair. |
| **Optional Gmail/Drive OAuth** | Google OAuth callback points to `localhost` on the Mac. Must authenticate from a browser running on the Mac. |
### Remote setup is possible via SSH port forwarding
If you SSH into your Mac from another machine, you can forward the container port to your local browser:
```bash
# From your remote machine — forward Mac's port 19002 to your local 19002
ssh -L 19002:localhost:19002 your-mac.local
# Then open http://127.0.0.1:19002/ in your local browser
```
This lets you complete the browser pairing step remotely.
### After setup is complete
Once the container is running and paired, you do **not** need to be at your Mac. OpenClaw runs as a background service (`--restart unless-stopped`). You interact with it entirely through your configured channel (Discord, Telegram, etc.) — from your phone, any browser, anywhere.
---
## Configuration
No persistent configuration required. All settings are chosen interactively in Step 0 and set as shell variables (`$INSTANCE`, `$HOST_PORT`).
**Optional integrations require additional setup:**
| Integration | Guide | Requires |
|-------------|-------|---------|
| Gmail (email + attachments) | `references/gmail-setup.md` | Gmail App Password |
| Google Drive / Docs / Sheets / Calendar | `references/google-drive-setup.md` | Google Cloud OAuth credentials |
**System dependencies:**
| Dependency | Purpose | Check |
|------------|---------|-------|
| Docker Desktop | Container runtime | `docker --version` |
| Python 3 | Auto-detect script in Step 0 | `python3 --version` |
| Claude Code CLI (`claude`) | Generate setup token (Claude Max/Pro only) | `claude --version` |
don't have the plugin yet? install it then click "run inline in claude" again.
set up a production-ready, fully isolated openclaw instance inside docker on macos. zero to running in one session. handles all common pitfalls inline. supports multiple instances on the same machine, each with unique name, port, and persistent volumes. use this when the user asks to install openclaw docker, set up dockerized openclaw, run openclaw in docker, or create an isolated openclaw instance.
docker environment:
docker --version && docker ps)claude authentication (choose one):
claude --version works) plus valid claude max/pro subscription. used to generate setup tokens on the host.sk-ant-api03-*). requires no setup token generation.external services (optional):
system dependencies:
step 0: pick instance name and port
run the auto-detect script to scan existing openclaw containers, find the next free port, and suggest a name:
python3 - << 'AUTODETECT'
import subprocess, re
result = subprocess.run(
["docker", "ps", "-a", "--format", "{{.Names}}\t{{.Ports}}"],
capture_output=True, text=True
)
existing = {}
for line in result.stdout.strip().splitlines():
parts = line.split('\t')
name = parts[0]
ports = parts[1] if len(parts) > 1 else ""
if 'openclaw' in name.lower() or '18789' in ports:
m = re.search(r'0\.0\.0\.0:(\d+)->18789', ports)
port = int(m.group(1)) if m else None
existing[name] = port
used_ports = set(p for p in existing.values() if p)
port = 19002
while port in used_ports:
port += 1
count = len(existing) + 1
names = ["openclaw-main", "openclaw-work", "openclaw-demo", "openclaw-test", "openclaw-lab"]
suggested_name = names[min(count - 1, len(names) - 1)]
print("\n=== existing openclaw instances ===")
if existing:
for n, p in existing.items():
print(f" {n} -> port {p}")
else:
print(" (none found)")
print(f"\n=== suggested for new instance ===")
print(f" INSTANCE={suggested_name}")
print(f" HOST_PORT={port}")
print(f"\nto accept, run:")
print(f" export INSTANCE={suggested_name}")
print(f" export HOST_PORT={port}")
AUTODETECT
export the variables based on output (accept suggestion or override):
export INSTANCE=openclaw-main
export HOST_PORT=19002
keep this terminal session open or re-export if opening a new one. all subsequent commands use $INSTANCE and $HOST_PORT.
input: user terminal, optional python3
output: shell variables $INSTANCE and $HOST_PORT set in current session
step 1: pull the image
pull from github container registry (not docker hub):
docker pull ghcr.io/openclaw/openclaw:latest
verify success:
docker images | grep openclaw
input: docker cli, network access to ghcr.io
output: local docker image ghcr.io/openclaw/openclaw:latest present on host
step 2: generate claude setup token (or prepare api key)
if using claude max/pro subscription: on the host mac, generate a setup token:
claude setup-token
copy the token (format: sk-ant-oat01-*). you will paste it into the container in step 5.
if using raw api key: obtain your anthropic api key (format: sk-ant-api03-*) and skip the paste in step 5. instead, pass it via environment variable in step 3.
input: host machine, claude code cli (if subscription-based), or existing anthropic api key output: setup token or api key string ready for step 3/5
step 3: launch the container
run the container with memory and cpu limits (do not reduce memory; 512 mb and 1024 mb cause crash loops):
docker run -d \
--name $INSTANCE \
--restart unless-stopped \
-p $HOST_PORT:18789 \
-m 2048m \
--cpus=2 \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt no-new-privileges \
-v ${INSTANCE}-data:/app/data \
-v ${INSTANCE}-home:/home/node \
-e NODE_OPTIONS="--max-old-space-size=1024" \
ghcr.io/openclaw/openclaw:latest
if using a raw api key instead of setup token, add before the image name:
-e ANTHROPIC_API_KEY=sk-ant-api03-YOUR_KEY_HERE \
wait 10 seconds, then verify:
docker ps --filter name=$INSTANCE
status must show Up X seconds and not restarting.
input: docker daemon running, $INSTANCE and $HOST_PORT variables set
output: running docker container named $INSTANCE accessible on host port $HOST_PORT
step 4a: set gateway bind to lan
docker port forwarding sends traffic to the container network interface, not loopback. switch from loopback to lan:
docker exec $INSTANCE node /app/openclaw.mjs config set gateway.bind lan
input: running container $INSTANCE
output: gateway.bind config set to lan
step 4b: set allowed origins
non-loopback bind requires explicitly allowed origins or gateway fails to start:
docker exec $INSTANCE node /app/openclaw.mjs config set \
gateway.controlUi.allowedOrigins '["http://127.0.0.1:'"$HOST_PORT"'"]' --json
input: $HOST_PORT variable
output: gateway.controlUi.allowedOrigins config set
step 4c: restart container
docker restart $INSTANCE
wait 10 seconds, then verify:
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:$HOST_PORT/
must return 200.
input: running container $INSTANCE
output: gateway responding at http://127.0.0.1:$HOST_PORT/ with http 200
step 5: register authentication
do not paste token in command line (shell history). use interactive prompt:
docker exec -it $INSTANCE node /app/openclaw.mjs models auth paste-token --provider anthropic
when prompted, paste the setup token from step 2 (or api key if using one).
success output: Updated ~/.openclaw/openclaw.json and Auth profile: anthropic:manual (anthropic/token)
input: setup token (step 2) or api key, running container with -it flags for interactivity
output: anthropic auth profile stored in container config
step 6a: verify model auth
docker exec $INSTANCE node /app/openclaw.mjs models status
success: output includes Providers w/ OAuth/tokens (1): anthropic (1)
input: running container, valid auth from step 5 output: anthropic provider active and authenticated
step 6b: verify gateway
docker exec $INSTANCE node /app/openclaw.mjs gateway status
success: output includes RPC probe: ok
input: running container, gateway configured in steps 4a-4c output: gateway status shows functional rpc probe
step 7a: get gateway auth token
cli redacts secrets. read raw config:
docker exec $INSTANCE cat /home/node/.openclaw/openclaw.json
find gateway.auth.token and copy it.
input: running container with gateway configured output: gateway auth token string (format: alphanumeric, ~64 chars)
step 7b: open dashboard and check pairing
open http://127.0.0.1:$HOST_PORT/ in browser. enter the gateway token to log in. dashboard shows "Pairing required" on first access (expected).
input: gateway token from step 7a, browser on macos host output: dashboard accessible, prompts for browser pairing
step 7c: approve browser pairing request
list pending pairing requests:
docker exec $INSTANCE node /app/openclaw.mjs devices list
find request id, then approve:
docker exec $INSTANCE node /app/openclaw.mjs devices approve REQUEST_ID_HERE
refresh browser. dashboard now fully loads.
input: running container, pending device request from browser output: browser device approved and paired; dashboard functional
step 8a: create and configure discord bot
go to https://discord.com/developers/applications. click new application. name it (e.g. "OpenClaw Isolated"). go to bot section, reset token, copy it. enable message content intent and server members intent. go to oauth2 > url generator: add scopes bot and applications.commands. add permissions: view channels, send messages, read message history, embed links, attach files. copy generated url and open in browser to add bot to server.
input: discord developer account, server admin access output: discord bot token, bot added to server with intents and permissions
step 8b: collect discord ids
enable developer mode in discord (user settings > advanced). right-click server icon for server id. right-click own avatar for user id.
input: discord client with developer mode enabled output: server id and user ids (numeric snowflakes)
step 8c: configure discord in container
set enabled flag:
docker exec $INSTANCE node /app/openclaw.mjs config set \
channels.discord.enabled true --json
set bot token (replace YOUR_DISCORD_BOT_TOKEN):
docker exec $INSTANCE node /app/openclaw.mjs config set \
channels.discord.token '"YOUR_DISCORD_BOT_TOKEN"' --json
set group policy to allowlist:
docker exec $INSTANCE node /app/openclaw.mjs config set \
channels.discord.groupPolicy '"allowlist"' --json
configure guild with authorized users (replace YOUR_SERVER_ID and USER_ID_1, USER_ID_2):
docker exec $INSTANCE node /app/openclaw.mjs config set \
channels.discord.guilds \
'{"YOUR_SERVER_ID":{"requireMention":false,"users":["USER_ID_1","USER_ID_2"]}}' \
--json
restart:
docker restart $INSTANCE
input: discord bot token, server id, authorized user ids from steps 8a/8b output: discord channel configured and enabled in openclaw
step 8d: verify discord connection
docker exec $INSTANCE node /app/openclaw.mjs channels status --probe
success: output includes Discord default: enabled, configured, running, ... works
check logs for user resolution:
docker logs --tail 10 $INSTANCE
look for: [discord] channel users resolved: USER_ID_1->USER_ID_1
input: running container with discord config from step 8c output: discord channel active and users resolved
optional: gmail integration
see references/gmail-setup.md. requires gmail app password. oauth browser auth does not work inside docker because callback points to localhost inside container. solution: authenticate on host mac first, copy tokens into container.
input: gmail credentials, host browser output: himalaya configured in container (email + attachments)
optional: google drive integration
see references/google-drive-setup.md. uses gog (gogcli), independent of gmail. requires google cloud oauth credentials.
input: google cloud oauth credentials, host browser output: google drive/docs/sheets/calendar accessible from openclaw
maintenance: daily operations
stop container:
docker stop $INSTANCE
start container:
docker start $INSTANCE
view logs (last 50 lines):
docker logs --tail 50 $INSTANCE
monitor resources:
docker stats --no-stream $INSTANCE
input: running or stopped container output: container state changed, logs displayed, or resource usage shown
maintenance: update to new version
pull latest image:
docker pull ghcr.io/openclaw/openclaw:latest
stop and remove old container:
docker stop $INSTANCE
docker rm $INSTANCE
relaunch with same command from step 3. named volumes retain all config, auth, and workspace data.
input: docker daemon, existing $INSTANCE container, original step 3 launch command
output: new container running with latest image; all data preserved via named volumes
maintenance: transfer files
host to container:
docker cp /path/to/local/file.txt $INSTANCE:/home/node/.openclaw/workspace/
container to host:
docker cp $INSTANCE:/home/node/.openclaw/workspace/file.txt ~/Desktop/
backup entire .openclaw directory:
docker exec $INSTANCE tar -czf - -C /home/node .openclaw \
> ~/Desktop/${INSTANCE}-backup.tar.gz
restore from backup:
cat ~/Desktop/${INSTANCE}-backup.tar.gz | \
docker exec -i $INSTANCE tar -xzf - -C /home/node
input: running container, host file paths output: files copied between host and container, or backup/restore complete
maintenance: remove everything
docker rm -f $INSTANCE
docker volume rm ${INSTANCE}-data ${INSTANCE}-home
warning: permanently deletes all config, auth, and workspace data. backup first.
input: container name $INSTANCE, volume names
output: container and volumes removed
if you have a claude max/pro subscription (step 2):
claude setup-token on host, copy token to step 5-e ANTHROPIC_API_KEY=... in step 3, skip the paste in step 5if gateway responds with non-200 after step 4 (step 4c):
if container keeps restarting (step 3):
docker logs --tail 20 $INSTANCE-m 2048m and -e NODE_OPTIONS="--max-old-space-size=1024". remove container, relaunch with full step 3 commandif port is in use (step 3):
lsof -i :$HOST_PORT$HOST_PORT variableif you get "exec: openclaw: executable file not found" (any step):
openclaw command directly. always prefix with docker exec $INSTANCE node /app/openclaw.mjs-it: docker exec -it $INSTANCE node /app/openclaw.mjs ...if discord slash commands say "not authorized" (step 8):
applications.commands scope. re-authorize using:https://discord.com/oauth2/authorize?client_id=BOT_CLIENT_ID&scope=bot+applications.commands&permissions=274877991936
channels.discord.guilds config is correct and includes your user idif you set discord guilds config and it fails with "expected record, received array" (step 8c):
config set channels.discord.guilds '{"SERVER_ID":{...}}'config set 'channels.discord.guilds.SERVER_ID' '{...}'if adding new discord users to existing guild (step 8c, later):
config set guilds replaces the entire objectif you need to authenticate from a remote machine (ssh):
ssh -L $HOST_PORT:localhost:$HOST_PORT your-mac.localif you need gmail or google drive (optional steps):
references/gmail-setup.md and `references/google-drive