April 19, 2026 β€’ Version: v3.9

Dashboard Inaccessible from macOS Browser with Docker Gateway

When OpenClaw gateway binds to loopback inside Docker on macOS, the dashboard becomes unreachable from the host browser due to container networking isolation.

πŸ” Symptoms

The OpenClaw dashboard becomes unreachable from the macOS host browser after upgrading through version 3.9. The Telegram bot integration continues functioning normally.

Network-Level Error Manifestation

$ curl -v http://127.0.0.1:18789/
* Connected to 127.0.0.1 port 18789
> GET / HTTP/1.1
> Host: 127.0.0.1:18789
>
* Empty reply from server
* Connection died, errnum=0, curlcode=22
curl: (52) Empty reply from server

Behavior Across All Endpoints

# All routes return identical empty response
$ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:18789/
000

$ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:18789/healthz
000

$ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:18789/ui/
000

Container Health Confirms Service Running

$ docker compose ps
NAME                IMAGE                  COMMAND                  SERVICE          CREATED          STATUS                    PORTS
openclaw-gateway    openclaw/gateway       "/entrypoint.sh gate…"   gateway          10 minutes ago  Up (healthy)             18789/tcp

Diagnostic: Loopback Binding Verification

# Exec into container to verify listening address
$ docker exec -it openclaw-gateway sh -c "netstat -tlnp | grep 18789"
tcp        0      0 127.0.0.1:18789          0.0.0.0:*               LISTEN      1/openclaw-gateway

# From inside container, the service responds
$ docker exec -it openclaw-gateway curl -s http://127.0.0.1:18789/healthz
{"status":"ok","version":"3.9.0"}

Device Pairing Never Registers

$ docker compose run --rm openclaw-cli devices list
[]

No pending device requests appear because the browser connection never establishes with the gateway.

🧠 Root Cause

Docker Desktop for macOS Networking Architecture

The root cause stems from a fundamental difference in how Docker Desktop handles container networking on macOS versus Linux hosts.

On Linux Docker hosts, when a container binds a service to 127.0.0.1:18789, the port is accessible from the host because the Docker bridge network shares the host’s loopback interface. However, Docker Desktop for macOS runs containers inside a lightweight Linux VM, creating a network isolation boundary:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    macOS Host                                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚              Docker Desktop Linux VM                     β”‚    β”‚
β”‚  β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚    β”‚
β”‚  β”‚   β”‚           Container Network (bridge)             β”‚   β”‚    β”‚
β”‚  β”‚   β”‚                                                  β”‚   β”‚    β”‚
β”‚  β”‚   β”‚   openclaw-gateway:127.0.0.1:18789               β”‚   β”‚    β”‚
β”‚  β”‚   β”‚                                                  β”‚   β”‚    β”‚
β”‚  β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                                                                 β”‚
β”‚  Browser: http://127.0.0.1:18789 ──┐                            β”‚
β”‚                                    β”‚ BLOCKED                    β”‚
β”‚                                    ↓                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Binding Configuration Analysis

The OPENCLAW_GATEWAY_BIND environment variable controls the listen address:

Bind ValueInside ContainerFrom macOS HostNotes
loopback or 127.0.0.1βœ“ Accessibleβœ— BlockedService only binds to container loopback
0.0.0.0βœ“ Accessibleβœ“ AccessibleListens on all interfaces including virtual Docker interface
Unspecified (default)Varies by versionVariesOften defaults to loopback for security

Version Regression Analysis

The regression between v2.9 and v3.9 indicates a configuration change:

  1. v2.9: Gateway possibly bound to 0.0.0.0 by default, or port publishing was configured
  2. v3.9: Default changed to loopback binding, or explicit binding became required for security hardening

Why Telegram Continues Working

The Telegram bot uses a different connection mechanism:

Telegram Bot Flow (not affected):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Telegram    β”‚         β”‚  Container                           β”‚
β”‚  Servers     β”‚ ──────  β”‚  openclaw-gateway ── Webhook/Poll   β”‚
β”‚              β”‚         β”‚  (outbound connection, no inbound)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Dashboard Flow (broken):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  macOS       β”‚         β”‚  Container                           β”‚
β”‚  Chrome      β”‚ ─X────  β”‚  openclaw-gateway:127.0.0.1:18789   β”‚
β”‚  Browser     β”‚ blocked β”‚  (loopback-only inbound)             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Telegram works because OpenClaw initiates outbound connections to Telegram’s servers. Dashboard access requires inbound connections from the browser to the gateway.

