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:
| Mode | When | Needs |
|---|---|---|
acme (default) | host is internet-reachable on :80/:443 | nothing — automatic Let’s Encrypt |
dns01 | behind NAT but you own the domain | a caddy-dns image (POND_CADDY_IMAGE) + POND_DNS_PROVIDER + POND_DNS_API_TOKEN |
byo | you 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):
| Var | Tier 1 / Tier 2 | Why |
|---|---|---|
POND_FORWARDED_ALLOW_IPS=* | both | trust X-Forwarded-Proto/For from the proxy (uvicorn) |
POND_CP_HOST=127.0.0.1 | both | plaintext control-plane port → loopback-only |
POND_PUBLIC_URL, CORS_ORIGINS | both | advertise the real https:// origin |
POND_BUNDLE_PUBLIC_ENDPOINT_URL | Tier 2 + workers | presign 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.