How to Deploy a Dynamic DNS Server with Docker on Debian 10

Dynamic DNS is a network service for mapping domain names to dynamic (temporary, frequently changing) IP addresses. It is used to access computers that do not have a static IP address, such as those in SOHO (Small Office/Home Office) networks, and is often used in combination with port forwarding to access systems that are behind NAT firewalls. This article will guide you through the complete setup of a Dynamic DNS server in a Docker container on a Debian 10 system, including setting up the required DNS records, placing the management API behind an Nginx HTTPS reverse proxy, and automating the client-side DNS record updates.

Requirements

  • A single Debian 10 server, optionally with IPv6 connectivity. (192.0.2.2 and 2001:0db8::0db9 will be used as placeholders for the server's IPv4 and IPv6 respectively.)
  • Access to the root user, or a user with sudo privileges.
  • The ports tcp/53 and udp/53 must be available on the host.
  • A registered domain name and access to its nameservers/zonefile. Create DNS records for this domain as shown in the next section.
  • The $EDITOR environment variable must be set.
  • Optionally, any Linux/Unix client system to setup automatic DNS record updates.

Creating DNS records.

You will need to create at least 2 DNS records for your dynamic DNS server to work. First, choose a subdomain such as ns1.your_domain which will point to your server's IPv4 address. Secondly, choose a subdomain such as ddns.your_domain which will be delegated to ns1.your_domain.

Your dynamic DNS server will handle all records under ddns.your_domain. The third record, of type AAAA, is optional. The corresponding records look like the following:

ns1.your_domain A 192.0.2.2
ddns.your_domain NS ns1.your_domain
ns1.your_domain AAAA 2001:0db8::0db9 (optional)

DNS records example

You should create these records in your domain registrar's control panel. Please note that it may take up to 24 hours for these records to propagate well, but it usually takes minutes.

Installation

If you are not using the root user, we recommend you start a temporary root shell, since most commands shown in this guide required elevated privileges. To launch a root shell, use one of the following commands:

sudo su - root
sudo -s

Step 1: Updating and installing dependencies.

It is always good practice to update your system first:

apt update
apt upgrade -y
reboot

After the reboot, install the software packages required for this setup:

  • certbot will be used to obtain SSL/TLS certificates.
  • make is required to build the docker image in which the DDNS server will run.
  • apt-transport-https, ca-certificates, curl, gnupg2 and software-properties-common are needed to install the Docker repository and its corresponding GPG key.
  • dnsutils provides dig, which will be used for testing.
apt install -y certbot make apt-transport-https curl ca-certificates software-properties-common gnupg2 dnsutils

Step 2: Install Docker CE.

Add Docker's GPG key:

curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -

Install the docker repository:

add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian buster stable"

Update Debian's repository cache then install docker and its dependencies:

apt update
apt install -y docker-ce docker-ce-cli containerd.io

Once the installation is complete, make sure the docker service is enabled and running as follows:

systemctl enable --now docker.service

Step 3: Download and build docker-ddns

Our dynamic DNS server will be powered by a docker container that uses Bind as the DNS server and a management API written in Go. First, clone the Github repository and build the container image with the following commands:

git clone https://github.com/dprandzioch/docker-ddns.git
cd docker-ddns
make image

Wait for the process to finish, which may take a while, then open the file envfile with a text editor:

$EDITOR envfile

And input the following:

SHARED_SECRET=your_secret 
ZONE=ddns.your_domain
RECORD_TTL=60

The shared secret is a password that will be used to authenticate with the management API. ZONE indicates which DNS zone your server will be responsible for, and the record TTL specifies how long DNS records can be cached for. A TTL of 60 seconds is recommended for frequently changing dynamic IPs.

If needed, you can generate a random 40-character string for the secret using the following command:

cat /dev/urandom | tr -dc "a-zA-Z0-9" | fold -w 40 | head -1

We can now create the container:

docker create -it -p 127.0.0.1:8080:8080 -p 53:53 -p 53:53/udp --env-file envfile -v /mnt/ddns-data:/var/cache/bind --name ddns-server davd/docker-ddns

This command will create a container named ddns-server from the image we built earlier, and will map the ports 8080/tcp, 53/tcp and 53/udp from the host to the container. It will also mount the directory /mnt/ddns-data from the host, on /var/cache/bind in the container's filesystem. This is used for persisting DNS data across container recreation.

Verify that the container was created with the command:

docker container ls -a

A single entry should be outputted with the name ddns-server.

Step 4: Systemd service (optional)

This step is for simpler management but is not strictly required. If you choose not to use a systemd service, you will have to manage the container manually or use another management solution. Please note that for larger, more complex container deployments, an orchestration solution such as Kubernetes or Docker Swarm is recommended. In this case, a systemd service is perfectly fitting, as we are only running a single container.

To be able to manage this container as a system service, we'll wrap it in a systemd unit. Create the file /etc/systemd/system/ddns-server-ct.service with your text editor:

$EDITOR /etc/systemd/system/ddns-server-ct.service

And add the following :

[Unit]
Description=DDNS Server Docker Container
After=docker.service
Requires=docker.service
Requires=network.target
[Service]
Type=oneshot
TimeoutStartSec=240
Restart=no
RemainAfterExit=yes
ExecStart=/usr/bin/docker start ddns-server
ExecStop=/usr/bin/docker stop ddns-server
[Install]
WantedBy=multi-user.target

Save and exit, then set the correct permissions on this unit file:

chmod 664 /etc/systemd/system/ddns-server-ct.service

Load the new service file with the following command:

systemctl daemon-reload

You should now be able to start and stop this container using systemctl as any other system service.

If you want the DDNS server to start automatically at system boot, execute the following:

systemctl enable ddns-server-ct.service

Step 5: Testing your server

Before proceeding with the setup, we'll test the management API locally. Start the container:

systemctl start ddns-server-ct.service

Send a GET request to the API to create a new record:

NOTE: The API is currently only accessible locally (i.e. from localhost).

curl "http://127.0.0.1:8080/update?secret=your_secret&domain=test1&addr=1.1.1.1"

Curl should return the following response:

{"Success":true,"Message":"Updated A record for test1 to IP address 1.1.1.1","Domain":"test1","Domains":["test1"],"Address":"1.1.1.1","AddrType":"A"}

NOTE: The domain test1 refers to test1.ddns.your_domain. since the server is handling the ddns.your_domain. zone.

Perform a DNS lookup to verify that the record was indeed created and to test DNS resolution:

dig +short -t A test1.ddns.your_domain @127.0.0.1

The output should be 1.1.1.1.

Step 6: Reverse proxy

Since the API works over HTTP, your authentication secret can potentially be sniffed whenever you send a request over the network. An attacker could then manipulate your DNS records using your secret. We'll setup a reverse proxy using Nginx and secure it using HTTPS. First, obtain an SSL certificate from Let's Encrypt using certbot:

certbot certonly --standalone --agree-tos -m [email protected] -d ns1.your_domain

The ownership of your domain will be verified and a certificate will be issued. Next, install Nginx and make sure it is enabled and running:

apt install -y nginx systemctl enable --now nginx.service

Then disable the default server block file, as it is not needed:

unlink /etc/nginx/sites-enabled/default

We will now create a new configuration file for the reverse proxy, for example:

$EDITOR /etc/nginx/sites-available/ddns-api-proxy.conf

And paste the following, making sure you replace the IP addresses and domain names with your own:

server {
listen 192.0.2.2:8080;
server_name ns1.your_domain;
ssl on;
ssl_certificate /etc/letsencrypt/live/ns1.your_domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ns1.your_domain/privkey.pem;

location /update {
proxy_pass http://127.0.0.1:8080;
}
location / {
return 404;
}
access_log /var/log/nginx/ddns-api-access.log;
error_log /var/log/nginx/ddns-api-error.log;
}

Optional: If you want the API to be accessible over IPv6, add the following line after the existing listen directive:

listen [2001:0db8::0db9]:8080;

Enable this configuration and apply the changes by reloading Nginx:

ln -s /etc/nginx/sites-available/ddns-api-proxy.conf /etc/nginx/sites-enabled/
systemctl reload nginx.service

The API should now be accessible over the internet, and will only accept HTTPS connections. To test it, issue the command:

curl "https://ns1.your_domain:8080/update?secret=your_secret&domain=test2&addr=1.1.1.2"

It should return the following:

{"Success":true,"Message":"Updated A record for test2 to IP address 1.1.1.2","Domain":"test2","Domains":["test2"],"Address":"1.1.1.2","AddrType":"A"}

Step 7: Client configuration

You can set up automatic record updates on any router that supports custom dynamic DNS providers, such as Pfsense. You can also set them up on most other devices in your office or home network. To update or create a record, a GET request should be sent to the following endpoint:

https://ns1.your_domain:8080/update?secret=your_secret&domain=your_subdomain&addr=your_ip_address

You can also update the records of multiple subdomains with a single request. For example, to create/update records for sub1.ddns.your_domain and sub2.ddns.your_domain with IP address 198.51.100.100, you would send a GET request to this URL:

https://ns1.your_domain:8080/update?secret=your_secret&domain=sub1,sub2&addr=198.51.100.100

The addr parameter can also hold an IPv6 address to create/update AAAA DNS records, for example:

https://ns1.your_domain:8080/update?secret=your_secret&domain=cheese&addr=2001:0db8:aaaa::

To automate these updates on a Linux client, save the following bash script as /opt/ddns-update.sh:

#!/bin/bash

while [ -z $CURRENTIP ] do
CURRENTIP=`dig -r +short myip.opendns.com @resolver1.opendns.com 2>/dev/null`
sleep 1
done
curl -s "https://ns1.your_domain:8080/update?secret=your_secret&domain=your_subdomain&addr=${CURRENTIP}"

This script employs a while-loop wrapped around a dig command that obtains the client's public IP address and stores it in a variable. The loop assures that the public IP is correctly fetched. Then, cURL is used to send an API request to update the DNS record with this newly-fetched IP. Make sure your replace the values for your_secret and your_subdomain.

Next, make this script executable:

chmod +x /opt/ddns-update.sh

Then launch the crontab editor:

crontab -e

Add the following line to the end of your crontab:

*/2 * * * * /opt/ddns-update.sh

Save and exit. The script will now run every two minutes, keeping your dynamic DNS record up to date with the client's latest public IP address.

Further Reading

Share this page:

1 Comment(s)