In addition to the standard format of expressing IP addresses with numbers and dots, symbolic names can also refer to a host. Symbolic names are preferred as they are easier to remember. A service like Domain Name System (DNS) converts domain names into IP addresses using a database that maintains the mapping between host names (or domain names) and host numbers (or IP addresses).

In the Linux environment, name resolution is described in the /etc/nsswitch.conf file, where the default entry for hosts includes files and DNS. It means that the system will check /etc/hosts file before requesting the DNS server for name resolution.

Docker provides a DNS system that can route service requests using their service names. However, a Fully Qualified Domain Name (FQDN) like nginx.service.com cannot be used within Docker. Instead, we'll need to use the container's name to connect between different services.

ℹ️
When using a service name to connect to other containers, the resolution service of Docker will translate the service name into the container's IP address. This is done under the hood, and no additional tools are required to perform it.

Let's get into it!

Pre-Requisites

To follow along with this blog, you'll need the following:

  • Linux System with Docker Pre-Installed
  • Docker-Compose

First, let's set up our DNS Server. We will use BIND 9 for the DNS Server, provided free by the Internet Consortium. It's available as a Docker image at DockerHub.

Setup Docker Network

Let's create a docker network.

docker network create --name lab-net --subnet 172.24.0.0/16

For our network, we're using a /16 subnet.

DNS Server Configuration

  1. First, setup a directory to store our BIND 9 configuration and create a new file called named.conf.options to start configuring the BIND 9 server.
mkdir -p /opt/bind9/configuration
vim /opt/bind9/configuration/named.conf.options

2. Copy the contents below into the file.

options {    
directory "/var/cache/bind";    
recursion yes;    
listen-on { any; };    
forwarders {            
8.8.8.8;            
8.8.4.4;    
};};

Contents of named.conf.options

This ensures that BIND listens on all interfaces for DNS requests, and any domain name requests that cannot be fulfilled by the BIND server (ones outside of the zone file) will be sent to the forwarders for resolution. For example, google.com, docker.com, etc.

3. Next, we'll define a Zone called lab-net.lan, which points to /etc/bind/zones/db. Zone File.

Let's create the file that will define our zone.

vim /opt/bind9/configuration/named.conf.local

Copy the contents below into the file.

zone "lab-net.lan" {    
type master;    
file "/etc/bind/zones/db.lab-net.lan";
};

Contents of named.conf.local

The Zone File called db.lab-net.lan contains the domain names that will be managed by our BIND DNS server. We will assign each domain name an IP address. Mention the container's IP address during container runtime so the domain names can point to the corresponding IP addresses.

We'll now add a few domain names to our zone file. Create the lab-net zone file.

vim /opt/bind9/configuration/db.lab-net.lan

4. Copy the contents below into the zone file.

$TTL    1d ; 
default expiration time (in seconds) of all RRs without their own 
TTL value@       IN      SOA     

ns1.lab-net.lan. root.lab-net.lan. 
(                  3      ; 
Serial  1d; 
Refresh 1h; 
Retry 1w; 
Expire 1h); 
Negative Cache TTL;

name servers - NS records     IN      NS      
ns1.lab-net.lan.; 

name servers - A records
ns1.lab-net.lan.          IN      A      172.24.0.2 
nginx.lab-net.lan.        IN      A      172.24.0.3 
httpd.lab-net.lan.        IN      A      172.24.0.4

Zone file contents

In the above example, one name server ns1.lab-net.lan, and two hosts nginx.lab-net.lan and httpd.lab-net.lan have been added.

✍️
The hosts in the zone file can be customized as per your needs. Say you have a Caddy web server and a MongoDB that need domain names specified to them. You can add their names to the zone file and the IP addresses you plan to assign them later.


Also Read: A Deep Dive Into Kubernetes Pods


Build the Docker Image

We'll use the official Docker Image of BIND 9 as a base image and install some additional dependencies into it. Finally, we'll copy our configuration files directly into the Docker image.

  1. Create the Dockerfile.
vim /opt/bind9/Dockerfile.bind9

2. Copy the contents below into the Dockerfile.

#Base Bind9 Image
FROM internetsystemsconsortium/bind9:9.18

