#`Error: unauthorized: gateway token mismatch` whenever the agent tries to run terminal commands
1 messages · Page 1 of 1 (latest)
That error is exactly what it says on the tin: the token your CLI/client is presenting does not match the token the Gateway service is expecting.
In OpenClaw terms:
- Server side (the running gateway):
gateway.auth.token - Client side (CLI / agents when they “call the gateway”): typically
gateway.remote.token(and/orOPENCLAW_GATEWAY_TOKEN, depending on mode/flags)
If those differ, the gateway accepts the TCP/WS connection and then closes it with:
1008 unauthorized: gateway token mismatch.
This is why it can look “random” / “only some commands”: anything that needs to connect to the gateway control plane (cron add/list/status, channel probing, etc.) will fail, while purely local commands might still work.
Common causes
-
You rotated/changed the gateway token (or ran onboarding on another machine/profile) but didn’t update the client-side token everywhere.
-
Gateway service is running with a different config than your CLI (super common on Linux/VPS when the service runs under a different OS user/home).
- The troubleshooting runbook calls this out as “Config (cli) vs Config (service)” → use
openclaw gateway status --deepto compare.
- The troubleshooting runbook calls this out as “Config (cli) vs Config (service)” → use
Source: https://docs.openclaw.ai/gateway/troubleshooting
- You’re in
gateway.mode=remoteandgateway.remote.tokenis set, so the CLI will prefer that token (even ifgateway.auth.tokenis different). Credential precedence is documented here:
https://docs.openclaw.ai/gateway/remote
Fix (safe, minimal)
Run these on the machine/profile that runs the gateway service:
- Check version (helps catch “client updated, service not” weirdness):
openclaw --version
- Confirm which config the service is actually using:
openclaw gateway status --deep
Look for the “Config (service)” path vs “Config (cli)” path.
- Make the tokens match:
- Ensure the gateway service has the token you intend:
gateway.auth.token - Ensure the client/CLI token matches it:
gateway.remote.token(orOPENCLAW_GATEWAY_TOKEN)
Do not paste tokens into Discord. If you need to sanity-check, compare “same length” + first/last 4 chars locally.
- Restart the gateway after changing config:
openclaw gateway restart
Important nuance (docs)
gateway.remote.token does not configure server auth. It’s just a client credential source. So setting gateway.remote.token alone won’t “fix the server” if gateway.auth.token is different.
Source: https://docs.openclaw.ai/gateway/remote
Separate issue: the doctor warning about Telegram groupPolicy: "allowlist" + empty groupAllowFrom/allowFrom means group messages will be dropped, but it’s not what’s causing the token mismatch. Fix the token first, then handle that allowlist.
If you paste only the output of openclaw gateway status --deep (redacting tokens/URLs), I can tell you which side is out of sync (service config vs CLI config vs remote mode).
Yep — gateway.bind: "lan" / "tailnet" is relevant, but it’s not directly the cause of the specific error you’re seeing.
What’s actually happening
unauthorized: gateway token mismatch means:
- the Gateway service (running in your Docker container) expects token A (
gateway.auth.token) - but the client making the call (the
openclaw cron add ...command your agent runs) is presenting token B (gateway.remote.token/OPENCLAW_GATEWAY_TOKEN/--token)
So the connection reaches the gateway, then the gateway closes it with code 1008.
bind: lan is why auth is required at all (even “locally”), but if auth were simply missing you’d typically see errors like “refusing to bind … without auth” or “missing token”, not a mismatch.
Docs worth skimming:
- Remote/token precedence +
gateway.remote.tokenmeaning: https://docs.openclaw.ai/gateway/remote - Troubleshooting runbook + “Config (cli) vs Config (service)” mismatch pattern: https://docs.openclaw.ai/gateway/troubleshooting
Why Docker setups hit this a lot
In containerized setups there are usually two separate configs/contexts:
- Gateway service config (inside container, whatever the service starts with)
- CLI config (wherever the
openclawcommand your agent runs is reading from)
If your agent is running openclaw cron add ... outside the container (host), it will read the host’s ~/.openclaw/openclaw.json and env vars — which may not match the container’s.
Even inside the container, you can get mismatches if:
- the gateway service runs under a different user/home than the CLI, or
- the service uses env
OPENCLAW_GATEWAY_TOKENwhile the CLI usesgateway.remote.tokenfrom config, or - there are accidentally two gateways running (host + container) on different ports/tokens.
Fix checklist (practical)
1) Verify which config the gateway service is using (inside the container)
Exec into the container and run:
openclaw gateway status --deep
You’re looking for the lines that show Config (service) vs Config (cli) (and generally whether the service is picking up the config you think it is).
2) Make the client token match the service token
You have three common ways:
- Set
gateway.remote.tokenon the client side to equal the service’sgateway.auth.token - or set
OPENCLAW_GATEWAY_TOKENin the environment where the client runs - or pass
--tokenexplicitly (good for quick testing, not ideal long-term)
Important nuance from the docs: gateway.remote.token is a client credential source; it does not configure server auth by itself. The server auth is gateway.auth.token.
https://docs.openclaw.ai/gateway/remote
3) Restart the gateway after changing server-side auth/config
If you change gateway.auth.token in the container config, you must restart the container / gateway service.
(Also: openclaw doctor --generate-gateway-token won’t rotate an existing token; it only fills it if missing.)
If you want the fastest diagnosis
Paste redacted output of:
openclaw --version(host and container, if both exist)openclaw gateway status --deep(inside container)
Redact URLs/tokens, but leave the config paths + mode/bind/auth mode visible. That usually makes it obvious whether you’re dealing with:
- service using different config path than you think, or
- client is pointing at the wrong gateway (host vs container), or
- stale
gateway.remote.tokenafter a token rotation.
(Separate note: the Telegram groupPolicy=allowlist warning is real, but unrelated to the token mismatch. Fix it after gateway auth is stable.)
That error is coming from Docker Compose itself: it can’t exec into a container that isn’t running.
Two likely issues:
- The container really is stopped/crashing
- You’re in the wrong folder / wrong compose project, so you’re looking at a different
docker-compose.ymlthan the one that actually runs OpenClaw.
Run these from the directory that has your compose.yml / docker-compose.yml:
docker compose ps -a
docker compose config --services
- Confirm the service name is actually
openclaw(it might begateway,openclaw-gateway, etc.). - Check the status (Exited? Restarting?).
Then grab logs:
docker compose logs --tail=200 openclaw
(If the service name differs, replace openclaw with whatever config --services shows.)
If it’s simply stopped, start it:
docker compose up -d openclaw
# or just:
docker compose up -d
After it’s running, re-run the diagnostics with the correct command inside the container (note the extra openclaw):
docker compose exec openclaw openclaw --version
docker compose exec openclaw openclaw gateway status --deep
If you paste:
docker compose ps -aoutputdocker compose logs --tail=200 openclaw
…I can usually tell immediately whether it’s a config crash (very common) vs a compose/project mismatch.
Nice, that --deep output is the smoking gun: even inside the openclaw-gateway container, the CLI can’t authenticate to the gateway WS on ws://127.0.0.1:18789 — so this is not “LAN bind requires auth” (that part is true), it’s specifically “the token the client is using ≠the token the gateway is expecting.”
In Docker setups this almost always happens because you effectively have two token sources:
.env/ Compose env var:OPENCLAW_GATEWAY_TOKEN=...- config file:
~/.openclaw/openclaw.jsonwithgateway.auth.tokenand sometimesgateway.remote.token
…and they drift out of sync (reran onboarding, edited config, changed .env, etc.). The Docker guide even calls out that it generates a token and writes it to .env. Source: https://docs.openclaw.ai/install/docker
1) Confirm it’s an env-vs-config mismatch (no token leaking)
Run these but don’t paste the raw token, just the hashes:
docker compose exec openclaw-gateway sh -lc 'echo -n "$OPENCLAW_GATEWAY_TOKEN" | sha256sum'
docker compose exec openclaw-gateway sh -lc 'openclaw config get gateway.auth.token | tr -d "\n" | sha256sum'
docker compose exec openclaw-gateway sh -lc 'openclaw config get gateway.remote.token | tr -d "\n" | sha256sum'
If those hashes don’t match each other, that’s your mismatch.
(Why this matters: client token resolution/precedence can involve env + config; see the precedence section here: https://docs.openclaw.ai/gateway/remote)
2) Fast fix (make everything use the same token)
Pick one “source of truth” (in Docker I usually pick the Compose env OPENCLAW_GATEWAY_TOKEN) and set config to match it:
docker compose exec openclaw-gateway sh -lc 'openclaw config set gateway.auth.token "$OPENCLAW_GATEWAY_TOKEN"'
docker compose exec openclaw-gateway sh -lc 'openclaw config set gateway.remote.token "$OPENCLAW_GATEWAY_TOKEN"'
docker compose restart openclaw-gateway
Then sanity check:
docker compose run --rm openclaw-cli openclaw status
# or:
docker compose run --rm openclaw-cli openclaw gateway status --deep
You want RPC probe: ok and no more 1008 unauthorized.
Notes on the other lines in your output
- The
systemd ... unavailablewarnings are expected in containers;openclaw gateway statustries to report systemd service status, but the RPC probe is the part that actually matters here. bind=lanis fine as long as token auth is consistent; it just means you must have auth enabled.
If you reply with just “the three sha256 hashes matched / didn’t match” (no tokens), I can tell you which direction to adjust (.env vs openclaw.json) if the quick fix above doesn’t immediately clear it.
Yep — that’s just a Docker/entrypoint quirk, not a new OpenClaw problem.
In the Docker setup, the openclaw-cli service is usually configured with openclaw as its entrypoint, so when you run:
docker compose run --rm openclaw-cli openclaw status
you’re effectively doing:
openclaw openclaw status
…and the CLI quite reasonably says: unknown command openclaw.
Use it like the docs examples (no extra openclaw)
Try:
docker compose run --rm openclaw-cli status
docker compose run --rm openclaw-cli gateway status --deep
And for cron:
docker compose run --rm openclaw-cli cron add --name "padel-reminder" --at "2026-03-03T09:00:00Z" --message "Reminder: …"
The CLAUDE_* warnings
Those are just Compose warning you that some optional env vars referenced in your compose/env file aren’t set (often from older Claude web-session login flows). If you’re using API keys / OAuth, they’re usually harmless and can be ignored (or you can delete those env var references later).
If docker compose run --rm openclaw-cli status now shows healthy output (and gateway status --deep shows RPC probe: ok), you’re unblocked and your agent’s cron commands should work again.
If this fixed it, please right-click the solution message → Apps → ✅ Mark Solution so Answer Overflow indexes it for the next person.
That output is basically telling you: your openclaw-cli “run” container is probing ws://127.0.0.1:18789, but inside that container 127.0.0.1 is itself, not the openclaw-gateway container.
So the token mismatch is fixed, but gateway status --deep from the CLI container is now failing because it’s trying to talk to a gateway that isn’t on its own loopback → you get the weird 1006 abnormal closure probe failure.
Quick confirm (probe from the gateway container itself)
This should be the “true” health check:
docker compose exec openclaw-gateway openclaw gateway status --deep
If that shows RPC probe: ok, your gateway is fine.
Fix the CLI container to target the gateway container (recommended)
Set the remote URL to the compose service DNS name:
docker compose run --rm openclaw-cli config set gateway.remote.url "ws://openclaw-gateway:18789"
Then re-run:
docker compose run --rm openclaw-cli gateway status --deep
(Compose gives you DNS for service names, so openclaw-gateway should resolve from the openclaw-cli container.)
Why the systemd warnings
Ignore those in Docker. gateway status --deep tries to inspect systemd user services; containers don’t have that.
If docker compose exec openclaw-gateway openclaw gateway status --deep is clean and your original cron add now works, you’re fully unblocked. If it still fails, paste the gateway status --deep from inside openclaw-gateway (redact token/url) and the last ~50 lines of docker compose logs --tail=50 openclaw-gateway.
That’s expected: openclaw gateway status --deep is a “local gateway service + localhost probe” command, and when you run it inside the ephemeral openclaw-cli container it will default to probing:
ws://127.0.0.1:18789(the CLI container’s own loopback)
…but your gateway is in the other container, so the probe fails with the 1006 closure.
The reason your config set gateway.remote.url "ws://openclaw-gateway:18789" didn’t change the output is that gateway status doesn’t automatically use gateway.remote.url for its probe; it probes “local/loopback” unless you override it.
What to do instead
Option A (recommended): run gateway-status from the gateway container (you already did this, and it’s the truth):
docker compose exec openclaw-gateway openclaw gateway status --deep
Option B: from the CLI container, override the probe URL explicitly
(run it with a shell so $OPENCLAW_GATEWAY_TOKEN expands inside the container):
docker compose run --rm openclaw-cli sh -lc '
openclaw gateway status --deep \
--url ws://openclaw-gateway:18789 \
--token "$OPENCLAW_GATEWAY_TOKEN"
'
If you just want a clean “can I talk to the gateway?” check, this is even simpler:
docker compose run --rm openclaw-cli sh -lc '
openclaw gateway health \
--url ws://openclaw-gateway:18789 \
--token "$OPENCLAW_GATEWAY_TOKEN"
'
Bottom line
Since docker compose exec openclaw-gateway … shows RPC probe: ok, your gateway is healthy and your original token mismatch issue is fixed. The remaining “RPC probe: failed” is just the CLI container probing its own localhost.
If this got your cron commands working again, please Mark Solution (right-click → Apps → ✅ Mark Solution) so Answer Overflow indexes it.
Awesome! Now that my gateway is healthy, could you help me also address the doctor warning about Telegram?
That Doctor warning means: your Telegram group messages are being dropped on purpose because you’ve told OpenClaw “only allow group senders from an allowlist”… but you haven’t actually provided any allowed sender IDs.
From the Telegram docs:
channels.telegram.groupPolicycontrols who is allowed to talk to the bot in groups (open | allowlist | disabled)channels.telegram.groupAllowFromis the allowlist of sender IDs for groups (if missing, it falls back tochannels.telegram.allowFrom)
Source: https://docs.openclaw.ai/channels/telegram
Step 1 — get your Telegram numeric user id (and optionally the group id)
Safest way (no third-party bots):
- In Telegram, DM your bot (or write in the group).
- On the server, tail logs and look for
from.id(your user) andchat.id(the group):
docker compose exec openclaw-gateway openclaw logs --follow
Step 2 — choose the group behavior you want
Option A (recommended for most people): allow only you in groups
Keep groupPolicy: "allowlist" and set groupAllowFrom to your Telegram user id:
docker compose run --rm openclaw-cli config set channels.telegram.groupAllowFrom '["tg:123456789"]' --strict-json
docker compose restart openclaw-gateway
(Use your real numeric id; tg:/telegram: prefixes are accepted and normalized per the docs.)
Also make sure you’ve allowed the group itself under channels.telegram.groups (separate control). You can either allow a specific group id, or "*".
Option B: allow any member of a specific group
Set that group’s groupPolicy to open in channels.telegram.groups (example shown in docs):
{
channels: {
telegram: {
groups: {
"-1001234567890": {
groupPolicy: "open",
requireMention: true
}
}
}
}
}
This is the “bot can respond to anyone in that group” mode; keeping requireMention: true avoids it butting into every message.
Quick note on pairing
Telegram pairing is DM-only; group sender auth does not inherit DM pairing approvals (docs call this out as a security boundary). So for groups you really do need groupAllowFrom or per-group groupPolicy: "open".
Source: https://docs.openclaw.ai/channels/telegram
If you tell me which mode you want (A: only you, B: whole group) and whether you want one group or any group, I can give you the exact config snippet/config set commands for your case.
Seeing your home server IP on the Dashboard: line is normal, and by itself it does not mean “publicly exposed to the internet.” It’s just telling you what address/URL the dashboard would be reachable at from whatever network can reach that host/port.
What matters is (a) what the gateway is bound to and (b) whether your router/firewall publishes that port.
In your case
Your status output showed:
Gateway: bind=lan (0.0.0.0), port=18789
bind=lan means the gateway is listening on all interfaces on that machine (0.0.0.0). So:
- Anyone on your LAN can hit
http://<home-server-ip>:18789/ - It is not automatically reachable from the public internet unless you also have port forwarding / firewall rules / cloud security group allowing inbound access.
How to check if it’s internet-exposed
On your router / firewall, look for any port forwards for 18789 (or whatever port you set). If there’s no port forward and your host firewall blocks it, it’s LAN-only.
If you want a quick external check, from a device not on your home network (cellular, or a VPS), try connecting to your public IP on that port. If it can’t connect, you’re not exposed.
If you want the safer default
Recommended approach is usually:
- keep the gateway loopback-only (
gateway.bind: "loopback") - access remotely via SSH tunnel or Tailscale Serve (docs: https://docs.openclaw.ai/gateway/remote)
That way, even LAN devices can’t hit it directly.
Important reassurance
Even if it is reachable on your LAN, OpenClaw expects you to use auth (token/password/device auth). Still, I’d treat bind=lan as “intentionally shared on the LAN” and only do it if you actually need it.
If you paste (redacted) your gateway.bind and whether you’re using Tailscale/SSH, I can recommend the cleanest “not exposed” setup for your use case.
Careful: in Docker, gateway.bind applies inside the container. If you set gateway.bind: "loopback", the gateway will typically listen on 127.0.0.1 inside the container, and Docker port publishing forwards traffic to the container’s eth0 IP (e.g. 172.x.x.x) — meaning the published port can stop working.
Recommended “loopback-only on your home server” (works well with Docker)
Leave the gateway bound to LAN inside the container, but publish it only on host loopback:
In docker-compose.yml (service openclaw-gateway), change ports to:
ports:
- "127.0.0.1:18789:18789"
Then:
docker compose up -d
Result: the dashboard/gateway is reachable only from the home server itself (http://127.0.0.1:18789/). If you want to access it from your laptop, use an SSH tunnel.
If you really want to set gateway.bind to loopback anyway
You can, but expect the dashboard/WS to become unreachable from outside the container unless you also change networking.
Command to set it:
docker compose run --rm openclaw-cli config set gateway.bind "loopback"
docker compose restart openclaw-gateway
If after that you can’t connect anymore, revert with:
docker compose run --rm openclaw-cli config set gateway.bind "lan"
docker compose restart openclaw-gateway
If you tell me how you normally access the Dashboard (from the server itself vs from another device on your LAN), I’ll tell you which of the two setups is the cleanest for your workflow.
That Compose line is syntactically correct: IP:HOST_PORT:CONTAINER_PORT.
So this:
ports:
- "<tailscale_ip>:${OPENCLAW_GATEWAY_PORT:-18789}:18789"
means “publish the container’s 18789 only on the host’s Tailscale interface IP, on host port ${OPENCLAW_GATEWAY_PORT} (default 18789).” That’s a reasonable way to make the dashboard reachable only via Tailscale, not via your LAN/public interfaces.
Two gotchas to double-check:
- Does the gateway inside the container still listen on 18789?
If you changed the gateway port via env/config (so it listens on, say, 19000), then mapping to container:18789will break. In that case you want:
ports:
- "<tailscale_ip>:${OPENCLAW_GATEWAY_PORT:-18789}:${OPENCLAW_GATEWAY_PORT:-18789}"
(i.e., same port on both sides), or keep the gateway listening on 18789 in-container and only change the host side.
- Is Tailscale up before Docker starts?
Binding to a specific IP requires that IP to exist on the host at container start. If Docker starts before Tailscale, you can get “cannot assign requested address” and the port won’t publish.
If you want, paste (with IPs redacted) the relevant bits of your compose env (what OPENCLAW_GATEWAY_PORT is set to, if at all) and whether you changed gateway.port in config — then I can tell you which port mapping form is correct for your setup.
Yeah, it works. I can access the gateway. I just wanted you to verify if it's secure.
If that line is literally what you’re running, it’s a good security posture for a home-server Docker gateway:
ports:
- "<tailscale_ip>:${OPENCLAW_GATEWAY_PORT:-18789}:18789"
What it means (security-wise)
- Docker will only bind the published port on the host’s Tailscale interface IP, not on
0.0.0.0and not on your LAN IP. - So devices not on your tailnet can’t even reach the port, unless you’ve separately done something like port-forwarding / reverse proxying / Tailscale Funnel.
That’s the main “is it exposed to the internet?” concern, and this avoids it.
Two quick checks to be sure
- From a LAN device that is not on Tailscale, try:
http://<your-lan-ip>:18789/→ should fail
- On the server, check what’s listening:
ss -lntp | grep 18789
You should see it bound to the Tailscale IP only, not 0.0.0.0:18789.
Still keep auth on
Even on Tailscale, keep gateway.auth.token set (which you already do). Tailscale is great, but token auth is still the right extra layer.
If those checks pass, you’re in the “reachable only from tailnet + token required” zone, which is about as sane as it gets for a home deployment.
Regarding the second quick check (ss -lntp | grep 18789). Did you mean on the server or on the docker container? Or is it the same?
On the server (the Docker host).
Reason: the security question you’re checking is “what interfaces is this port published on from the host?” Docker’s ports: publishing happens at the host networking layer (iptables/nftables / userland-proxy), so you want to inspect listeners on the host.
A good workflow:
1) On the server (host)
ss -lntp | grep 18789
You should see it bound to your Tailscale IP:18789 (or at least not 0.0.0.0:18789).
2) Optionally, inside the container (different question)
Inside openclaw-gateway, checking sockets answers “what is the gateway process itself listening on in the container namespace?” That’s often 0.0.0.0:18789 regardless, and that’s fine—because the host is still only publishing it on the Tailscale IP.
If you want to check that too:
docker compose exec openclaw-gateway ss -lntp | grep 18789
So: host check = exposure, container check = internal binding.
Could you make a markdown file with useful things to remember from this conversation?
Saved a markdown recap here:
/home/exedev/clawd/memory/conversation-notes-zaro-gateway-token-mismatch-2026-03-02.md
Contents:
# Conversation notes — Zaro — gateway token mismatch + Docker/Tailscale/Telegram
Date: 2026-03-02 (UTC)
## Symptoms
- Agent/CLI commands that need Gateway RPC (e.g. `openclaw cron add …`) failed with:
- `unauthorized: gateway token mismatch (set gateway.remote.token to match gateway.auth.token)`
- WS close code `1008`.
## Root cause (auth)
- **Client token ≠server token**.
- Server expects: `gateway.auth.token`
- Client uses: `gateway.remote.token` and/or `OPENCLAW_GATEWAY_TOKEN` (plus `--token` overrides)
- In Docker setups, token drift commonly happens because:
- Compose `.env` sets `OPENCLAW_GATEWAY_TOKEN`, while
- `~/.openclaw/openclaw.json` may contain a different `gateway.auth.token` / `gateway.remote.token`.
## Fix (Docker “fastfix”)
- Align config tokens to Compose env token, then restart:
- Set `gateway.auth.token = $OPENCLAW_GATEWAY_TOKEN`
- Set `gateway.remote.token = $OPENCLAW_GATEWAY_TOKEN`
- Restart gateway container
Security note: never paste tokens into chat; compare via hashes if needed.
## Docker Compose service name gotcha
- The service was named `openclaw-gateway` (not `openclaw`).
- `docker compose exec <service> …` only works if the service is running.
## CLI container entrypoint gotcha
- `openclaw-cli` service commonly has `openclaw` as entrypoint.
- Therefore:
- âś… `docker compose run --rm openclaw-cli status`
- ❌ `docker compose run --rm openclaw-cli openclaw status` (becomes `openclaw openclaw status` → “unknown command 'openclaw'”).
## “gateway status --deep” from the CLI container can look broken (but isn’t)
- `openclaw gateway status --deep` probes **localhost** (`ws://127.0.0.1:18789`) by default.
- When run inside the ephemeral `openclaw-cli` container, `127.0.0.1` refers to the CLI container itself, **not** the gateway container.
- So you can see:
- `RPC probe: failed` with `1006 abnormal closure`
- even though the actual gateway is healthy.
### Correct checks
- “Truth” check (inside gateway container):
- `docker compose exec openclaw-gateway openclaw gateway status --deep` → should show `RPC probe: ok`.
- To probe from CLI container, **override URL and pass token explicitly**:
- `openclaw gateway health --url ws://openclaw-gateway:18789 --token "$OPENCLAW_GATEWAY_TOKEN"`
## Telegram Doctor warning (group allowlist)
- Warning: `channels.telegram.groupPolicy is "allowlist" but groupAllowFrom (and allowFrom) is empty — all group messages will be silently dropped.`
- Fix options:
- Keep group allowlist and set `channels.telegram.groupAllowFrom` to numeric Telegram user IDs (recommended for tight control), and ensure group IDs are configured under `channels.telegram.groups`.
- Or set a group (or `"*"`) to `groupPolicy: "open"` if you want anyone in that group to be able to talk to the bot.
- Pairing approvals are **DM-only**; group sender auth does not inherit DM pairing.
Docs: https://docs.openclaw.ai/channels/telegram
## “Is my dashboard exposed to the internet?” (Docker + Tailscale)
- Seeing the server IP on the `Dashboard:` line is normal; it indicates where the UI is reachable from.
- Exposure depends on bind + publishing + firewall/port-forwarding.
### Good pattern: publish only on Tailscale IP
- Compose port publish example:
- `<tailscale_ip>:${OPENCLAW_GATEWAY_PORT:-18789}:18789`
- This restricts reachability to tailnet devices (assuming no other port forwards).
### Verification tip
- Run `ss -lntp | grep 18789` on the **host** to see what interfaces the port is bound/published on.
- Host view = exposure