Skip to content

Domains, DNS and certificates ​

Three things have to line up before https://my-app.dqx opens: the name has to resolve, something has to answer on port 443, and the certificate it presents has to be one your browser trusts. dqx arranges all three, and they fail in different ways - so it is worth knowing which is which.

The two families of domain ​

dqx works with two kinds of local domain, and they resolve by different routes.

dqx's own suffixes - resolved on your machine ​

A new project gets .dqx. You can serve others beside it, .internal for one. No public DNS server knows anything about these, so each machine has to answer for itself.

On macOS that is /etc/resolver/<suffix>, here /etc/resolver/dqx:

text
nameserver 127.0.0.1
port 5354

macOS reads /etc/resolver/<suffix> and sends every query for that suffix to the address in the file. Port 5354 is dqx's own DNS server - a process listening on 127.0.0.1:5354 that answers 127.0.0.1 for anything under a suffix it serves. The installer sets it up (dqx resolver install), and dqx suffix lists what is served.

Deliberately not port 53, and deliberately not 5300: Lerd's dnsmasq listens on 5300 and Herd runs its own. Two servers fighting over one port is not a fallback, it is a coin toss.

Only on this machine

A colleague who clones the project and opens https://my-app.dqx gets nothing until dqx has set up their resolver too. That is not a bug - it is what a suffix no public DNS knows means.

*.local.example.eu under a real domain - resolved by public DNS ​

The other family is a wildcard on a domain the company actually owns, with a public DNS record pointing at 127.0.0.1. Dolphiq's is local.dolphiq.eu, and a Dolphiq machine serves it beside .dqx. It looks like this:

bash
$ dig +short anything.local.example.eu @8.8.8.8
127.0.0.1

That record is real, public, and answered by Google's resolver - for any subdomain, on any machine, with nothing installed. A colleague, a new laptop, a CI runner: they all resolve it, and they all get their own loopback address.

It costs one wildcard record in a domain you own. In exchange, a project's domains work on a machine where dqx has never run, and nobody has to be talked through editing /etc/hosts. dqx's resolver answers local.dolphiq.eu too, so it also works offline and beside Herd.

What about .test

.test works when a project states it, or after dqx suffix add test. It is the suffix Herd, Valet and Lerd use, so dqx does not take it by default. dqx next to Herd has the details.

Which to use

.dqx for anything new: it needs no DNS record and works offline. A wildcard under your own domain where the domain has to resolve on a machine without dqx.

What answers on 443 ​

Every project could publish its own ports, and then only one project could run at a time - 80 and 443 have a single owner per machine. So dqx runs one shared reverse proxy (Traefik) that owns both, and every project's web container sits behind it on an internal network, publishing nothing.

The proxy is told about a project through a small file per project, generated by dqx:

yaml
http:
  routers:
    my_app:
      rule: Host(`my-app.dqx`) || Host(`app.my-app.local.example.eu`)
      entryPoints: [websecure]
      service: my_app
      tls: {}
  services:
    my_app:
      loadBalancer:
        servers:
          - url: "http://my_app_web:80"

Every domain a site declares - primary and aliases - becomes part of that rule. A missing alias is the failure that looks like nothing: Traefik has no route for the host, so it answers its own bare 404 page not found, which mentions neither your project nor a domain.

Behind the proxy, the project's own nginx picks the site by server_name and serves its docroot. That is the second place a domain has to be listed, and dqx generates both from one declaration precisely so they cannot disagree.

The certificate ​

A local site over https:// needs a certificate your browser trusts. dqx uses mkcert, which creates a local certificate authority, installs it into your system trust store once, and then signs certificates with it.

  • One certificate per project. Each project gets its own, carrying its own domains and nothing else, in ~/.local/share/dqx/router/certs/<key>-<project id>.crt. Traefik holds the list and picks on SNI, so a browser is handed the certificate of the project it asked for. Measured on this machine: 76 of them.
  • It follows the project, in both directions. A domain the project stops declaring leaves its certificate, a certificate whose project is gone from disk is dropped by dqx router:up, and dqx remember --off removes a forgotten project's certificate at once. The one certificate dqx used to write for the whole machine could only ever grow, and it still carried names of projects deleted months earlier.
  • The container trusts it too. The root certificate is copied into the app container, because a site calling another site over https:// fails inside the container while the same URL works in your browser - the browser was told about the CA, the container never was.

dqx doctor reports a certificate that does not cover a domain, and dqx up reissues that project's. The shared router has the rest: why it is one per project rather than one per machine, and what happens to the old shared one.

When it does not work ​

symptomwhat it means
Browser cannot resolve the host at allDNS. For .dqx, the resolver file or dqx's DNS server; for a wildcard domain, the public record
404 page not found, plain text, no stylingThe proxy has no route for that host - usually an alias missing from the declaration
The site's own 404 or 500You reached the project. It is nginx or the application now
"Your connection is not private"The certificate does not cover this domain, or the CA is not in your trust store
Bind for 0.0.0.0:443 failedSomething else owns 443 - another backend, or a project stack that predates the shared proxy

dqx doctor names each of these, and dqx verify asks every declared domain what it actually answers.

dqx v1.78.0