Dear Readers,
with the gateway running on localhost:18789, next step is making the Control UI accessible from outside — with HTTPS, auth, and WebSocket support.
Docker + Nginx Proxy Manager#
NPM runs as a Docker container. The setup is minimal:
# /root/nginx-proxy-manager/docker-compose.yml
services:
npm:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "81:81"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
extra_hosts:
- "host.docker.internal:host-gateway"Start it:
cd /root/nginx-proxy-manager
docker compose up -dAdmin panel is on port 81 (default credentials on first login).
The Proxy Host#
Created a proxy host for hub.clawd.it pointing to the OpenClaw gateway.
Gotcha: Don’t use host.docker.internal as the forward hostname. Docker resolves it at container start, but nginx can’t resolve it at runtime when it tries to proxy requests. Use the Docker bridge IP directly:
Forward Hostname: 172.17.0.1
Forward Port: 18789Then enable WebSocket support and request a Let’s Encrypt SSL certificate.
WebSocket + Basic Auth: The Conflict#
Here’s the tricky part. HTTP basic auth to protect the dashboard makes sense, but browsers cannot send basic auth headers with WebSocket upgrade requests. Enable basic auth globally and WebSocket connections fail silently.
The fix: an nginx map directive that conditionally disables auth for WebSocket upgrades.
Custom HTTP Config#
The map directive needs to live at the http level. Luckily NPM supports custom configs in /data/nginx/custom/:
# /data/nginx/custom/http.conf
map $http_upgrade $auth_type {
default "Authorization required";
~(?i)websocket "off";
}If it’s a WebSocket upgrade, $auth_type becomes "off". Everything else gets the auth prompt.
Custom Server/Proxy Config#
# /data/nginx/custom/server_proxy.conf
auth_basic $auth_type;
auth_basic_user_file /data/access/1;
proxy_hide_header Content-Security-Policy;
add_header Content-Security-Policy "default-src 'self'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; style-src-elem 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' ws: wss:" always;This gives us conditional basic auth (skipped for WebSocket) plus a tight Content Security Policy.
The htpasswd Gotcha#
NPM manages its own access files and overwrites /data/access/1 when you make config changes through the UI. Custom htpasswd entries get wiped.
The workaround is regenerating the htpasswd file after any NPM config change:
docker exec nginx-proxy-manager \
htpasswd -nb admin "your-strong-password" > \
/root/nginx-proxy-manager/data/access/1Not elegant, but it works.
OpenClaw Gateway Config#
A few settings in /root/.openclaw/openclaw.json make the proxy setup work:
{
"gateway": {
"controlUi": {
"dangerouslyDisableDeviceAuth": true,
"allowInsecureAuth": true
},
"trustedProxies": ["172.18.0.0/16"]
}
}dangerouslyDisableDeviceAuth— skips the device pairing flow since we’re relying on nginx basic auth insteadallowInsecureAuth— lets auth work through a reverse proxytrustedProxies— trusts the Docker network so the gateway sees real client IPs
The Result#
hub.clawd.it now serves the OpenClaw Control UI with HTTPS via Let’s Encrypt, HTTP basic auth that automatically steps aside for WebSocket upgrades, proper CSP headers, and real-time WebSocket connections working through the proxy.
You can authenticate with the gateway token directly in the URL:
https://hub.clawd.it/#token=your-gateway-tokenThe WebSocket + basic auth conflict was the trickiest part here. That silent failure where connections just refuse to work can cost you a while if you don’t know about the map approach.
Next up: Connecting Claude Max and Telegram

