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:

Architecture

OpenClaw is two separate processes running as two separate containers:

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:


Step 1: Create the folder structure

SSH into your NAS and run:

mkdir -p /volume1/docker/openclaw/home
mkdir -p /volume1/docker/openclaw/workspace

Fix 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/openclaw

Step 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:

/volume1/docker/openclaw/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 node

Also create a .dockerignore so Docker doesn’t try to include your persistent data folders in the build context (it will fail if it tries):

/volume1/docker/openclaw/.dockerignore
home
workspace

Build 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 onboard

Key answers during the wizard:

QuestionWhat to choose
Personal-by-default promptYes, continue
Onboarding modeManual
What to set upLocal gateway (this machine)
Workspace directoryLeave default, just press Enter
Gateway bindLAN (0.0.0.0). Required for Docker port mapping to work. Loopback makes the gateway unreachable from outside the container.
Model providerYour provider; enter your API key when prompted
TailscaleNo. Configuring it here can leave the instance unreachable.
Messaging channelEnter 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 files

Step 4: Deploy the Portainer stack

  1. Log into Portainer
  2. Click Stacks in the left sidebar → + Add stack
  3. Name the stack openclaw
  4. Paste the following into the Web editor:
Portainer Stack
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
  1. 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 ~/.bashrc

All commands from here on use this alias. In a fresh SSH session, run source ~/.bashrc first.


Step 6: Access the dashboard

openclaw dashboard --no-open

Copy 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 open

Step 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 list

You 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, node

Now 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 off

tools.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-Node

Ask 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.json

Find 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:

HeaderValue
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
exit

Packages 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 install

agent-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 -> Redeploy

Mounting 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/notes

If Syncthing runs as a different user and can’t access the parent directory, give it traverse permission:

sudo chmod o+x /volume1/docker/openclaw

Then 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/outputs

Use :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

DataSurvives image updates?Location on NAS
OpenClaw config, memory, API keysyeshome/.openclaw/
Agent workspace filesyesworkspace/
npm global toolsyeshome/.local/ or home/.npm-global/
Python toolsyeshome/.local/lib/python3.x/
All dotfiles (.gcalclirc etc.)yeshome/
All tool auth/config/cacheyeshome/.config/, home/.cache/ etc.
System packages (Chrome, pip etc.)yesDockerfile (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.json

Find and delete the "profile": "messaging", line from the tools block, save, then restart both containers:

docker restart OpenClaw && sleep 10 && docker restart OpenClaw-Node

Container 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 off

tools.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-Node

Telegram 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 openclaw with: 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