Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Reverse Proxy (nginx / Caddy)

The daemon serves its HTTP API + UI on status_listen (default :18334) as plain HTTP. For a public or friends pool you usually want it behind a reverse proxy that terminates TLS (HTTPS) on port 443, so miners and the dashboard reach it over an encrypted, named endpoint (https://pool.example.com).

You have two ways to get HTTPS — pick one:

  1. Built-in TLS — set status_tls_listen (and provide a cert) and the daemon serves HTTPS directly. Simplest; no extra software. See Configuration Reference.
  2. Reverse proxy (this page) — an established web front (nginx / Caddy) does TLS, HTTP/2, automatic certificate renewal, access logs and its own rate-limit, forwarding to the daemon over loopback. This is the recommended setup once the pool is internet-facing.

Stratum is not HTTP. Only the API/UI (:18334) goes through an HTTP reverse proxy. The Stratum endpoints — plain :3333, TLS :3334, and Stratum V2 on its own sv2_listen port (the reference deployment uses :3336, NOISE NX handshake) — are raw TCP protocols. Expose those ports directly (or through an L4/TCP load balancer such as nginx stream {} or HAProxy TCP mode), never through an HTTP server {} / reverse_proxy block.

Two settings that matter behind a proxy

When the daemon runs behind a proxy, set both of these in config.toml under [server]:

[server]
# The proxy now sees the real client; the daemon only sees the proxy's loopback
# address. Setting this true makes the daemon trust the proxy's X-Forwarded-For
# header for the real client IP — used by the audit log and the per-IP rate-limit
# and ban list. See the warning below.
trust_proxy_headers = true

# The login cookie is only sent over HTTPS. Set true whenever the UI is reached
# via https:// (i.e. always, behind a TLS-terminating proxy).
cookie_secure = true

⚠️ Only set trust_proxy_headers = true behind a proxy you control. If the daemon is reachable directly from the internet with this on, any client can forge X-Forwarded-For and spoof its IP into your audit log and past your IP bans. Bind status_listen to loopback (127.0.0.1:18334) so only the proxy can reach it. See Security.

Public wallet pages (/users/<address>)

A per-worker 0% pool exposes a login-free page per miner at /users/<address>, with ckpool-compatible JSON at /api/users/<address> for firmware and ckstats. Both are ordinary HTTP on the same status port as the dashboard, so the catch-all location / (nginx) / reverse_proxy (Caddy) below already forwards them — no per-route config needed (unlike /api/events, they are plain request/response, no buffering tweaks). Just confirm they answer through the proxy: curl https://pool.example.com/api/users/<some-address> should return the ckpool-style JSON, and https://pool.example.com/users/<address> should load the wallet page.

Server-Sent Events (/api/events)

The dashboard’s live updates (block-found, new-job, …) use a long-lived Server-Sent-Events stream at GET /api/events. A proxy that buffers responses or times the connection out will break live push. Both examples below disable response buffering and raise the read timeout for that path.

Caddy

Caddy gets you automatic HTTPS (Let’s Encrypt) with almost no config:

pool.example.com {
    reverse_proxy 127.0.0.1:18334 {
        # SSE: stream the response, don't buffer it.
        flush_interval -1
    }
}

That’s the whole file. Caddy fetches and renews the certificate, terminates TLS, forwards X-Forwarded-For / X-Forwarded-Proto automatically, and flush_interval -1 keeps /api/events flowing.

nginx

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

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

    # Dashboard, API, and the public wallet pages (/users/<address> +
    # ckpool-compatible /api/users/<address> JSON) — all plain HTTP, no
    # per-route tuning needed.
    location / {
        proxy_pass http://127.0.0.1:18334;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Server-Sent-Events stream: no buffering, long-lived connection.
    location /api/events {
        proxy_pass http://127.0.0.1:18334;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header Connection        '';
        proxy_buffering off;
        proxy_cache off;
        proxy_read_timeout 1h;
    }
}

# Redirect plain HTTP → HTTPS
server {
    listen 80;
    server_name pool.example.com;
    return 301 https://$host$request_uri;
}

Use certbot (certbot --nginx -d pool.example.com) for the certificate.

Checklist

  • status_listen = "127.0.0.1:18334" — daemon only reachable via the proxy.
  • trust_proxy_headers = true and cookie_secure = true in [server].
  • /api/events is proxied without buffering (Caddy flush_interval -1 / nginx proxy_buffering off).
  • Public wallet pages reachable through the proxy: /users/<address> loads and /api/users/<address> returns JSON (per-worker pools only; served by the catch-all block, no special config).
  • Stratum ports (:3333 plain, :3334 TLS, sv2_listen e.g. :3336) exposed directly, not via the HTTP proxy.
  • After changing [server], restart the daemon.