PLATINUM DOCS
Self-hosting

Domains & TLS

Serve the API, preview URLs and the console over HTTPS with Caddy.

Caddy on the control plane serves the API, preview URLs and the console over HTTPS, with certificates from Let's Encrypt.

Create DNS records

Point every name at the control plane, never at a sandbox host.

RecordTypeFor
api.example.comAThe API, SDKs, CLI and the host installer
*.sbx.example.comAPreview URLs, if you turn them on
app.example.comAThe console, if you turn it on

A preview hostname is <port>-<sandbox>.sbx.example.com: one label under the domain, so one wildcard record covers every sandbox.

dig +short api.example.com              # the control plane's address
dig +short 8080-test.sbx.example.com    # the same address

Ports 80 and 443 must be reachable from the internet for the HTTP-01 challenge.

Set the domains

setup.sh writes them.

# on the control plane, in platinum/infra/compose
./setup.sh --domain api.example.com --sbx-domain sbx.example.com
docker compose up -d
SettingEffect
PT_DOMAINCaddy gets a certificate for this name. The API URL is https://<PT_DOMAIN>.
PT_SBX_DOMAINTurns on the preview site. Unset, preview URLs use <port>-<sandbox>.127.0.0.1.nip.io, which resolves only on the control plane.
PT_CONSOLE_DOMAINTurns on the console site. See Console.

Choose how preview certificates are issued

On demand is the default and needs nothing more. Caddy fetches one certificate per preview hostname on its first request, which takes a few seconds. Before each one it asks the server, which says yes only for hostnames of existing sandboxes.

Let's Encrypt allows 50 new certificates per registered domain per week. For many previews, use one wildcard certificate instead.

Use a wildcard certificate

A wildcard needs the DNS-01 challenge. The stock Caddy image has no DNS modules, so build one with yours:

FROM caddy:2.8.4-builder AS b
RUN xcaddy build --with github.com/caddy-dns/<provider>
FROM caddy:2.8.4
COPY --from=b /usr/bin/caddy /usr/bin/caddy

Then set DNS_API_TOKEN in .env, and in the Caddyfile preview block replace on_demand with dns <provider> {env.DNS_API_TOKEN}.

Serve previews without a domain

On a private network, preview URLs can use plain HTTP and nip.io. Preview tokens then travel in clear.

# in platinum/infra/compose/.env
PT_SBX_DOMAIN=<private-ip>.nip.io
PT_SBX_PUBLIC_PORT=80

Then uncomment the plain-HTTP preview block at the end of the Caddyfile and run docker compose up -d.

Keep the proxy settings

The Caddyfile sets these for the server. Keep them when you edit it.

SettingWhy
header_up Host {host}The server matches preview hostnames on it
header_up X-Real-IP {remote_host}The real client address, for rate limits and the admin allowlist
versions 1.1Keeps WebSocket upgrades for terminals and previews simple
response_header_timeout 90sMust exceed the longest blocking call, a create with wait_for_state
flush_interval -1Streams exec output, logs and server-sent events as they are written

The server's :3000 is not published. Caddy is the only way in.

Check it

curl -s -o /dev/null -w '%{http_code}\n' https://api.example.com/health   # 200
docker compose logs -f caddy                                               # certificate issuance

Then expose a port on a running sandbox and open the URL it returns. See Networking.