dozeGitHubGet doze
doze

Your own domain, on your laptop, over https.

Put dev.acme.com in doze.hcl and run doze trust once. Your browser gets a padlock it believes, your sign-in provider gets the https callback it insists on, and every tenant under *.dev.acme.com answers too. Served from this machine, by doze, with no web server of your own to configure.

doze.hcldomains = ["dev.acme.com", "*.dev.acme.com"]
once$ doze trust
https://larder.dev.acme.com: a tenant, under *.dev.acme.com. A drawing of what you see, on your laptop, with the stack up.
Why you’d want it

localhost gets you most of the way. Then you add sign-in.

redirect_uri_mismatch

Sign-in providers want a real name.

Google, for one, accepts plain http://localhost, but not acme.localhost, not a .test name and not a bare IP address. Anything else must be https, under a public top-level domain. The moment your app has tenants or a callback of its own, that means a real domain.

Set-Cookie: …; Domain=.acme.com

Cookies, CORS and HSTS go by host.

A cookie for .acme.com, a CORS allow-list that names https://dev.acme.com, an HSTS header: each is about the host. On localhost:3000 you’re testing a different setup from the one you ship.

*.dev.acme.com

Tenants are subdomains.

A customer per subdomain is one wildcard line. larder.dev.acme.com and any name you make up next answer at once, each with its own certificate, made on its first visit.

The usual way, and doze

The usual way works. There’s just a lot of it.

Caddy and nginx are fine web servers, built to serve production traffic. mkcert makes certificates your machine trusts, with no configuration. Each does its job well. Stitching them into a dev setup is the part that doesn’t travel with the repo.

The usual wayby hand, on each laptop

  1. A reverse proxy, to put a name in front of a port
    dev.acme.com {
      reverse_proxy localhost:3000
    }

    Caddy or nginx, configured by hand and kept running. Caddy can even be its own local authority (tls internal, then caddy trust).

  2. A certificate your browser believes
    mkcert -install
    mkcert dev.acme.com "*.dev.acme.com"

    mkcert is the right tool for this: it trusts its authority in the system store, in Firefox’s when certutil is there, and in Java’s when JAVA_HOME is set.

  3. /etc/hosts, one line per name
    127.0.0.1  dev.acme.com
    127.0.0.1  larder.dev.acme.com

    The hosts file has no wildcards, so every tenant is another line, and another sudo.

  4. Each runtime told about the authority
    export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"

    Node doesn’t read the keychain by default, and Python’s requests carries its own bundle, so a call from one service to another fails until you point each at the root.

  5. Then again, on every laptop
    # SETUP.md, step 7 of 12

    The proxy config and the hosts lines live on your machine, not in the repo. Each teammate does it again from a wiki page, and the wiki page drifts.

With dozein the repo, then once per machine

  1. One line, in the repo
    domains = ["dev.acme.com", "*.dev.acme.com"]

    On the app block that already has a port. It travels with the code, so every teammate’s doze.hcl already says it.

  2. doze trust, once per machine
    doze trust

    One admin prompt does both halves: a resolver route for each domain, and a certificate authority of this machine’s own, trusted. doze trust --print shows the script first.

  3. A wildcard is a route, not a list
    /etc/resolver/dev.acme.com

    One resolver file covers the name and every name under it. Each tenant gets its own certificate on its first visit.

  4. Every process in the stack already trusts it
    SSL_CERT_FILE  NODE_EXTRA_CA_CERTS
    REQUESTS_CA_BUNDLE  DOZE_CA_FILE

    Set on everything doze runs, so Go, curl, Node and Python verify without being told. The two that replace a store get your machine’s roots plus doze’s, so public hosts keep working.

  5. No port, no proxy to keep running
    https://dev.acme.com

    doze’s own front door answers on 443 and 80 while your stack is up, speaks HTTP/2, and sends a plain http request for a declared domain to https.

Already have a certificate? One from mkcert, or a real one for the domain: give the app cert and key, and doze serves yours and trusts nothing of its own. doze trust then installs the route only.

How it works

Two hard parts doze already had.

