docker and 127.0.0.1 in /etc/resolv.conf

I’ve been doing increasingly more stuff with docker these days, and I’ve bumped into an issue: all my systems use a local DNS resolver, one way or another: as I’m roaming and have some VPNs with split-horizon DNS, my laptop uses dnsmasq as configured by NetworkManager; as I want DNSSEC validation, my servers use a local unbound instance. (I’d prefer to use unbound everywhere, but I roam on some networks where the only DNS recursors that aren’t firewalled don’t support DNSSEC, and unbound is pretty bad at handling that unfortunately)

Docker handles 127.0.0.1 in /etc/resolv.conf the following way: it ignores the entry (upstream discussion). When there’s no DNS servers left, it will fall back to using 8.8.8.8 and 8.8.4.4.

This is all fine and dandy, if that’s the sort of thing you like (I don’t kinkshame), but if you don’t trust the owners of those DNS recursors, or if your network helpfully firewalls outbound DNS requests, you’ll likely want to use your own resolver instead.

The upstream docker FAQ just tells people to disable the local resolver and/or hardcode some DNS entries in the docker config. That’s not very helpful when you roam and you really want to use your local resolver… To do things properly, you’ll have to set two bits of configuration: tell docker to use the local resolver, and tell your local resolver to listen on the docker interface.

Docker DNS configuration

First, you need to configure the docker daemon to use the host as DNS server: In the /etc/docker/daemon.json file, set the dns key to [“172.17.0.1”] (or whatever IP address your docker host is set to use).

Local resolver: dnsmasq in NetworkManager

When NetworkManager is set up to use dnsmasq, it runs with a configuration that’s built dynamically, and updated when the network settings change (e.g. to switch upstream resolvers according to DHCP or VPN settings).

You can add drop-in configurations in the /etc/NetworkManager/dnsmasq.d directory. I have set the following configuration variables in a /etc/NetworkManager/dnsmasq.d/docker.conf file:

# Makes dnsmasq bind to all interfaces (but still only accept queries on localhost as per NetworkManager configuration)
bind-interfaces

# Makes dnsmasq accept queries on 172.17.0.0/16
listen-address=172.17.0.1

Restarting NetworkManager brings up a new instance of dnsmasq, which will let Docker do its thing.

Local resolver: unbound

The same sort of configuration is done with unbound. We tell unbound to listen to all interfaces and to only accept recursion from localhost and the docker subnet. In /etc/unbound/unbound.conf.d/docker.conf:

interface: 0.0.0.0
interface: ::
access-control: 127.0.0.0/8 allow
access-control: ::1/128 allow
access-control: 172.17.0.0/16 allow

Restart unbound (reload doesn’t reopen the sockets) and your docker container will have access to your local resolver.

Both these local resolver configurations should work even when the docker interface comes up after the resolver: we tell the resolver to listen to all interfaces, then only let it answer to clients on the relevant networks.

Result

Before:

$ docker run busybox nslookup pypi.python.org
Server: 8.8.8.8
Address 1: 8.8.8.8 google-public-dns-a.google.com

Name: pypi.python.org
Address 1: 2a04:4e42::223
Address 2: 2a04:4e42:200::223
Address 3: 2a04:4e42:400::223
Address 4: 2a04:4e42:600::223
Address 5: 151.101.0.223
Address 6: 151.101.64.223
Address 7: 151.101.128.223
Address 8: 151.101.192.223

After:

$ docker run busybox nslookup pypi.python.org
Server: 172.17.0.1
Address 1: 172.17.0.1

Name: pypi.python.org
Address 1: 2a04:4e42:1d::223
Address 2: 151.101.16.223