Running OpenClaw on Synology NAS: complete setup
This guide was compiled with the help of Claude (Anthropic) based on my actual setup session with all the little tweaks I had to do to make it work.. He just helped me shape the instructions and explanations in a clear way. If you find any errors or points of confusion, let me know so I can clarify them in the guide.
What we’re building
OpenClaw (also known as Clawbot / Moltbot) is an AI agent that runs as a local gateway and connects to messaging channels like Telegram, WhatsApp, Discord, and Slack so you can control it from your phone. It also has a built-in web dashboard.
This guide uses:
- A custom local Docker image built on top of
ghcr.io/phioranex/openclaw-docker:latest, which bakes Chrome, pip, and any other system dependencies in permanently - Manual
docker runfor onboarding, so data lives cleanly in/volume1/docker/openclaw/from the very first moment - Portainer Stacks for deployment and all future updates
- A persistent home directory mount, so the entire
/home/nodeis on your NAS and every tool, config file, dotfile, auth token, and credential survives container image updates automatically
Architecture
OpenClaw is two separate processes running as two separate containers:
- Gateway (
openclaw-gateway): handles routing, messaging channels, the web dashboard, and the WebSocket API - Node host (
openclaw-node): the actual agent process that gives OpenClaw its tools (file read/write, exec, browser, web search, etc.). Without this running and paired to the gateway, the agent only has messaging tools and can’t interact with files or run commands
Both must be running for a fully functional setup. The node host shares the gateway’s network namespace so it can reach it on 127.0.0.1:18789.
Before you start
Have these ready:
- SSH access to your NAS (DSM → Control Panel → Terminal & SNMP → Enable SSH)
- Portainer already running on your NAS
- Your AI provider API key (e.g.
sk-ant-...from console.anthropic.com, or an OpenAI key) - Optional but recommended: a Telegram account and a bot token from @BotFather. Do
/newbotin BotFather, follow the prompts, and copy the token it gives you.
Step 1: Create the folder structure
SSH into your NAS and run:
mkdir -p /volume1/docker/openclaw/home
mkdir -p /volume1/docker/openclaw/workspaceFix ownership so the container user (UID 1000) can write to these folders. Run this after all mkdir commands or newly created directories will be owned by root:
sudo chown -R 1000:$(id -g) /volume1/docker/openclaw
sudo chmod -R u+rwX,g+rwX,o-rwx /volume1/docker/openclawStep 2: Create the Dockerfile and build the image
The custom image bakes Google Chrome in permanently so it survives every future update.
Create the Dockerfile:
FROM ghcr.io/phioranex/openclaw-docker:latest
USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
wget \
gnupg \
ca-certificates \
fonts-liberation \
libasound2 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libcups2 \
libdbus-1-3 \
libnspr4 \
libnss3 \
libxcomposite1 \
libxdamage1 \
libxrandr2 \
xdg-utils \
python3-pip \
&& curl -fsSL https://dl.google.com/linux/linux_signing_key.pub \
| gpg --dearmor -o /usr/share/keyrings/google-linux-keyring.gpg \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-linux-keyring.gpg] \
http://dl.google.com/linux/chrome/deb/ stable main" \
> /etc/apt/sources.list.d/google-chrome.list \
&& apt-get update && apt-get install -y google-chrome-stable \
&& rm -rf /var/lib/apt/lists/*
USER nodeAlso create a .dockerignore so Docker doesn’t try to include your persistent data folders in the build context (it will fail if it tries):
home
workspaceBuild the image (takes a few minutes on first run):
cd /volume1/docker/openclaw
docker build -t openclaw-custom:latest .Step 3: Run the one-time onboarding wizard
Interactive and must be done via SSH before deploying with Portainer. Only ever needs to run once. All config is saved to your volume and persists forever.
docker run -it --rm \
-v /volume1/docker/openclaw/home:/home/node \
-v /volume1/docker/openclaw/workspace:/home/node/.openclaw/workspace \
--user 1000:1000 \
openclaw-custom:latest onboardKey answers during the wizard:
| Question | What to choose |
|---|---|
| Personal-by-default prompt | Yes, continue |
| Onboarding mode | Manual |
| What to set up | Local gateway (this machine) |
| Workspace directory | Leave default, just press Enter |
| Gateway bind | LAN (0.0.0.0). Required for Docker port mapping to work. Loopback makes the gateway unreachable from outside the container. |
| Model provider | Your provider; enter your API key when prompted |
| Tailscale | No. Configuring it here can leave the instance unreachable. |
| Messaging channel | Enter your Telegram bot token if you have one, or skip |
If you set up Telegram during onboarding, OpenClaw will send your bot a pairing code. Don’t try to approve it yet. Do that after the gateway starts in Step 4.
Once the wizard finishes, verify config was written correctly:
ls /volume1/docker/openclaw/home/.openclaw/
# Should show openclaw.json and other config filesStep 4: Deploy the Portainer stack
- Log into Portainer
- Click Stacks in the left sidebar → + Add stack
- Name the stack
openclaw - Paste the following into the Web editor:
services:
openclaw-gateway:
image: openclaw-custom:latest
pull_policy: never
container_name: OpenClaw
restart: unless-stopped
command: gateway
stdin_open: true
tty: true
mem_limit: 4g
cpu_shares: 768
security_opt:
- no-new-privileges:true
volumes:
- /volume1/docker/openclaw/home:/home/node
- /volume1/docker/openclaw/workspace:/home/node/.openclaw/workspace
ports:
- 18789:18789
- 18790:18790
environment:
- NODE_ENV=production
- OPENCLAW_SKIP_SERVICE_CHECK=true
- TZ=Europe/Lisbon
- PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:18789/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 30s
timeout: 10s
retries: 3
openclaw-node:
image: openclaw-custom:latest
pull_policy: never
container_name: OpenClaw-Node
restart: unless-stopped
command: node run --node-id nas-node --display-name "NAS Node"
stdin_open: true
tty: true
mem_limit: 1g
cpu_shares: 512
# Share the gateway's network namespace so 127.0.0.1:18789 resolves correctly
network_mode: "service:openclaw-gateway"
security_opt:
- no-new-privileges:true
volumes:
- /volume1/docker/openclaw/home:/home/node
- /volume1/docker/openclaw/workspace:/home/node/.openclaw/workspace
environment:
- TZ=Europe/Lisbon
- PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright
depends_on:
- openclaw-gateway
openclaw-cli:
image: openclaw-custom:latest
container_name: OpenClaw-CLI
volumes:
- /volume1/docker/openclaw/home:/home/node
- /volume1/docker/openclaw/workspace:/home/node/.openclaw/workspace
stdin_open: true
tty: true
command: ["--help"]
profiles:
- cli- Click Deploy the stack
Watch Portainer → Containers → OpenClaw → Logs and wait until you see:
[gateway] listening on ws://0.0.0.0:18789 (PID 1)
Any getUpdates conflict Telegram warnings right after that will clear within seconds. Check OpenClaw-Node logs too and confirm there’s no ECONNREFUSED error.
Step 5: Set up the shell alias
The OpenClaw binary path inside the container is long. Set up an alias on your NAS so you can run all OpenClaw commands cleanly from SSH:
echo "alias openclaw='docker exec OpenClaw /app/node_modules/.pnpm/node_modules/.bin/openclaw'" >> ~/.bashrc
source ~/.bashrcAll commands from here on use this alias. In a fresh SSH session, run source ~/.bashrc first.
Step 6: Access the dashboard
openclaw dashboard --no-openCopy the URL it outputs (contains ?token=...) and open it in your browser.
If you see disconnected (1008): pairing required, your browser needs to be approved as a device:
openclaw devices list
openclaw devices approve <requestId>Step 7: Complete Telegram pairing
If you configured Telegram during onboarding, OpenClaw sent your bot a pairing code:
openclaw pairing approve telegram <CODE>Telegram group messages
By default OpenClaw silently drops all group messages. To allow them:
openclaw config set channels.telegram.groupPolicy openStep 8: Pair the node host
The node needs to be approved as a device before the agent gets its full tool set.
Two things worth knowing before you start.
openclaw nodes list will always show Paired: 0. That’s a confirmed bug. The node pairs as a device, not through the nodes store. Ignore nodes list entirely and use devices list instead.
The node must have a fixed identity. Without --node-id in the command, the node fails to generate a device identity and never creates a pairing request. That’s already handled in the stack above with --node-id nas-node --display-name "NAS Node".
Restart the node container to trigger a fresh pairing request, then immediately check devices:
docker restart OpenClaw-Node && sleep 5 && openclaw devices listYou should see a pending request. Approve it:
openclaw devices approve <requestId>Then verify it shows up with the node role:
openclaw devices list
# Should show a paired device with Roles: operator, nodeNow configure exec routing. All four of these are required:
openclaw config set tools.exec.host node
openclaw config set tools.exec.node nas-node
openclaw config set tools.exec.security full
openclaw config set tools.exec.ask offtools.exec.node pins exec to your specific node. Without it, the gateway doesn’t know which node to target and falls back to the sandbox, which fails in Docker since there’s no companion app. tools.exec.ask off disables the approval prompt for the same reason.
Then restart both containers:
docker restart OpenClaw && sleep 10 && docker restart OpenClaw-NodeAsk your agent to list its available tools. It should now include read, write, edit, exec, web_search, web_fetch, and browser.
If it still only shows messaging tools, check what the config actually has:
sudo cat /volume1/docker/openclaw/home/.openclaw/openclaw.json | grep -A8 '"tools"'The tools block should look like this:
"tools": {
"exec": {
"host": "node",
"node": "nas-node",
"security": "full",
"ask": "off"
}
}If tools.profile is present, that’s the problem. Remove it manually:
sudo nano /volume1/docker/openclaw/home/.openclaw/openclaw.jsonFind and delete the "profile": "messaging", line from the tools block, save, then restart both containers.
The pairing and exec config persist forever, saved in your home volume so you never need to redo this after image updates.
Step 9: Reverse proxy WebSocket (if using DSM Application Portal)
If routing OpenClaw through Synology’s built-in reverse proxy you must enable WebSocket support or the chat will disconnect constantly.
DSM → Control Panel → Application Portal → Reverse Proxy → Edit your OpenClaw entry → Custom Header tab → add both:
| Header | Value |
|---|---|
Upgrade | $http_upgrade |
Connection | $connection_upgrade |
Updating OpenClaw
Because you’re using a custom local image, updating is a two-step process. Never use Container Manager; always go through Portainer for the redeploy step.
Rebuild the image (SSH into your NAS):
cd /volume1/docker/openclaw
docker build --pull -t openclaw-custom:latest .The --pull flag fetches the latest upstream phioranex image before building, so you always get the newest OpenClaw version baked together with Chrome. Takes about 2 minutes.
Then redeploy in Portainer: Stacks → openclaw → Redeploy.
You don’t need “Pull and redeploy” here since the image is local. Just Redeploy is enough. Both containers get restarted on the new image. All config, tools, auth tokens, and credentials live in your mounted volumes so nothing is lost.
Installing Python tools
Since the entire home directory is persistent, install packages directly inside the container and they survive every update:
docker exec -it OpenClaw bash
pip install gcalcli yt-dlp --break-system-packages
exitPackages land in ~/.local/ which is part of your mounted home volume. If a tool isn’t found after install, add ~/.local/bin to PATH in your ~/.bashrc inside the container.
Installing npm tools (agent-browser, etc.)
Since home is persistent, any npm install -g inside the container survives updates:
docker exec OpenClaw npm install -g agent-browser
docker exec OpenClaw agent-browser installagent-browser install downloads Playwright’s Chromium (~167MB). It only happens once because PLAYWRIGHT_BROWSERS_PATH points to your persistent home volume.
System packages (ripgrep, ffmpeg, etc.)
System packages belong in the Dockerfile, not installed at runtime. Add them to the RUN apt-get install block:
RUN apt-get update && apt-get install -y --no-install-recommends \
wget \
gnupg \
...existing packages... \
ripgrep \
ffmpeg \
&& ...rest of Chrome install...Then rebuild and redeploy:
cd /volume1/docker/openclaw
docker build --pull -t openclaw-custom:latest .
# Then Portainer -> Stacks -> openclaw -> RedeployMounting additional folders and Syncthing
Always create the host directory and fix ownership before redeploying. If the path doesn’t exist on the host, the container won’t start:
mkdir -p /volume1/syncthing/notes
sudo chown -R 1000:$(id -g) /volume1/syncthing/notesIf Syncthing runs as a different user and can’t access the parent directory, give it traverse permission:
sudo chmod o+x /volume1/docker/openclawThen add entries to the volumes: section of both openclaw-gateway and openclaw-node:
volumes:
- /volume1/docker/openclaw/home:/home/node
- /volume1/docker/openclaw/workspace:/home/node/.openclaw/workspace
# Syncthing-synced notes, read-only so the agent can't accidentally delete them
- /volume1/syncthing/notes:/home/node/.openclaw/workspace/notes:ro
# A documents library
- /volume1/documents:/home/node/.openclaw/workspace/documents:ro
# A writable output folder
- /volume1/docker/openclaw/outputs:/home/node/.openclaw/workspace/outputsUse :ro for anything you want the agent to read but never modify. Syncthing writes to the host path in real time so changes appear inside the container instantly, no restart needed.
Full persistence reference
| Data | Survives image updates? | Location on NAS |
|---|---|---|
| OpenClaw config, memory, API keys | yes | home/.openclaw/ |
| Agent workspace files | yes | workspace/ |
| npm global tools | yes | home/.local/ or home/.npm-global/ |
| Python tools | yes | home/.local/lib/python3.x/ |
| All dotfiles (.gcalclirc etc.) | yes | home/ |
| All tool auth/config/cache | yes | home/.config/, home/.cache/ etc. |
| System packages (Chrome, pip etc.) | yes | Dockerfile (rebuild to update) |
Troubleshooting
Config was last written by a newer OpenClaw warnings
Version mismatch between the running image and your config. Rebuild the image and redeploy: cd /volume1/docker/openclaw && docker build --pull -t openclaw-custom:latest . then Portainer → Stacks → openclaw → Redeploy.
node host gateway connect failed: connect ECONNREFUSED 127.0.0.1:18789
The node is hardcoded to connect to 127.0.0.1 and needs to share the gateway’s network namespace. Make sure network_mode: "service:openclaw-gateway" is set on the openclaw-node service in the stack.
openclaw nodes list always shows Paired: 0
Known bug. The node pairs as a device, not through the nodes store. Use openclaw devices list instead and look for a device with the node role.
Agent only has 5 messaging tools despite node being paired
Onboarding sets tools.profile: "messaging" in your config which limits the agent to only messaging tools. Remove it manually:
sudo nano /volume1/docker/openclaw/home/.openclaw/openclaw.jsonFind and delete the "profile": "messaging", line from the tools block, save, then restart both containers:
docker restart OpenClaw && sleep 10 && docker restart OpenClaw-NodeContainer restarts in a loop printing only the CLI help text
The command: is missing from the service. Make sure command: gateway is set on the gateway service and command: node run on the node service.
disconnected (1008): pairing required in the dashboard
Your browser session needs to be approved as a device. Run openclaw devices list then openclaw devices approve <requestId>.
Exec requires approval / exec fails silently Make sure all four config values are set:
openclaw config set tools.exec.host node
openclaw config set tools.exec.node nas-node
openclaw config set tools.exec.security full
openclaw config set tools.exec.ask offtools.exec.node pins exec to your specific node. tools.exec.ask off disables the approval prompt. Without both, exec silently fails in Docker because there’s no companion app UI available to handle approval requests.
Then restart both containers:
docker restart OpenClaw && sleep 10 && docker restart OpenClaw-NodeTelegram group messages silently dropped
Run: openclaw config set channels.telegram.groupPolicy open
Quick reference cheat sheet
All commands assume the shell alias from Step 5 is active. If not, replace
openclawwith:docker exec OpenClaw /app/node_modules/.pnpm/node_modules/.bin/openclaw
# Get authenticated dashboard URL
openclaw dashboard --no-open
# Check gateway status
openclaw status
# List paired devices (nodes list has a known bug, use this instead)
openclaw devices list
# Approve a device
openclaw devices approve <requestId>
# Approve Telegram pairing
openclaw pairing approve telegram <CODE>
# Allow Telegram group messages
openclaw config set channels.telegram.groupPolicy open
# Install an npm tool (persistent across updates)
docker exec OpenClaw npm install -g <package>
# Install a Python tool (persistent across updates)
docker exec -it OpenClaw bash -c "pip install <package> --break-system-packages"
# Add a system package: edit Dockerfile, then rebuild + redeploy
# View gateway logs
docker logs OpenClaw --tail 50
# View node logs
docker logs OpenClaw-Node --tail 50
# Update OpenClaw:
# 1. cd /volume1/docker/openclaw && docker build --pull -t openclaw-custom:latest .
# 2. Portainer -> Stacks -> openclaw -> Redeploy