Internal DNS resolver combining CoreDNS and docker-gen in a single container. Automatically populates DNS entries for every running container and resolves your domain zone to the host IP — ready to pair with Traefik for zero-config service routing.
Configured entirely via environment variables. No config file to maintain.
flowchart LR
client["🖥️ Client"]
subgraph container["dns-resolver container"]
coredns["CoreDNS\n:53 UDP/TCP"]
dockergen["docker-gen\n(background watcher)"]
end
traefik["Traefik\n(reverse proxy)"]
svc1["Service A"]
svc2["Service B"]
docker["🐋 Docker socket"]
client -->|"DNS query\n*.home.example.com"| coredns
coredns -->|"→ HOST_IP\n(wildcard zone)"| client
coredns -->|"container-name\n→ container IP\n(hosts file)"| client
docker -->|"container start/stop\nevents"| dockergen
dockergen -->|"rewrites\n/etc/coredns/hosts"| coredns
client -->|"HTTPS\nHost: svc-a.home.example.com"| traefik
traefik --> svc1
traefik --> svc2
docker-gen watches the Docker socket and rewrites
/etc/coredns/hostson every container event (DOCKERGEN_WAITdebounce, default 5 s–30 s). CoreDNS reloads the file atHOSTS_RELOADinterval (default 15 s), making new containers resolvable within seconds.
# Pull the image
docker pull ghcr.io/circle-rd/dns-resolver:latest
# Or build locally
git clone https://github.com/Circle-RD/dns-resolver.git
cd dns-resolver
docker build -t dns-resolver .Create a .env from the template and adjust values:
cp .env.example .env
$EDITOR .env
docker compose up -d| Variable | Required | Default | Description |
|---|---|---|---|
DOMAIN |
Yes | — | Internal domain zone (e.g. home.example.com) |
HOST_IP |
Yes | — | IP of the host; all *.DOMAIN queries resolve to this address |
DNS_UPSTREAM |
No | 1.1.1.1 |
Upstream resolver for external queries |
DNS_AUTHORITATIVE |
No | true |
true → NXDOMAIN for unknown names; false → forward unknowns to upstream |
ACME_DNS_ENABLED |
No | false |
Enable ACME DNS-01 support; adds a CoreDNS forward stanza for auth.DOMAIN (true/false) |
HOSTS_RELOAD |
No | 15s |
CoreDNS reload interval for /etc/coredns/hosts |
ZONE_RELOAD |
No | 30s |
CoreDNS reload interval for /etc/coredns/zone |
DOCKERGEN_WAIT |
No | 5s:30s |
docker-gen debounce interval (default 5 s–30 s) |
Note:
HOST_IPis typically the IP of the host running the container, but can also be a local IP of a router or other device.
Production Tips: On a docker infrastructure with few containers (stable),
DOCKERGEN=5s:30s+HOSTS_RELOAD=15sis recommended. On a very dynamic infrastructure with many containers (unstable),DOCKERGEN=1s:5s+HOSTS_RELOAD=5sis recommended.
DNS_AUTHORITATIVE |
Unknown subdomain under DOMAIN |
External query |
|---|---|---|
true (default) |
NXDOMAIN |
Forwarded to DNS_UPSTREAM |
false |
Forwarded to DNS_UPSTREAM |
Forwarded to DNS_UPSTREAM |
Use false for split-horizon DNS — the zone is served locally, but unknown subdomains fall through to your upstream resolver.
When paired with an internal CA (step-ca, Vault PKI, …) and Traefik, dns-resolver can serve the _acme-challenge TXT records required by the DNS-01 challenge, enabling fully automatic certificate issuance without exposing port 80.
sequenceDiagram
participant T as Traefik (lego)
participant CA as Internal CA
participant AD as acme-dns (sidecar)
participant CD as CoreDNS (dns-resolver)
T->>CA: POST /acme/order (request cert)
CA-->>T: DNS-01 challenge token
T->>AD: POST /update (write TXT record)
AD-->>T: 200 OK
CA->>CD: TXT _acme-challenge.svc.auth.DOMAIN?
CD->>AD: forward auth.DOMAIN → acme-dns:53
AD-->>CA: TXT <token>
CA-->>T: challenge passed → certificate
T->>AD: POST /update (clear TXT record)
- Traefik requests a certificate from the internal CA.
- The CA issues a DNS-01 challenge: create a TXT at
_acme-challenge.svc.auth.DOMAIN. - Traefik calls the acme-dns REST API (
POST /update) to write the TXT record. - The CA queries CoreDNS. Because
ACME_DNS_ENABLED=true, CoreDNS forwards allauth.DOMAINqueries to theacme-dnssidecar, which responds with the TXT record. - Challenge passes → certificate issued. Traefik cleans up the TXT record.
CoreDNS uses Docker's internal DNS to resolve the
acme-dnshostname — no fixed container IP required.
1. Enable in .env:
ACME_DNS_ENABLED=true2. Keep the acme-dns service in your compose (already included in the example below).
3. Configure Traefik (static config + environment variables):
# traefik.yml — static configuration
certificatesResolvers:
internal:
acme:
caServer: "https://ca.home.example.com/acme/acme/directory"
storage: "/data/acme.json"
dnsChallenge:
provider: acmedns
resolvers:
- "<HOST_IP>:53"# In the Traefik service environment block:
environment:
- ACMEDNS_STORAGE_PATH=/data/acmedns.json # credentials per domain (auto-created)
- ACMEDNS_API_BASE=http://acme-dns:80 # acme-dns REST API (internal)Traefik must share the
dns-netnetwork withacme-dnsto reach its REST API.
First-time registration — on first certificate request, the acmedns lego provider automatically calls POST /register on acme-dns and saves the per-domain credentials to ACMEDNS_STORAGE_PATH. Subsequent renewals are fully automatic.
# Register a domain and obtain credentials manually
curl -s -X POST http://<HOST_IP or service name>:80/register | jq .
# → { "username": "...", "password": "...", "fulldomain": "<uuid>.auth.home.example.com", ... }
# Verify the TXT query reaches acme-dns via CoreDNS
dig @<HOST_IP> _acme-challenge.test.auth.home.example.com TXT +shortRequires Docker Compose CLI v2.24+ for the inline
configs.contentinterpolation.
volumes:
acme_dns_data:
configs:
acme_dns_config:
content: |
[general]
listen = "0.0.0.0:53"
protocol = "both"
domain = "auth.${DOMAIN}"
nsname = "acme-dns.${DOMAIN}"
nsadmin = "hostmaster.${DOMAIN}"
debug = false
[database]
engine = "sqlite3"
connection = "/var/lib/acme-dns/acme-dns.db"
[api]
ip = "0.0.0.0"
port = "80"
tls = "none"
disable_registration = false
autoregister = false
use_header = false
[logconfig]
loglevel = "info"
logtype = "stdout"
logformat = "text"
networks:
dns-net:
name: dns-net
services:
dns:
image: ghcr.io/circle-rd/dns-resolver:latest
container_name: dns
restart: unless-stopped
ports:
- "${HOST_IP}:53:53/udp"
- "${HOST_IP}:53:53/tcp"
environment:
- DOMAIN=${DOMAIN}
- HOST_IP=${HOST_IP}
- DNS_UPSTREAM=${DNS_UPSTREAM:-1.1.1.1}
- DNS_AUTHORITATIVE=${DNS_AUTHORITATIVE:-true}
- ACME_DNS_ENABLED=${ACME_DNS_ENABLED:-false}
- HOSTS_RELOAD=${HOSTS_RELOAD:-15s}
- ZONE_RELOAD=${ZONE_RELOAD:-30s}
- DOCKERGEN_WAIT=${DOCKERGEN_WAIT:-5s:30s}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- dns-net
# Optional — remove if ACME DNS-01 is not needed.
acme-dns:
image: joohoi/acme-dns:latest
container_name: acme-dns
restart: unless-stopped
configs:
- source: acme_dns_config
target: /etc/acme-dns/config.cfg
volumes:
- acme_dns_data:/var/lib/acme-dns
networks:
- dns-netBinding port 53 to
${HOST_IP}instead of0.0.0.0avoids conflicts withsystemd-resolvedon Linux hosts.
Verify the wildcard zone resolves to HOST_IP:
dig @<HOST_IP> anything.home.example.com +short
# → <HOST_IP>Verify container name resolution:
# With a running container named "myapp":
dig @<HOST_IP> myapp +short
# → <container-IP>Verify external resolution still works:
dig @<HOST_IP> github.com +short
# → (public IP)Traefik integration — route a service:
# In the service container labels:
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`myapp.home.example.com`)"Point your client's DNS to HOST_IP, then open https://myapp.home.example.com — Traefik receives the request via the wildcard zone and routes it by the Host() header.
The published image supports the following platforms:
| Platform | Architecture |
|---|---|
linux/amd64 |
x86-64 servers, VMs |
linux/arm64 |
ARM64 (Apple M*, AWS Graviton, RPi 4/5) |
linux/arm/v7 |
ARMv7 (Raspberry Pi 2/3) |
Docker will automatically pull the correct variant for your host.
-
On startup,
entrypoint.shvalidatesDOMAINandHOST_IP, then generates:/etc/coredns/Corefile— CoreDNS configuration viaenvsubst/etc/coredns/zones/db.<DOMAIN>— authoritative SOA / NS / A / wildcard zone file/etc/coredns/hosts— empty seed file, populated at runtime
-
docker-gen starts in the background, watches
/var/run/docker.sock, and rewrites/etc/coredns/hostson every container start or stop (DOCKERGEN_WAITdebounce, default5s:30s). -
CoreDNS starts and serves:
*.DOMAIN→HOST_IP(wildcard zone, Traefik routes byHost()header)- Container names → container IPs (via hosts file, reloaded every
HOSTS_RELOAD, default15s) - All other queries → upstream resolver (default:
1.1.1.1)
-
Both processes run under the same entrypoint script. A shared
traponSIGTERM/SIGINTensures a clean shutdown of both processes when the container stops.
MIT — see LICENSE.