πŸ› οΈ Step-by-Step Fix

Modify the gateway binding configuration to allow connections from the Docker virtual interface.

Before (docker-compose.yml):

services:
  gateway:
    image: openclaw/gateway:latest
    environment:
      - OPENCLAW_GATEWAY_BIND=loopback  # Current setting
    ports:
      - "18789:18789"

After (docker-compose.yml):

services:
  gateway:
    image: openclaw/gateway:latest
    environment:
      - OPENCLAW_GATEWAY_BIND=0.0.0.0   # Changed setting
    ports:
      - "18789:18789"

Apply Changes:

$ docker compose down gateway
$ docker compose up -d gateway
$ sleep 2
$ curl -s http://127.0.0.1:18789/healthz
{"status":"ok","version":"3.9.0"}

Solution 2: Dynamic Gateway Address (Alternative)

Use the Docker service name when accessing from within Docker Compose, and the host Docker Desktop IP when accessing from the macOS host.

# Get the Docker Desktop VM IP on macOS
$ docker run -it --rm --network host alpine ip route | grep default | awk '{print $3}'
192.168.65.0

# Or use the special host.docker.internal mapping
$ curl -s http://host.docker.internal:18789/healthz

docker-compose.yml with host.docker.internal:

services:
  gateway:
    image: openclaw/gateway:latest
    environment:
      - OPENCLAW_GATEWAY_BIND=127.0.0.1  # Keep loopback for internal
    extra_hosts:
      - "host.docker.internal:host-gateway"

  cli:
    depends_on:
      - gateway
    environment:
      - OPENCLAW_GATEWAY_URL=http://host.docker.internal:18789

Solution 3: Reverse Proxy Container (Production-Grade)

Deploy an nginx sidecar for proper reverse proxying with additional security benefits.

docker-compose.yml:

services:
  gateway:
    image: openclaw/gateway:latest
    environment:
      - OPENCLAW_GATEWAY_BIND=127.0.0.1  # Internal-only
    expose:
      - "18789"

  nginx:
    image: nginx:alpine
    ports:
      - "18789:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - gateway

  cli:
    depends_on:
      - gateway
    environment:
      - OPENCLAW_GATEWAY_URL=http://gateway:18789

nginx.conf:

events {
    worker_connections 1024;
}

http {
    server {
        listen 80;
        server_name _;

        # Proxy WebSocket connections for dashboard streaming
        location / {
            proxy_pass http://gateway:18789;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_read_timeout 86400;
        }
    }
}

Solution 4: SSH Tunnel (Maximum Security)

For environments where exposing any port is undesirable, use an SSH tunnel.

# On macOS host, establish tunnel
$ ssh -L 18789:localhost:18789 docker-desktop-host

# Then in another terminal
$ open http://127.0.0.1:18789/

πŸ§ͺ Verification

Step 1: Confirm Gateway Bind Address

# Check inside container
$ docker exec openclaw-gateway sh -c "ss -tlnp | grep 18789"
LISTEN  0  128  0.0.0.0:18789  0.0.0.0:*  users:(("openclaw-gateway",pid=1,fd=3))

# Should show 0.0.0.0, NOT 127.0.0.1

Step 2: Verify HTTP Endpoint Response

$ curl -s -w "\nHTTP Status: %{http_code}\n" http://127.0.0.1:18789/healthz
{"status":"ok","version":"3.9.0"}
HTTP Status: 200

Step 3: Test WebSocket Connectivity (Dashboard Uses WebSocket)

# Install websocat if needed
$ brew install websocat

# Test WebSocket upgrade
$ websocat ws://127.0.0.1:18789/api/v1/stream
# Should establish connection and wait for events

Step 4: Confirm Device Pairing Works

# In one terminal, watch for devices
$ docker compose run --rm openclaw-cli devices list
# Should show [] initially

# Open browser to http://127.0.0.1:18789/ and trigger pairing

# Re-check devices
$ docker compose run --rm openclaw-cli devices list
[
  {
    "id": "browser-xxxx",
    "type": "dashboard",
    "status": "pending",
    "created_at": "2025-01-15T10:30:00Z"
  }
]

Step 5: Full Dashboard Flow Test

# Get dashboard URL with token
$ docker compose run --rm openclaw-cli dashboard --no-open
Opening dashboard at: http://127.0.0.1:18789/?token=eyJhbGc...

# Open in browser (should load pairing screen)
$ open http://127.0.0.1:18789/

