2026 OpenClaw Gateway with Nginx or Caddy reverse proxy on a dedicated remote Mac
Deployment & Ops 2026-05-11

2026 OpenClaw Gateway: Nginx vs Caddy on the Same Dedicated Remote Mac (SSH Headless) — TLS Termination, WebSocket Upgrade, allowedOrigins & trusted-proxy — Reproducible Deployment Matrix + FAQ

When browsers, IDE webviews, and automation scripts talk to OpenClaw Gateway, listening on 127.0.0.1 alone stops scaling: you need HTTPS, predictable WebSocket upgrades, a tight allowedOrigins story, and an explicit trusted-proxy contract for X-Forwarded-*. This article compares Nginx and Caddy as co-hosted reverse proxies on an exclusive remote Mac managed over SSH—TLS paths, minimal configs, a deployment matrix, headless acceptance checks, and FAQ you can paste into a runbook.

1. Why add a reverse proxy in front of Gateway?

Gateway exposes HTTP, SSE, and WebSocket surfaces. SSH port forwards are fine for solo debugging, but shared teams and SaaS callbacks quickly surface the same constraints:

  • HTTPS is required to avoid mixed-content blocks for EventSource and secure WebSockets.
  • You need hostname-based routing when multiple instances (dev/stage/prod) share one machine.
  • You want consistent X-Forwarded-For / X-Forwarded-Proto parsing, rate limits, and optional edge ACLs.
  • On a headless Mac, certificate issuance and renewal must be scriptable—no GUI wizard.

This post assumes the common topology: Nginx or Caddy on loopback-facing ports, Gateway bound to 127.0.0.1:<port> only. Pair the matrix below with OpenClaw multi-model keys and provider wiring on remote Mac SSH so outbound model traffic stays separate from inbound browser trust.

2. Decision matrix: Nginx vs Caddy (co-hosted with Gateway)

Both terminate TLS and reverse-proxy to Gateway. The split is mostly operational ergonomics versus existing fleet patterns:

Dimension Nginx Caddy Guidance
Config style Directive + map Caddyfile blocks Prefer Caddy for small teams; keep Nginx if the host already runs complex vhosts.
Automatic HTTPS certbot / acme.sh Built-in ACME Public hostname + single-node edge: Caddy usually wins time-to-green.
WebSocket upgrade Manual Upgrade/Connection reverse_proxy defaults Nginx misconfigs here are the top cause of flaky WS; Caddy reduces foot-guns.
Trusted client IP chain set_real_ip_from + real_ip_header trusted_proxies Never trust raw X-Forwarded-For without an explicit hop list.
Long-lived streams proxy_read_timeout tuning Transport timeouts SSE/WebSockets need ≥1h read timeouts and buffering off.
macOS install brew install nginx brew install caddy Both are first-class on Apple Silicon via Homebrew.

One-line picks: greenfield public HTTPS on one host → Caddy; existing multi-site Nginx fleet → Nginx; if the edge is Tailscale Funnel or Cloudflare Tunnel, keep this proxy as the inner TLS/Origin layer and let the tunnel handle upstream identity.

For why bare-metal Mac mini class hardware still beats many "cloud Mac" slices under sustained gateway load, see Real Mac mini vs virtual cloud Mac: 2026 performance and stability comparison.

3. TLS termination paths

3.1 mkcert / local CA

Best for ssh -L workflows and internal hostnames. Import the local root once per operator laptop; renewals are scripted.

3.2 Tailscale TLS (*.ts.net)

Great for tailnet-only teams: no public 80/443 requirement, certificates rotate automatically, but migration across tailnets is deliberate work.

3.3 Let's Encrypt (public DNS)

Requires HTTP-01 reachability on :80 or DNS-01 automation. Caddy wires ACME with fewer moving parts; Nginx typically delegates to certbot hooks plus reload.

3.4 Minimal Caddyfile (example)

# Example only — replace host, port, and log paths.
gw.example.com {
    encode gzip
    log {
        output file /opt/homebrew/var/log/caddy/gw.access.log
        format json
    }
    reverse_proxy 127.0.0.1:18789 {
        trusted_proxies 127.0.0.1/32 ::1/128
        header_up Host {host}
        header_up X-Real-IP {remote_host}
    }
}

