In this article we will setup CoreDNS and DNS Proxy using Podman and systemd to serve DNS request for our homelab server or workstation.

0. Set up Podman pod

Create quadlet file /etc/containers/systemd/ns.pod.

[Unit]
Description=DNS Pod

[Pod]
PodName=ns
DNS=127.0.0.1
IP=10.88.0.2
HostName=ns.saoirse.home.arpa

[Install]
WantedBy=default.target

1. Set up DNS Proxy

Pull latest DNS Proxy official image.

sudo podman pull docker.io/adguard/dnsproxy:latest

Create config for DNS Proxy.

# config.yaml
---
edns: true
listen-addrs:
  - "0.0.0.0"
listen-ports:
  - 54
max-go-routines: 0
ratelimit: 0
ratelimit-subnet-len-ipv4: 24
ratelimit-subnet-len-ipv6: 64
udp-buf-size: 0
upstream:
  - "https://1.1.1.1/dns-query"
  - "https://1.0.0.1/dns-query"
  - "https://8.8.8.8/dns-query"
  - "https://8.8.4.4/dns-query"
upstream-mode: 'load_balance'
timeout: '30s'

Save the file as config.yaml. We use port 54 because port 53 will be used by CoreDNS to serve DNS queries. DNS Proxy will act as DNS forwarder with the advantage of always connecting to DNS servers using HTTPS (DoH). We use both Cloudflare & Google public DNS servers with load balancing.

Then copy the config file to some directory, we will mount this directory to the container. I use /var/opt/containers/dnsproxy.

sudo mkdir -p /var/opt/containers/dnsproxy
sudo cp config.yaml /var/opt/containers/dnsproxy

Create quadlet file /etc/containers/systemd/dnsproxy.container.

[Unit]
Description=AdGuard DNS Proxy

[Service]
Restart=on-failure

[Container]
Pod=ns.pod
Image=docker.io/adguard/dnsproxy:latest
AutoUpdate=registry
CgroupsMode=no-conmon
Volume=/var/opt/containers/dnsproxy/config.yaml:/opt/dnsproxy/config.yaml:z
Memory=1g

2. Set up CoreDNS

Pull latest CoreDNS official image.

sudo podman pull docker.io/coredns/coredns:latest

Now we need to create configuration file for CoreDNS. Create Corefile.

. {
    forward . 0.0.0.0:54
    errors
}

This is the default config for our CoreDNS setup, ensuring all DNS queries will be forwarded to DNS Proxy. Since CoreDNS and DNS Proxy will run in the same pod, they will share the same host, hence 0.0.0.0. We forward the queries to port 54 since DNS Proxy serve DNS requests on port 54.

Now we need to create zone files and reverse zone files for services running in our homelab. Create db.saoirse.home.arpa.

$ORIGIN saoirse.home.arpa.
$TTL 3600
@                   IN  SOA     ns.saoirse.home.arpa. tamado.tamado.codes. (
                        1       ; serial
                        7200    ; refresh
                        3600    ; retry
                        1209600 ; expire
                        3600    ; nxdomain ttl
                    )
                    IN  NS      ns.saoirse.home.arpa.
                    IN  A       10.88.0.1

ns                  IN  A       10.88.0.2

We can add other DNS records, e.g.

kafka-1             IN  A       10.88.0.11
kafka-2             IN  A       10.88.0.12
kafka-3             IN  A       10.88.0.13
postgres            IN  A       10.88.0.100
haproxy             IN  A       10.88.0.101
mail                IN  A       10.88.0.141
nifi                IN  A       10.88.0.181

calendar            IN  CNAME   haproxy
roundcube           IN  CNAME   haproxy

_mail._tcp          IN  SRV     0 100 80 mail

_8443._https.nifi   IN  HTTPS   0 .

Create reverse zone file 88.10.in-addr.arpa.

$ORIGIN 88.10.in-addr.arpa.
$TTL 86400
@                   IN  SOA     ns.saoirse.home.arpa. tamado.tamado.codes. (
                        1       ; serial
                        7200    ; refresh
                        3600    ; retry
                        1209600 ; expire
                        3600    ; nxdomain ttl
                    )
                    IN  NS      ns.saoirse.home.arpa.

1.0                 IN  PTR     saoirse.home.arpa.
2.0                 IN  PTR     ns.saoirse.home.arpa.
11.0                IN  PTR     kafka-1.saoirse.home.arpa.
12.0                IN  PTR     kafka-2.saoirse.home.arpa.
13.0                IN  PTR     kafka-3.saoirse.home.arpa.
100.0               IN  PTR     postgres.saoirse.home.arpa.
101.0               IN  PTR     haproxy.saoirse.home.arpa.
141.0               IN  PTR     mail.saoirse.home.arpa.
181.0               IN  PTR     nifi.saoirse.home.arpa.

Now we need to make sure CoreDNS read these files for queries for these domains. Add these lines to the Corefile:

saoirse.home.arpa. {
    file /etc/coredns/db.saoirse.home.arpa
    log
    errors
}

88.10.in-addr.arpa. {
    file /etc/coredns/88.10.in-addr.arpa
    log
    errors
}

Now we need to copy the config files to a directory that will be mounted on the container. I will use /var/opt/containers/coredns. We will place Corefile on that directory and make a subdir for the zone files.

sudo mkdir -p /var/opt/containers/coredns/etc/coredns
sudo cp Corefile /var/opt/containers/coredns
sudo cp db.saoirse.home.arpa /var/opt/containers/coredns/etc/coredns
sudo cp 88.10.in-addr.arpa /var/opt/containers/coredns/etc/coredns

Then we create quadlet file /etc/containers/systemd/coredns.container.

[Unit]
Description=CoreDNS
After=dnsproxy.container
Requires=dnsproxy.container

[Service]
Restart=on-failure

[Container]
Pod=ns.pod
Image=docker.io/coredns/coredns:latest
AutoUpdate=registry
CgroupsMode=no-conmon
Volume=/var/opt/containers/coredns/Corefile:/Corefile:z
Volume=/var/opt/containers/coredns/etc/coredns:/etc/coredns:z
Memory=1g

3. Starting CoreDNS and DNS Proxy

Now we need to generate service files from the quadlet files. We simply need to reload systemd daemon.

sudo systemctl daemon-reload

Then we start the pod.

sudo systemctl start ns-pod

Observe the pod and the containers for any errors.

systemctl status ns-pod dnsproxy coredns

Test the DNS server.

dig @10.88.0.2 mail.saoirse.home.arpa A
dig @10.88.0.2 calendar.saoirse.home.arpa CNAME
dig @10.88.0.2 google.com A
dig @10.88.0.2 -x 10.88.0.100 PTR

If everything OK, and the addresses are resolved, we can use the pod IP (10.88.0.2) as our DNS server.

4. (Optional) Setting systemd-resolved to use CoreDNS as resolver

Create file /etc/systemd/resolved.conf.d/99-coredns.

[Resolve]
DNS=10.88.0.2
DNSOverTLS=no
Domains=~.
MulticastDNS=yes
Cache=yes
ReadEtcHosts=yes

We need to put Domains=~. to make sure it’s going to be the default resolver for all networks. Then we need to restart systemd-resolved

sudo systemctl restart systemd-resolved