Every doze stack already has a resolver for its .doze names and a front door that routes by name. A domain is one more name for both, plus a certificate.

  1. Your browserhttps://larder.dev.acme.comAsks the system for the name.
  2. 1 · The resolver route/etc/resolver/dev.acme.commacOS sends that name, and every name under it, to doze’s resolver on 127.0.0.1:5323, which answers with a loopback address. Nothing else in .com is touched.
  3. 2 · doze’s front door, :443TLS, by namePicks a certificate for the name. The first handshake mints it from the doze authority, good for 30 days and renewed a week before. HTTP/2. On :80 it sends a declared domain to https.
  4. 3 · Your app127.0.0.x:3000On its own loopback address, served plain, exactly as pnpm dev serves it.
The doze authority · ~/.doze/ca
root.crt trusted once, in the System keychainroot.key readable by you alone (0600); never leaves the foldercerts/ the leaves the front door minted

When the stack is down, and no stack declares the name, the resolver forwards it to your usual DNS. The real dev.acme.com is back.

On Linux the route goes into systemd-resolved and the authority into the distribution’s bundle (update-ca-certificates or update-ca-trust), in the same single prompt.
No surprises

What doze trust changes, and how to take it back.

  • A file per domain in /etc/resolver. /etc/resolver/dev.acme.com, pointing at doze’s resolver. A file that isn’t doze’s is never written over or removed.
  • The doze authority, trusted. In the System keychain, as a root for TLS. Made only when a stack declares a domain; nothing is trusted before that.
  • A folder in your home. ~/.doze/ca: the root, its key (readable by you alone) and the certificates it minted.

Be clear about what trusting a root means. Like any root, it could sign any name, so whoever got hold of root.key could sign for this machine. doze only ever mints names declared here, and the key never leaves the folder. Keep ~/.doze/ca out of anything synced or shared.

Read it, check it, undo itone prompt each
$ doze trust --print    # the script, to read or paste yourself
$ doze trust --check    # the authority, and when it expires
$ doze trust --rotate   # a new one; the old one untrusted
$ doze trust --remove   # this project's routes, and the
                       # authority with the last project
$ doze dns-setup --uninstall
                       # everything doze set up for names

A domain that leaves doze.hcl leaves a stale route behind; doze up and doze doctor say so, and the next doze trust removes it in the same prompt.

Where things can get in the way

The edge cases, before you find them.

A VPN or a management profile

macOS consults configuration profiles before /etc/resolver, so one that sets DNS can win. doze doctor reports a domain that doesn’t resolve to the stack despite the route.

Firefox

Firefox 120 and later read the system keychain; older versions keep their own store. Chrome and Safari always read it.

Containers in the stack

A container doesn’t see the machine’s resolver routes. From inside one, use the app’s .doze name with its port.

Something else on 80 or 443

doze doesn’t fight Caddy or nginx for the port. doze doctor names what holds it, and the domain still works with the app’s port in the URL.

A resolver file of your own

doze never writes over or removes a file in /etc/resolver that isn’t its own. doze trust says so and leaves the domain to it.

One level of wildcard

*.dev.acme.com serves larder.dev.acme.com and dev.acme.com itself. a.b.dev.acme.com resolves but fails the handshake, as certificates do.

The real site

While a stack declares dev.acme.com, this machine sends it to the stack. When none does, the name goes to your usual DNS and the real host is back.

Java, and names off the public list

Java reads its own store: import DOZE_CA_FILE with keytool. A name like dev.acme.test is accepted, and doze doctor warns that sign-in providers will refuse it.

In the app

A banner, a row, a line. No new page.

The app does the same thing in the places you already look. Drawn here from its own screens until captures land.

The banner. On the stack’s page until this machine is set up for its domains. The button asks with macOS’s own password prompt; the stack runs meanwhile, at its .doze name.
The row. Inside the app, its card leads with the https address; the Library’s address pills show it first too, and doze url storefront prints it.
The line. Settings › Machine lists what doze set up here, part by part, each with the command that fixes it and Repair for all of them.
Try it

One line. One prompt. Your domain.

Add domains to an app that has a port, bring the stack up, and say yes once.

app "web" {
  run     = "pnpm dev"
  port    = 3000
  domains = ["dev.acme.com", "*.dev.acme.com"]
}
$ doze trust

Part of the doze family. Works alone.