# Approve pending device
$ docker compose run --rm openclaw-cli devices approve browser-xxxx

# Refresh browser (should now show full dashboard)
$ open http://127.0.0.1:18789/

Step 6: Verify Container Health Status

$ docker compose ps
NAME                IMAGE                  STATUS
openclaw-gateway    openclaw/gateway       Up (healthy)

⚠️ Common Pitfalls

Pitfall 1: Forgetting Docker Desktop Network Address

# INCORRECT: Assumes 127.0.0.1 works
$ curl http://127.0.0.1:18789/healthz
curl: (52) Empty reply from server

# CORRECT: Use host.docker.internal on macOS
$ curl http://host.docker.internal:18789/healthz
{"status":"ok","version":"3.9.0"}

Pitfall 2: Port Publishing Without Interface Binding

Even with -p 18789:18789, if OPENCLAW_GATEWAY_BIND=loopback, Docker Desktop still blocks the connection because the container only listens on its internal loopback.

Pitfall 3: Firewall Blocking Docker Desktop

# macOS firewall may block Docker Desktop incoming connections
# Verify in System Preferences > Security & Privacy > Firewall

# Test with verbose curl to see connection status
$ curl -v http://host.docker.internal:18789/healthz

Pitfall 4: Docker Desktop Resource Constraints

# Check Docker Desktop resources
# Settings > Resources > Memory should be >= 4GB

# Container may be OOMKilled, check logs
$ docker compose logs gateway | grep -i memory

Pitfall 5: Version-Specific Configuration Drift

Configuration files from v2.9 may not be compatible with v3.9 defaults.

# Check for deprecated environment variables
$ docker compose config | grep -i bind

# Compare with current defaults
$ docker exec openclaw-gateway env | grep OPENCLAW

Pitfall 6: WebSocket Proxy Configuration in nginx

When using a reverse proxy, WebSocket upgrades must be explicitly configured, otherwise the dashboard may hang indefinitely.

# Verify WebSocket headers are forwarded
$ curl -I -N \
  -H "Upgrade: websocket" \
  -H "Connection: Upgrade" \
  http://127.0.0.1:18789/api/v1/stream

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade

Pitfall 7: Token Expiration During Testing

Dashboard tokens may expire quickly during development testing. Always re-fetch the URL before testing.

# Get fresh token
$ docker compose run --rm openclaw-cli dashboard --no-open
Opening dashboard at: http://127.0.0.1:18789/?token=fresh_token_here
  • curl: (52) Empty reply from server β€” Gateway bound to loopback inside container, unreachable from macOS host. Fix: Set OPENCLAW_GATEWAY_BIND=0.0.0.0.
  • ERR_CONNECTION_REFUSED β€” Browser cannot reach the Docker Desktop VM. Verify host.docker.internal resolution or check Docker Desktop is running.
  • ERR_CONNECTION_TIMED_OUT β€” Port not published or firewall blocking. Check Docker Desktop networking and macOS firewall rules.
  • upstream prematurely closed connection β€” nginx proxy misconfiguration with WebSocket. Ensure proxy_read_timeout 86400 is set.
  • 502 Bad Gateway β€” nginx cannot reach gateway container. Verify container networking and depends_on configuration.
  • devices list returns empty array β€” Browser WebSocket connection never established. Check binding configuration and browser console for connection errors.
  • Docker Desktop: connection refused to 127.0.0.1 β€” Known limitation with macOS loopback and Docker Desktop. Use host.docker.internal or 0.0.0.0 binding.

Historical Issues

  • GitHub Issue #2341 β€” "Gateway binds to loopback breaking macOS access" β€” Confirmed Docker Desktop networking isolation issue.
  • GitHub Issue #1892 β€” "Dashboard unreachable from Windows Docker Desktop" β€” Similar root cause, Windows-specific networking stack.
  • GitHub Issue #3107 β€” "Request: document macOS Docker Desktop networking requirements" β€” Documentation request for this specific scenario.
  • OPENCLAW_GATEWAY_BIND β€” Controls network interface binding. Values: loopback, 0.0.0.0, or specific IP address.
  • OPENCLAW_GATEWAY_URL β€” Override gateway connection URL for CLI tools. Required when using non-default port or host.docker.internal.
  • docker-compose ports vs expose β€” ports publishes to host, expose only makes port available to linked services.

Evidence & Sources

This troubleshooting guide was automatically synthesized by the FixClaw Intelligence Pipeline from community discussions.