Routing

Every app gets its own subdomain of one of the instance's domains: notes.mycooldomain.com is the app named notes. You never create a DNS record or issue a certificate per app; a wildcard covers all of them.

Domains

An instance answers on one or more domains, managed in Settings → Domains. One is the primary: the canonical domain used for outbound links and background tasks. Each domain is one of:

  • Public (HTTPS): a real domain served over TLS. The usual case.
  • Local HTTP: a plain-HTTP domain such as lvh.me:8080, served by the router directly. Used before an instance is taken public.

Host matching is longest-suffix, so overlapping domains resolve to the most specific one.

DNS

For a public domain, you delegate the whole zone to the machine rather than pointing an A record at it (see Exposing a server with a static IP). The instance then runs CoreDNS as the authoritative nameserver for the zone and serves:

  • an A record for the domain itself,
  • a wildcard A record for *.domain, so every app subdomain resolves,
  • TXT records under _acme-challenge.domain, written only while a certificate is being issued.

CoreDNS reloads automatically when the zone file changes.

Delegating the zone is what makes both the wildcard and DNS-01 certificate validation work without you doing anything per app. If you would rather manage DNS yourself, you can: point domain and *.domain at the machine at your provider, disable CoreDNS, and supply certificates yourself; the home server page shows this with acme.sh.

TLS certificates

Each TLS domain gets one certificate covering both domain and *.domain, validated over ACME DNS-01: the instance writes the challenge TXT record into its own CoreDNS zone, the CA reads it, and the record is removed afterwards.

There are two ways to reach a CA:

  • Your own ACME account: configure acme_account_key_path, acme_email, and acme_directory_url (Let's Encrypt by default). The instance talks to the CA directly.
  • The Imbue certificate broker: used by managed spaces. The instance generates its keypair and CSR locally and sends only the CSR; the private key never leaves the machine.

Certificates are cached on disk and reused across restarts. A background task checks every 12 hours and renews anything expiring within 7 days, then reloads Caddy. A domain whose certificate has not landed yet is served with Caddy's self-signed certificate, so it is reachable, with a browser warning, while acquisition is in flight.

The proxy path

Caddy sits in front and does nothing but TLS:

  • :443: terminates TLS and reverse-proxies to the router on loopback :8080.
  • :80: permanently redirects to HTTPS.

Caddy's config is generated by the router at startup; there is no Caddyfile to edit. An HTTP-only install runs with start_caddy = false and no Caddy at all; the router serves :8080 directly. Configuring a TLS domain without Caddy is refused at startup, since nothing would terminate TLS.

The router then matches the Host header to a domain, extracts the app name from the subdomain, and proxies to that app's container port. Requests for the bare domain are handled by the router itself: the dashboard, the API, and the docs you are reading.

What the app sees

The router forwards HTTP and WebSocket traffic alike, and sets a few headers on the way in:

  • X-Forwarded-For: the real client IP, taken from Caddy.
  • X-OpenHost-Is-Owner: true is set only when the request carries a valid owner session.

Any X-OpenHost-* header from the client is stripped first; the router is the only thing allowed to set them.

Apps that speak something other than HTTP can bind extra host ports with [[ports]] in the manifest; those bypass the router and are exposed directly on the machine. See the manifest spec.

This page lives in the Cloud in a Bottle repo. Suggest an edit. Prefer the raw markdown? Add .md to the URL, or grab the whole manual at all.md.