#Install required tools and dependencies
RUN apt update && apt install -y \  
        bind9-doc \  
        dnsutils \  
        geoip-bin \  
        mariadb-server \  
        net-tools

#Copy configuration files
COPY configuration/named.conf.options /etc/bind/
COPY configuration/named.conf.local /etc/bind/
COPY configuration/db.lab-net.lan /etc/bind/zones/

# Expose Ports
EXPOSE 53/tcp
EXPOSE 53/udp
EXPOSE 953/tcp

# Start the Name Service
CMD ["/usr/sbin/named", "-g", "-c", "/etc/bind/named.conf", "-u", "bind"]

BIND 9 Image Dockerfile Content

3. Build and tag the BIND image using the command below.

docker build -t dns-master . -f Dockerfile.bind9

Run the Docker Container

Once our container has been successfully built, we will now run it in our lab-net network with an explicitly mentioned IP address.

docker run -d -p 53:53/tcp -p 53:53/udp -p 127.0.0.1:953:953/tcp --rm --name=dns-master --net=lab-net --ip=172.24.0.2 internetsystemsconsortium/bind9:9.18

Verifying server configuration:

docker exec -ti dns-master /bin/bash 
named-check
confnamed-checkzone lab-net.lan /etc/bind/zones/db.lab-net.lan
zone lab-net.lan/IN: loaded serial 3
OK

Checking DNS configuration of BIND 9 with named-checkzone


Also Read: 9 Essential Tips for Writing Engaging Tech Blogs


Demo Setup

We can now use the previously started DNS container as our DNS server. Any services we need domain mapping for can simply be rebuilt with the required domain names.

docker run -d --rm --name=nginx --net=lab-net --ip=172.24.0.3 --dns=172.24.0.2 nginx:latest
sudo docker run -d --rm --name=httpd --net=lab-net --ip=172.24.0.4 --dns=172.24.0.2 httpd:latest

As you can see, all containers now run on the same network.

docker network inspect lab-net

[{        
"Name": "lab-net",        
"IPAM": 
{            
"Config": 
[{                    
"Subnet": "172.24.0.0/16"                
}            
]},        
"Containers": 
{            "12hbd7e3b3a033fd643d36fff787c123adlkda485dc5f3d4468212568b8ff4498e776993": 
{                
"Name": "dns-master",                
"IPv4Address": "172.24.0.2/16"            
},            "14deb32e260d15ff8543571f2c5fd1d99eeb9ba97042a97c34d9b933525ca8aa": 
{                
"Name": "service2",                
"IPv4Address": "172.24.0.4/16"            },            "cb6840cfd76d360dfe4cefc96486a11cd4b73f405d114c2830fd792c4883dd8b": 
{                
"Name": "service1",                
"IPv4Address": "172.24.0.3/16"            
}},    
}]

Output of Docker network inspect lab-net

We can test whether our domain name mapping functions properly by executing an nslookup command. Using the nslookup command, we will be able to determine the IP address of a domain name and the DNS server supplying the details to us.

docker exec -it nginx nslookup httpd.lab-net.lan 

Server:         127.0.0.11
Address:        127.0.0.11:53

Name:   httpd.lab-net.lan
Address: 172.24.0.4

Using nslookup to check IP Address of the httpd service

To check if the forwarder is working, we can perform nslookup on a domain like google.com, which exists outside of our internal DNS zone. If our request is successful, we should be able to see the IP address of Google's domain.

docker exec -it nginx nslookup google.com                             Server:         127.0.0.11
Address:        127.0.0.11:53

Non-authoritative answer:
Name:   google.com
Address: 142.250.182.110

Using nslookup to check IP Address of the nginx service

As you can see, the reply that came back has the string Non-authorative. This means the DNS server that supplied us with domain details google.com is not under our control. We got this outcome because we added forwarders in our BIND configuration. The domain name we requested detail for was not available in the local zone file, so its details could not be supplied by the BIND server.

Conclusion

This way, we can set up a local DNS server in Docker.

The benefits of setting up a local DNS server are endless. It helps a lot when you don't have access to domain names and want to test different technologies that rely somehow on a domain name.

Please comment below if you have any queries. I try to update my articles regularly to maintain legibility!