Wildcard Domains: One Certificate, Deliberately Narrow
Every deployed app lives at {project.slug}.{yourdomain.com}, and the
whole TLS story for this system is built around exactly one certificate:
a wildcard covering *.yourdomain.com — and, deliberately, nothing
else. This page covers why that narrowness is a feature, how the
certificate actually gets issued (wildcards are harder than ordinary
certs), and what changes if you want apps under a deeper namespace like
*.apps.yourdomain.com.
Why the apex is left out — on purpose
The dashboard has no public hostname at all. Nothing on the box answers
for yourdomain.com, www.yourdomain.com, or
api.yourdomain.com — the control plane is loopback-only, reached via
SSH tunnel (see SELF-HOSTING.md and the network
posture in auth/README.md).
That decision cascades straight into the certificate: it requests only
*.yourdomain.com. Requesting the apex too would be pointless — nothing
serves it — and would additionally require you to prove control of a
domain whose apex you may have deliberately pointed somewhere else.
And that's the practical payoff of the wildcard-only design: DNS delegation is per-record, not per-domain. You point only the wildcard record at your box:
yourdomain.com A/CNAME -> (unchanged — wherever it already points)
*.yourdomain.com A -> <VPS IP> (new — wildcard only)
An existing site living at the apex keeps working untouched. Deploying a
project called hello gives you hello.yourdomain.com served by your
box while the bare domain still serves whatever it served before.
How the certificate gets issued
A wildcard certificate cannot be issued via the common HTTP-01
challenge at all — there's no way to serve a challenge file that proves
you control every possible subdomain at once. The only mechanism is the
DNS-01 challenge: you publish a one-time TXT record under
_acme-challenge.yourdomain.com proving you control the domain's DNS,
and the certificate authority issues the cert.
scripts/lib/issue-certificate.sh runs this flow for you, two ways:
- Unattended: pass a Cloudflare API token (
--cloudflare-tokenoninstall.sh) scoped to Edit zone DNS on your domain's zone. The script creates and cleans up the TXT record automatically — and, more importantly, the same token lets the renewal cron job renew unattended forever. - Interactive: no token? The script pauses mid-flow and shows you the exact TXT record to create with whatever DNS provider you use; you add it by hand, press Enter once it's propagated, and continue. Works with any provider; the tradeoff is each renewal (~every 60 days, though the cron job handles remembering) needs the same manual step.
Either way the result is the same: one certificate covering every
possible single-label subdomain — hello.yourdomain.com,
my-company.yourdomain.com, anything a project slug can produce — plus,
covered by the same wildcard, hooks.yourdomain.com if you turn on
push-to-deploy. No per-subdomain certificates, ever; a new deployment
needs zero certificate work.
Renewal
install.sh installs /etc/cron.d/dreamer-local-engine-cert-renewal,
running scripts/renew-certs.sh daily. The underlying client only
actually renews within 30 days of expiry, so most days this is a no-op —
harmless to run daily. With the Cloudflare-token path this is fully
hands-off; with the interactive path, re-run the issuance script by hand
when the cron job starts reporting an upcoming expiry.
Two-level wildcards: *.sub.yourdomain.com
*.yourdomain.com and *.sub.yourdomain.com look like the same idea at
different depths. For TLS purposes they're different problems entirely:
proving you control yourdomain.com says nothing about whether you
intended to hand out certificates for an entire second-level namespace
underneath it, so a CA will happily issue the first via one DNS proof
but requires its own explicit validation for the second. Either can be
obtained through the same DNS-01 flow above — but nobody hands the
two-level case to you automatically; it's always yours to manage.
Does the routing code need to understand two levels?
No — and this is worth knowing before reaching for a code change.
reverse-proxy's routing assumes deployed apps live one label under
whatever BASE_DOMAIN is set to. If you want apps under
*.apps.yourdomain.com instead, the simplest fix isn't touching any
routing code — it's installing with --domain apps.yourdomain.com.
From the platform's perspective that string IS the "apex" it sets up
one-level automation for; the fact that it's also two levels below your
real registrar apex is a DNS/certificate detail that lives entirely
outside the application. You still need a certificate specifically
covering *.apps.yourdomain.com — but zero lines of routing change.
If you also want something else serving at the true apex
(yourdomain.com), remember that hostname now sits OUTSIDE the
certificate requested in step 1 — it needs its own separate certificate
and its own server block, managed however the rest of that site is
managed. The installer deliberately doesn't try to juggle both in one
pass; splitting namespaces is a manual extension, not a supported flag.
The mental model, compressed
- One certificate, one DNS record, one renewal cron covers every app
you'll ever deploy at
{slug}.yourdomain.comand the optional webhook host. - The apex stays yours — pointed wherever you want it, untouched by this system, unrequested by its certificate.
- DNS-01 is non-negotiable for wildcards; the Cloudflare-token path exists purely to automate the TXT-record dance, not because the certificate itself is special.