In my opinion, Technitium's DNS is the best DNS server for caching, local domains, and public domains. It also meets all the needs of a service provider.
mkdir -p ~/docker/technitium
cd ~/docker/technitium
cat << '_EOF_' > docker-compose.yml
services:
dns-server:
container_name: dns-server
hostname: dns-server
image: technitium/dns-server:latest
ports:
- "5380:5380/tcp" # DNS web console (HTTP)
- "53443:53443/tcp" # DNS web console (HTTPS)
- "53:53/udp" # DNS service
- "53:53/tcp" # DNS service
environment:
- DNS_SERVER_DOMAIN=dns-server # The primary domain name used by this DNS Server to identify itself.
- TZ=America/Campo_Grande # Set your desired timezone here
- DNS_SERVER_LOG_USING_LOCAL_TIME=true # Enable this option to use local time instead of UTC for logging.
volumes:
- config:/etc/dns
restart: unless-stopped
sysctls:
- net.ipv4.ip_local_port_range=1024 65535
volumes:
config:
_EOF_
docker compose up -d
[+] up 14/14
✔ Image technitium/dns-server:latest Pulled 6.2s
✔ Network technitium_default Created 0.2s
✔ Volume technitium_config Created 0.0s
✔ Container dns-server Created 4.6s
docker compose logs -f
dns-server | Technitium DNS Server was started successfully.
dns-server | Using config folder: /etc/dns
dns-server |
dns-server | Note: Open http://dns-server:5380/ in web browser to access web console.
dns-server |
dns-server | Press [CTRL + C] to stop...
^C
I installed the DNS server Technitium (https://technitium.com/dns/) on my OCI Servers:
1. ns1.tiozaodolinux.com (164.152.199.42) running on port 53 (TCP/UDP) with web console https://web-ns1.tiozaodolinux.com/
2. ns2.tiozaodolinux.com (146.235.37.231) running on port 53 (TCP/UDP) with web console https://web-ns2.tiozaodolinux.com/
The cluster was created in accordance with official documentation - https://blog.technitium.com/2025/11/understanding-clustering-and-how-to.html
It's a fairly simple procedure. It only took a few minutes and the cluster was up and running.
I configured a secondary DNS Server for redundancy. If the primary server fails, the secondary will continue serving DNS requests.
The most time-consuming part is creating and adding entries for a new zone. Since I already had a file containing this information in a format used by BIND9, I simply imported that zone and didn't need to create each entry manually.

This script was created out of the need to validate whether all entries registered on the new DNS server Technitium have the same values as the entries on the old DNS server (bind)
cat << '_EOF_' > validate-dns.sh
#!/bin/bash
#
# Validate DNS entries before migrating to another DNS server.
#
# Jarbas, 13/Fev/2026
# License: MIT
#
set -Eeuo pipefail
trap 'echo "Erro em ${BASH_SOURCE[0]}:${LINENO}: $BASH_COMMAND"' ERR
if [ $# -lt 3 ]; then
cat << EOF
Usage:
======
$0 file-with-zone old-dns-server:port new-dns-server:port
Hint: old-dns-server:port and new-dns-server:port are in format: IP:PORT or just IP for default port 53
Examples:
=========
$0 ms.sebrae.com.br.zone 8.8.8.8:53 200.53.30.182
$0 tiozaodolinux.zone 1.1.1.1 164.152.199.42:53
EOF
exit 1
fi
ZONE_FILE=$1 # Your zone file
OLD_SERVER_PORT=$2 # Your old DNS server and port
NEW_SERVER_PORT=$3 # Your new DNS server and port
NEW_DNS_SERVER=${NEW_SERVER_PORT%%:*} # Your new DNS IP
if [[ "$NEW_SERVER_PORT" != *:* ]]; then
NEW_DNS_PORT=53
else
NEW_DNS_PORT=${NEW_SERVER_PORT##*:}
fi
OLD_DNS_SERVER=${OLD_SERVER_PORT%%:*} # Your old DNS IP
if [[ "$OLD_SERVER_PORT" != *:* ]]; then
OLD_DNS_PORT=53
else
OLD_DNS_PORT=${OLD_SERVER_PORT##*:}
fi
TMP_FILE=/tmp/dns-records.txt
DOMAIN=$(awk '/^\$ORIGIN/ { print $2 }' "$ZONE_FILE")
# Filter only some types entries
awk '$4 ~ /^(A|CNAME|MX|TXT)$/ { print $1" "$4" "$5 }' "$ZONE_FILE" > "$TMP_FILE"
# It indicates how many records will be processed
TOTAL_RECORDS=$(wc -l < "$TMP_FILE")
cat << EOF
Your domain...: $DOMAIN
Old DNS server: $OLD_DNS_SERVER:$OLD_DNS_PORT
New DNS server: $NEW_DNS_SERVER:$NEW_DNS_PORT
Records active: $TOTAL_RECORDS
EOF
# First record
RECORD=1
# Number of errors
ERRORS=0
echo "Testing... ($TOTAL_RECORDS records)";
while read -r subdom type value;
do
if [ "$subdom" != "@" ]; then
fqdn=$subdom.$DOMAIN
else
fqdn=$DOMAIN
fi
RESP_OLD_DNS=$(host -p "$OLD_DNS_PORT" -t "$type" "$fqdn" "$OLD_DNS_SERVER" | awk '/^'"$fqdn"'/ { $1=""; print $0 }' | sort -n | tr -d '\r\n')
RESP_NEW_DNS=$(host -p "$NEW_DNS_PORT" -t "$type" "$fqdn" "$NEW_DNS_SERVER" | awk '/^'"$fqdn"'/ { $1=""; print $0 }' | sort -n | tr -d '\r\n')
if [ "$RESP_OLD_DNS" == "$RESP_NEW_DNS" ]; then # || [[ "$RESP_OLD_DNS" =~ "$RESP_NEW_DNS" ]] ; then
echo -e "$RECORD)\tOK\t: $fqdn $type $value";
else
echo -e "$RECORD)\tNOT OK\t: $fqdn $type $value";
echo -e "\t\t## OLD_DNS = $RESP_OLD_DNS";
echo -e "\t\t## NEW_DNS = $RESP_NEW_DNS";
((ERRORS+=1))
fi
((RECORD+=1))
done < $TMP_FILE
cat << EOF
Summary:
========
Your domain...: $DOMAIN
Old DNS server: $OLD_DNS_SERVER:$OLD_DNS_PORT
New DNS server: $NEW_DNS_SERVER:$NEW_DNS_PORT
Total Records.: $TOTAL_RECORDS
Errors Records: $ERRORS
EOF
_EOF_
chmod +x validate-dns.sh
./validate-dns.sh ~/Downloads/dns/ms.sebrae.com.br.zone 164.152.199.42 146.235.37.231
Your domain...: ms.sebrae.com.br.
Old DNS server: 164.152.199.42:53
New DNS server: 146.235.37.231:53
Records active: 149
Testing... (149 records)
1) OK : @.ms.sebrae.com.br. A 34.160.112.175
2) OK : 360.ms.sebrae.com.br. CNAME entrada-01
...
149) OK : zabbix.ms.sebrae.com.br. CNAME entrada-01
Summary:
========
Your domain...: ms.sebrae.com.br.
Old DNS server: 164.152.199.42:53
New DNS server: 146.235.37.231:53
Total Records.: 149
Errors Records: 0
grep -v -E "(RRSIG|NSEG)" ~/Downloads/dns/ms.sebrae.com.br.zone > ~/Downloads/dns/ms.sebrae.com.br.txt
i=0; while true; do ((i++)); echo; echo "$i) `date`"; host -t CNAME servicos.ms.sebrae.com.br 146.235.37.231; sleep 0.001; done
