Reference

How It Works

Internals of DNS Resolver — entrypoint, config generation, CoreDNS zone, and docker-gen template.

How It Works

DNS Resolver is a thin shell entrypoint that generates configuration for two co-managed processes: CoreDNS and docker-gen.

Startup sequence

  1. The container starts and /templates/entrypoint.sh runs.
  2. The script validates that DOMAIN and HOST_IP are set. If either is missing, it exits immediately with an error.
  3. It computes a date-based zone serial (YYYYMMDDNN using date -u +'%Y%m%d%H').
  4. It determines the DNS_DOMAIN_FORWARD directive based on DNS_AUTHORITATIVE:
    • true → empty string (no forward stanza; unknown names return NXDOMAIN)
    • falseforward . ${DNS_UPSTREAM} (unknown names forwarded upstream)
  5. It builds the ACME_DNS_BLOCK stanza (empty unless ACME_DNS_ENABLED=true).
  6. It generates /etc/coredns/zones/db.${DOMAIN} from zone.template using envsubst.
  7. It generates /etc/coredns/Corefile from Corefile.template using envsubst.
  8. It seeds an empty /etc/coredns/hosts file (populated at runtime by docker-gen).
  9. It starts docker-gen in the background: docker-gen -watch -wait ${DOCKERGEN_WAIT} /templates/hosts.tmpl /etc/coredns/hosts.
  10. It starts CoreDNS in the background: coredns -conf /etc/coredns/Corefile.
  11. It installs a SIGTERM/SIGINT trap to forward signals to both child processes for clean shutdown.
  12. 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 SOA record for DOMAIN
  • An NS record pointing to ns.DOMAIN
  • An A record for nsHOST_IP
  • An A record for the root domain → HOST_IP
  • A wildcard A record: *HOST_IP

CoreDNS Corefile structure

The generated Corefile has two blocks:

Domain block — serves DOMAIN:53:

  • file plugin: loads the static zone (wildcard A records), reloads every ZONE_RELOAD
  • hosts plugin: loads the container hosts file, reloads every HOSTS_RELOAD, falls through on miss
  • Optional forward directive (split-horizon mode only)
  • errors and log plugins

Catch-all block — serves .:53:

  • forward to DNS_UPSTREAM
  • cache 300 (5-minute TTL for upstream responses)
  • errors and log

Optional ACME block — appended when ACME_DNS_ENABLED=true:

  • Serves auth.DOMAIN:53
  • forward to acme-dns:53
  • errors and log

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.

Copyright © 2026