docs/book/src/ops/network-deployment.md
Deploying ZeroClaw so it can receive inbound traffic: gateway exposure, webhook channels, tunnels, and LAN-only vs. public-facing configurations. Raspberry Pis and other home-network hosts are first-class targets here.
| Mode | Needs inbound port | Notes |
|---|---|---|
| Telegram (long-poll) | No | ZeroClaw polls api.telegram.org — works behind NAT |
| Matrix / Mattermost / Nextcloud Talk | No | Sync/WebSocket — outbound only |
| Discord / Slack (Socket Mode) | No | Outbound WebSocket |
Signal (signal-cli-rest-api) | No | Localhost container |
| Nostr / IMAP / MQTT | No | All outbound |
| Webhooks (GitHub, Slack Events API, WhatsApp, Nextcloud Talk bot, custom) | Yes | Public POST endpoint required |
| Gateway pairing from LAN | Yes (LAN-scope) | Bind to 0.0.0.0 or use a tunnel |
| Discord / Slack (HTTP Events) | Yes | If you don't use Socket Mode |
Upshot: a Telegram-only bot runs on a Pi behind a consumer router with zero port forwarding. Anything webhook-based needs a reachable URL — which is where tunnels come in.
By default the gateway binds to 127.0.0.1 — unreachable from other devices. Three options to expose it:
[gateway]
host = "0.0.0.0"
port = 42617
allow_public_bind = true # required safety flag
Then any device on the LAN can reach http://<pi-ip>:42617. Doesn't help for internet-reachable webhooks — your router's public IP isn't forwarded to the Pi.
Safety: allow_public_bind = true is required because binding to 0.0.0.0 is a significant posture change. Without it, the daemon refuses. This is deliberate.
[tunnel]
provider = "tailscale" # or "cloudflare", "ngrok"
Then restart the daemon — the tunnel is managed declaratively from config, starting alongside the gateway.
The tunnel forwards from a public URL to the gateway on 127.0.0.1. No router config, no opened ports. All three supported tunnels work similarly:
| Provider | Setup friction | Cost | Good for |
|---|---|---|---|
| Tailscale Funnel | Create account, install client | Free tier | Long-term, stable URLs |
| Cloudflare Tunnel | Create Cloudflare account, install cloudflared | Free | Custom domains |
| ngrok | Sign up, install CLI | Free with limits | Testing, short-lived |
Run nginx / Caddy / Traefik in front of the gateway. Terminate TLS there, proxy to localhost:42617. Suitable for:
A minimal Caddy config:
agent.example.com {
reverse_proxy localhost:42617
}
The gateway stays bound to 127.0.0.1 — the proxy does the listening.
For a Pi running Raspberry Pi OS:
curl -fsSL https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/install.sh | bash -s -- --prebuilt
Prefer --prebuilt on a Pi — compiling from source can take 30+ minutes.
For a Pi running Alpine:
apk add curl rust cargo openssl-dev pkgconf
curl -fsSL https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/install.sh | bash
cargo install --locked --path . --features "hardware peripheral-rpi"
Grants access to GPIO, I2C, SPI via rppal. The stock service unit already adds the user to the gpio, spi, i2c groups.
zeroclaw onboardzeroclaw service install && zeroclaw service start[gateway] host = "0.0.0.0" + allow_public_bind = true[tunnel] with a providerOpenRC services run system-wide. Install as root:
sudo zeroclaw service install
Creates:
/etc/init.d/zeroclaw — init script/etc/zeroclaw/ — config directory/var/log/zeroclaw/ — log filesEnable and start:
sudo rc-update add zeroclaw default
sudo rc-service zeroclaw start
sudo rc-service zeroclaw status
Logs:
sudo tail -f /var/log/zeroclaw/error.log
zeroclaw:zeroclaw (least privilege)/etc/zeroclaw/config.tomlsudoTelegram Bot API's getUpdates is single-poller per bot token. You cannot run two instances with the same token — the second gets Conflict: terminated by other getUpdates request.
If you see this:
ps aux | grep zeroclaw and confirm only one daemon is runningcargo run --bin zeroclaw -- channel start telegram from a dev session hanging aroundcurl -X POST "https://api.telegram.org/bot$TOKEN/close"
A publicly-reachable webhook URL is attack surface. At minimum:
secret configured on each webhook channelrate_limit_per_sec in the webhook channel configSee Channels → Webhooks for the full set of knobs.