3.5 Minimal Nginx fragment (example)

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 443 ssl http2;
    server_name gw.example.com;

    ssl_certificate     /etc/letsencrypt/live/gw.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/gw.example.com/privkey.pem;

    set_real_ip_from 127.0.0.1;
    real_ip_header   X-Forwarded-For;
    real_ip_recursive on;

    location / {
        proxy_pass         http://127.0.0.1:18789;
        proxy_http_version 1.1;
        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_set_header   X-Forwarded-Proto $scheme;
        proxy_set_header   Upgrade           $http_upgrade;
        proxy_set_header   Connection        $connection_upgrade;
        proxy_buffering    off;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

Footgun: Nginx without the map + Upgrade headers yields WebSockets that appear to connect then die immediately.

4. WebSocket upgrades and streaming timeouts

Concern Nginx Caddy Suggested value
Read timeout proxy_read_timeout transport read_timeout ≥ 1h for long tool runs
Write timeout proxy_send_timeout transport write_timeout Match read timeout
SSE buffering proxy_buffering off flush_interval -1 Must disable buffering or first bytes stall

Acceptance: use curl -N against an SSE endpoint and confirm bytes arrive within seconds; exercise a WebSocket with an idle window to validate NAT idle timeouts and application-level pings (15–30s).

5. allowedOrigins and trusted-proxy (two layers)

5.1 Who owns Origin checks?

Keep allowedOrigins as a Gateway-owned allowlist (exact matches). The reverse proxy should enforce coarse rules (TLS, method allowlists, optional IP ACLs) but not silently "fix" CORS for the app—dual implementations drift.

5.2 Minimal trusted-proxy set (same-machine proxy)

When Caddy/Nginx and Gateway share the Mac, trust only 127.0.0.1/32 and ::1/128 for the inner hop. If another edge (Cloudflare, Funnel) sits upstream, model that as its own trust zone with explicit IP ranges—never accept arbitrary X-Forwarded-For from the Internet.

6. SSH-headless deployment matrix

Scenario Proxy Certs Egress pattern Notes
Solo IDE + SSH tunnel Caddy mkcert SSH local forward Lowest ceremony; document root CA install.
Team tailnet Caddy Tailscale TLS Tailnet only Avoids punching public 80/443 if not required.
Public SaaS callbacks Caddy or Nginx Let's Encrypt Direct or tunnel Tunnel handles discovery; inner proxy still terminates TLS for Gateway.
Existing Nginx host Nginx Existing stack Existing stack Add a server block; avoid double TLS unless intentional.

6.1 launchd and Homebrew services

Prefer brew services start … or explicit LaunchAgents so proxies survive SSH disconnects. Ordering principle: the proxy may start before Gateway returns 200—configure short upstream retries or health gates in your orchestration doc.

6.2 Headless acceptance snippets

# Process check
ssh user@mac 'pgrep -lf "caddy|nginx" | head'

# Listener on 443
ssh user@mac 'lsof -nP -iTCP:443 -sTCP:LISTEN'

# TLS headers (adjust SNI / resolve)
ssh user@mac 'curl -sI --resolve gw.example.com:443:127.0.0.1 https://gw.example.com/health | head'

# WebSocket upgrade probe (raw backend)
ssh user@mac 'curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGVzdA==" \
  http://127.0.0.1:18789/ 2>&1 | head -8'

7. FAQ

Q1: Browser shows "WebSocket is closed before the connection is established"?

A: On Nginx, verify Upgrade/Connection headers and read timeouts. On Caddy, check whether a CDN or tunnel strips Upgrade.

Q2: SSE first byte takes tens of seconds?

A: Buffering is still on (Nginx proxy_buffering or missing flush settings in Caddy). Disable buffering for streaming routes.

Q3: Logs only show the proxy IP in X-Forwarded-For?

A: Expand trusted hop configuration to include the real immediate client of the proxy (loopback for same-box). If Cloudflare fronts you, trust Cloudflare's published ranges or terminate there intentionally.

Q4: CORS says blocked but Origin looks correct?

A: allowedOrigins is usually exact string match—https://gw.example.com differs from https://gw.example.com:443. Fix the allowlist, do not paper over in the proxy.

Q5: Multiple Gateway ports behind one proxy?

A: Use distinct hostnames per environment, each mapping to its own 127.0.0.1:port. Avoid path-based routing for WebSocket-heavy APIs unless every client supports the same path layout.

8. Why Mac mini / macOS for this edge

A reverse proxy plus Gateway is a 24/7 control plane: you want low idle power, quiet thermals, and a service manager you can reason about from SSH alone. macOS gives launchd for deterministic restarts, OpenSSH and Homebrew paths your operators already know, and Apple Silicon Mac mini systems routinely idle in the very low watt range while still delivering strong single-thread headroom for TLS and JSON fan-out.

Gatekeeper, SIP, and FileVault materially reduce drive-by malware risk compared with many commodity Windows images, while unified memory keeps proxy buffers and Gateway workers on the same fast fabric. If you are standardizing OpenClaw ingress on hardware that will not fight you on sleep, thermals, or background daemons, Mac mini M4 is a sensible 2026 baseline—visit the home page via the CTA below to compare plans and ship this matrix on metal that stays efficient when nobody is logged in at the console.

Recommended Plans

M4.S Best Seller

10-Core 16GB 256GB
$105.9
/ mo
Get Now