The introduction of user-defined networks signaled a big change in Docker networking. While the ability to provision custom networks was the big news, there were also major enhancements in name resolution. User-defined networks can benefit from what’s being named embedded DNS. The Docker engine itself now has the ability to provide name resolution to all of the containers. This is a marked improvement from the legacy solution where the only means for name resolution was external DNS or linking, which relied on the hosts
file. In this recipe, we’ll walk through how to use and configure embedded DNS.
In this recipe, we’ll be demonstrating the configuration on a single Docker host. It is assumed that this host has Docker installed and that Docker is in its default configuration. We’ll be altering name resolution settings on the host, so you’ll need root-level access.
As mentioned, the embedded DNS system only works on user-defined Docker networks. That being said, let’s provision a user-defined network and then start a simple container on it:
user@docker1:~$ docker network create -d bridge mybridge1 0d75f46594eb2df57304cf3a2b55890fbf4b47058c8e43a0a99f64e4ede98f5f user@docker1:~$ docker run -d -P --name=web1 --net=mybridge1 jonlangemak/web_server_1 3a65d84a16331a5a84dbed4ec29d9b6042dde5649c37bc160bfe0b5662ad7d65 user@docker1:~$
As we saw in an earlier recipe, by default, Docker pulls the name resolution configuration from the Docker host and provides it to the container. This behavior can be changed by providing different DNS servers or search domains either at the service level or at container runtime. In the case of containers connected to a user-defined network, the DNS settings provided to the container are slightly different. For instance, let’s look at the resolv.conf
file for the container we just connected to the user-defined bridge mybridge1
:
user@docker1:~$ docker exec -t web1 more /etc/resolv.conf
search lab.lab
nameserver 127.0.0.11
options ndots:0
user@docker1:~$
Notice how the name server for this container is now 127.0.0.11
. This IP address represents Docker’s embedded DNS server and will be used for any container, which is connected to a user-defined network. It is a requirement that any container connected to a user-defined network should use the embedded DNS server.
Containers not initially started on a user-defined network will get updated the moment they connect to a user-defined network. For instance, let’s start another container named web2
but have it use the default docker0
bridge:
user@docker1:~$ docker run -dP --name=web2 jonlangemak/web_server_2 d0c414477881f03efac26392ffbdfb6f32914597a0a7ba578474606d5825df3f user@docker1:~$ docker exec -t web2 more /etc/resolv.conf :::::::::::::: /etc/resolv.conf :::::::::::::: # Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8) # DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN nameserver 10.20.30.13 search lab.lab user@docker1:~$
If we now connect the web2
container to our user-defined network, Docker will update the name server to reflect the embedded DNS server:
user@docker1:~$ docker network connect mybridge1 web2
user@docker1:~$ docker exec -t web2 more /etc/resolv.conf
search lab.lab
nameserver 127.0.0.11
options ndots:0
user@docker1:~$
Since both our containers are now connected to the same user-defined network, they can now reach each other by name:
user@docker1:~$ docker exec -t web1 ping web2 -c 2 PING web2 (172.18.0.3): 48 data bytes 56 bytes from 172.18.0.3: icmp_seq=0 ttl=64 time=0.107 ms 56 bytes from 172.18.0.3: icmp_seq=1 ttl=64 time=0.087 ms --- web2 ping statistics --- 2 packets transmitted, 2 packets received, 0% packet loss round-trip min/avg/max/stddev = 0.087/0.097/0.107/0.000 ms user@docker1:~$ docker exec -t web2 ping web1 -c 2 PING web1 (172.18.0.2): 48 data bytes 56 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.060 ms 56 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.119 ms --- web1 ping statistics --- 2 packets transmitted, 2 packets received, 0% packet loss round-trip min/avg/max/stddev = 0.060/0.089/0.119/0.030 ms user@docker1:~$
You’ll note that the name resolution is bidirectional, and it works inherently without the use of any links. That being said, with user-defined networks, we can still define links for the purpose of creating local aliases. For instance, let’s stop and remove both containers web1
and web2
and reprovision them as follows:
user@docker1:~$ docker run -d -P --name=web1 --net=mybridge1 --link=web2:thesecondserver jonlangemak/web_server_1 fd21c53def0c2255fc20991fef25766db9e072c2bd503c7adf21a1bd9e0c8a0a user@docker1:~$ docker run -d -P --name=web2 --net=mybridge1 --link=web1:thefirstserver jonlangemak/web_server_2 6e8f6ab4dec7110774029abbd69df40c84f67bcb6a38a633e0a9faffb5bf625e user@docker1:~$
The first interesting item to point out is that Docker lets us link to a container that did not yet exist. When we ran the container web1
, we asked Docker to link it to the container web2
. At that point, web2
didn’t exist. This is a notable difference in how links work with the embedded DNS server. In legacy linking, Docker needed to know the target container information prior to making the link. This was because it had to manually update the source container's host file and environmental variables. The second interesting item is that aliases are no longer listed in the container's hosts
file. If we look at the hosts
file on each container, we’ll see that the linking no longer generates entries:
user@docker1:~$ docker exec -t web1 more /etc/resolv.conf search lab.lab nameserver 127.0.0.11 options ndots:0 user@docker1:~$ docker exec -t web1 more /etc/hosts …<Additional output removed for brevity>… 172.18.0.2 9cee9ce88cc3 user@docker1:~$ user@docker1:~$ docker exec -t web2 more /etc/hosts …<Additional output removed for brevity>… 172.18.0.3 2d4b63452c8a user@docker1:~$
All of the resolution is now occurring in the embedded DNS server. This includes keeping track of defined aliases and their scope. So even without host records, each container is able to resolve the other containers alias through the embedded DNS server:
user@docker1:~$ docker exec -t web1 ping thesecondserver -c2 PING thesecondserver (172.18.0.3): 48 data bytes 56 bytes from 172.18.0.3: icmp_seq=0 ttl=64 time=0.067 ms 56 bytes from 172.18.0.3: icmp_seq=1 ttl=64 time=0.067 ms --- thesecondserver ping statistics --- 2 packets transmitted, 2 packets received, 0% packet loss round-trip min/avg/max/stddev = 0.067/0.067/0.067/0.000 ms user@docker1:~$ docker exec -t web2 ping thefirstserver -c 2 PING thefirstserver (172.18.0.2): 48 data bytes 56 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.062 ms 56 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.042 ms --- thefirstserver ping statistics --- 2 packets transmitted, 2 packets received, 0% packet loss round-trip min/avg/max/stddev = 0.042/0.052/0.062/0.000 ms user@docker1:~$
The aliases created have a scope that is local to the container itself. For instance, a third container on the same user-defined network is not able to resolve the aliases created as part of the links:
user@docker1:~$ docker run -d -P --name=web3 --net=mybridge1 jonlangemak/web_server_1 d039722a155b5d0a702818ce4292270f30061b928e05740d80bb0c9cb50dd64f user@docker1:~$ docker exec -it web3 ping thefirstserver -c 2 ping: unknown host user@docker1:~$ docker exec -it web3 ping thesecondserver -c 2 ping: unknown host user@docker1:~$
You’ll recall that legacy linking also automatically created a set of environmental variables on the source container. These environmental variables referenced the target container and any ports it might be exposing. Linking in user-defined networks does not create these environmental variables:
user@docker1:~$ docker exec web1 printenv PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=4eba77b66d60 APACHE_RUN_USER=www-data APACHE_RUN_GROUP=www-data APACHE_LOG_DIR=/var/log/apache2 HOME=/root user@docker1:~$
As we saw in the previous recipe, keeping these variables up to date wasn’t achievable even with legacy links. That being said, it’s not a total surprise that the functionality doesn’t exist when dealing with user-defined networks.
In addition to providing local container resolution, the embedded DNS server also handles any external requests. As we saw in the preceding example, the search domain from the Docker host (lab.lab
in my case) was still being passed down to the containers and configured in their resolv.conf
file. The name server learned from the host becomes a forwarder for the embedded DNS server. This allows the embedded DNS server to process any container name resolution requests and hand off external requests to the name server used by the Docker host. This behavior can be overridden either at the service level or by passing the --dns
or --dns-search
flag to a container at runtime. For instance, we can start two more instances of the web1
container and specify a specific DNS server in either case:
user@docker1:~$ docker run -dP --net=mybridge1 --name=web4 --dns=10.20.30.13 jonlangemak/web_server_1 19e157b46373d24ca5bbd3684107a41f22dea53c91e91e2b0d8404e4f2ccfd68 user@docker1:~$ docker run -dP --net=mybridge1 --name=web5 --dns=8.8.8.8 jonlangemak/web_server_1 700f8ac4e7a20204100c8f0f48710e0aab8ac0f05b86f057b04b1bbfe8141c26 user@docker1:~$
Now if we try to resolve a local DNS record on either container, we can see that in the case of web1
it works since it has the local DNS server defined, whereas the lookup on web2
fails because 8.8.8.8
doesn’t know about the lab.lab
domain:
user@docker1:~$ docker exec -it web4 ping docker1.lab.lab -c 2 PING docker1.lab.lab (10.10.10.101): 48 data bytes 56 bytes from 10.10.10.101: icmp_seq=0 ttl=64 time=0.080 ms 56 bytes from 10.10.10.101: icmp_seq=1 ttl=64 time=0.078 ms --- docker1.lab.lab ping statistics --- 2 packets transmitted, 2 packets received, 0% packet loss round-trip min/avg/max/stddev = 0.078/0.079/0.080/0.000 ms user@docker1:~$ docker exec -it web5 ping docker1.lab.lab -c 2 ping: unknown host user@docker1:~$