How It Works
How It Works
DNS Resolver is a thin shell entrypoint that generates configuration for two co-managed processes: CoreDNS and docker-gen.
Startup sequence
- The container starts and
/templates/entrypoint.shruns. - The script validates that
DOMAINandHOST_IPare set. If either is missing, it exits immediately with an error. - It computes a date-based zone serial (
YYYYMMDDNNusingdate -u +'%Y%m%d%H'). - It determines the
DNS_DOMAIN_FORWARDdirective based onDNS_AUTHORITATIVE:true→ empty string (no forward stanza; unknown names returnNXDOMAIN)false→forward . ${DNS_UPSTREAM}(unknown names forwarded upstream)
- It builds the
ACME_DNS_BLOCKstanza (empty unlessACME_DNS_ENABLED=true). - It generates
/etc/coredns/zones/db.${DOMAIN}fromzone.templateusingenvsubst. - It generates
/etc/coredns/CorefilefromCorefile.templateusingenvsubst. - It seeds an empty
/etc/coredns/hostsfile (populated at runtime by docker-gen). - It starts docker-gen in the background:
docker-gen -watch -wait ${DOCKERGEN_WAIT} /templates/hosts.tmpl /etc/coredns/hosts. - It starts CoreDNS in the background:
coredns -conf /etc/coredns/Corefile. - It installs a
SIGTERM/SIGINTtrap to forward signals to both child processes for clean shutdown. - It waits (
wait) until either child exits, then the container stops.
Zone file generation
The static zone file (zone.template) is processed with a restricted envsubst call:
envsubst '${DOMAIN} ${HOST_IP} ${SERIAL}' \
< /templates/zone.template \
> "/etc/coredns/zones/db.${DOMAIN}"
The explicit variable list prevents envsubst from replacing DNS zone directives that begin with $ (e.g. $TTL, $ORIGIN).
The generated zone contains:
- A
SOArecord forDOMAIN - An
NSrecord pointing tons.DOMAIN - An
Arecord forns→HOST_IP - An
Arecord for the root domain →HOST_IP - A wildcard
Arecord:*→HOST_IP
CoreDNS Corefile structure
The generated Corefile has two blocks:
Domain block — serves DOMAIN:53:
fileplugin: loads the static zone (wildcard A records), reloads everyZONE_RELOADhostsplugin: loads the container hosts file, reloads everyHOSTS_RELOAD, falls through on miss- Optional
forwarddirective (split-horizon mode only) errorsandlogplugins
Catch-all block — serves .:53:
forwardtoDNS_UPSTREAMcache 300(5-minute TTL for upstream responses)errorsandlog
Optional ACME block — appended when ACME_DNS_ENABLED=true:
- Serves
auth.DOMAIN:53 forwardtoacme-dns:53errorsandlog
docker-gen template
templates/hosts.tmpl is the Go template that docker-gen processes on every container event:
{{- range $c := . }}
{{- if and $c.State.Running $c.Networks }}
{{- with index $c.Networks 0 }}
{{- if .IP }}
{{ .IP }} {{ $c.Name }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
For each running container with at least one network, it emits one line:
<container-IP> <container-name>
CoreDNS loads this as a standard hosts file, making container names resolvable by their first network IP. The template uses slice-index syntax (index $c.Networks 0) because nginxproxy/docker-gen v0.16+ exposes Networks as a slice rather than a map.
Debounce and reload timing
The end-to-end latency from a container start to DNS resolution:
Container starts
└─ docker-gen detects event
└─ waits DOCKERGEN_WAIT (min:max, default 5s:30s)
└─ rewrites /etc/coredns/hosts
└─ CoreDNS re-reads hosts file after HOSTS_RELOAD (default 15s)
└─ name is resolvable
Worst-case latency with defaults: 30s + 15s = 45 seconds. For dynamic environments, reduce both values.