Exposing pond (TLS, public URL, behind NAT)

By default the compose stack serves the control plane as plain HTTP on localhost. This guide makes it reachable — with TLS, from another machine, or from behind NAT — without any third-party SaaS. Everything here is opt-in via env vars install.sh reads; leave them empty and nothing changes.

Two questions decide what you need:

  • Does this host have a public IP (or can open :80/:443)? Tier 1 (a bundled Caddy front door).
  • Is it behind NAT with no inbound? Tier 2 (dial out to a relay).

And one more, orthogonal:

  • Remote consumer, workers stay local (the common case) expose only the control plane. Scenario A.
  • Workers run on other machines also expose the orchestrator + bundle store. Scenario B (POND_RELAY_WORKERS=true).

Tier 1 — your own subdomain (public IP)

A Caddy service terminates TLS for your domain and reverse-proxies the control plane. The plaintext port drops to loopback-only; Caddy is the sole ingress.

# DNS: point an A/AAAA record for pond.acme.com at this host first.
POND_DOMAIN=pond.acme.com ./install.sh          # → https://pond.acme.com

Three TLS modes via POND_TLS_MODE:

ModeWhenNeeds
acme (default)host is internet-reachable on :80/:443nothing — automatic Let’s Encrypt
dns01behind NAT but you own the domaina caddy-dns image (POND_CADDY_IMAGE) + POND_DNS_PROVIDER + POND_DNS_API_TOKEN
byoyou have a cert (real, internal-CA, or self-signed)cert.pem+key.pem in POND_TLS_CERT_DIR
# bring-your-own cert
POND_DOMAIN=pond.acme.com POND_TLS_MODE=byo POND_TLS_CERT_DIR=/etc/pond/tls ./install.sh

Tier 2 — behind NAT (no inbound)

The host dials out to get a public URL — no open ports, no cert here. Two options, depending on whether you’d rather avoid a VM or avoid a third party in the path:

Option A — Cloudflare Tunnel (no VM)

cloudflared dials out to Cloudflare’s edge, which routes your hostname back down it. Cloudflare IS the relay — no VM, no extra bill (free tier). The trade-off: Cloudflare terminates TLS, so it sees /v1 traffic. Recommended when you’d rather not run a box.

# After creating a tunnel + public hostname in the Cloudflare dashboard:
POND_CF_TUNNEL_TOKEN=<tunnel token> \
POND_CF_HOSTNAME=acme.pond.neotype.io \
./install.sh                                     # → https://acme.pond.neotype.io

Full setup (dashboard steps + multi-tenant): deploy/cloudflared/.

Option B — self-hosted relay (no third party in the path)

Dial out to a relay VM you run, so traffic never transits a third party. The relay terminates TLS on infrastructure you own.

POND_RELAY=relay.neotype.io POND_RELAY_DOMAIN=pond.neotype.io \
POND_RELAY_TOKEN=<from the relay operator> POND_SUBDOMAIN=acme \
./install.sh                                     # → https://acme.pond.neotype.io

Use a relay someone else runs, or self-host one (frp + Caddy, one command) — see deploy/relay/.

Scenario B — off-host workers over the tunnel/relay

To let workers on other machines claim jobs, also expose the orchestrator and the (encrypted) bundle store:

POND_RELAY=pond.neotype.io POND_RELAY_TOKEN=… POND_SUBDOMAIN=acme \
POND_RELAY_WORKERS=true ./install.sh

The control plane must presign the bundle download URL against the public store host (uploads stay internal — SigV4 signs the host, so the presign must match what the worker fetches), via POND_BUNDLE_PUBLIC_ENDPOINT_URL. The self-hosted relay (POND_RELAY_WORKERS=true) derives it for you; with Cloudflare Tunnel you add orch-…/s3-… public hostnames in the dashboard and set POND_BUNDLE_PUBLIC_ENDPOINT_URL=https://s3-acme.pond.neotype.io yourself. Then attach a worker from anywhere:

python swarm/swarm.py worker \
  --orchestrator https://orch-acme.pond.neotype.io \
  --token <POND_POOL_TOKEN> --capabilities harness.codex,sandbox.docker --repo-root /tmp/pondwork

Bundles are ChaCha20Poly1305-sealed per worker, so exposing the store is safe.

What gets set under the hood

install.sh derives these into .env (so you don’t have to):

VarTier 1 / Tier 2Why
POND_FORWARDED_ALLOW_IPS=*bothtrust X-Forwarded-Proto/For from the proxy (uvicorn)
POND_CP_HOST=127.0.0.1bothplaintext control-plane port loopback-only
POND_PUBLIC_URL, CORS_ORIGINSbothadvertise the real https:// origin
POND_BUNDLE_PUBLIC_ENDPOINT_URLTier 2 + workerspresign bundle URLs for the public store host

Compose profiles gate the new services (frontdoor Caddy, cftunnel cloudflared, relay frpc), so a plain docker compose up with no exposure vars is byte-for-byte unchanged.

k8s? The Helm chart already does ingress + cert-manager TLS — see deploy/. This guide is the docker-compose equivalent.