Technitium DNS Server is a full-featured DNS server with a friendly web console. Think of it as Pi-hole and a proper authoritative DNS server rolled into one: it blocks ads and trackers for your whole network, resolves names recursively or via encrypted upstreams (DNS-over-HTTPS, TLS or QUIC), hosts your own local zones such as nas.home.arpa, and can even run DHCP.
In this guide we will run it in Docker on a Linux host, bind it to the host's LAN address so it does not clash with systemd-resolved, keep the admin password out of the compose file, and point the network at it.
β Before you start
- A Linux host with Docker and Docker Compose v2, and a static IP address (DNS servers must not change address). In this guide it is
192.168.1.53. - Access to your router's DHCP settings so you can hand out the new DNS server.
- Ideally a second machine to run another instance, so the whole house does not lose the internet when you reboot one box. More on that at the end.
When DNS stops, everything stops. Keep your router's old DNS settings written down so you can switch back quickly while you are testing.
STEP 1: Check what is already using port 53
Ubuntu and many other distros run systemd-resolved, which listens on port 53 at 127.0.0.53. Let's see what is there:
sudo ss -lunp 'sport = :53'You will probably see 127.0.0.53%lo:53 (and maybe 127.0.0.54:53). That is fine. Rather than disabling systemd-resolved, we will bind Technitium only to the host's LAN IP and to 127.0.0.1, which do not overlap with it. If you see anything else on port 53 (an old Pi-hole or dnsmasq, say), stop that first.
STEP 2: Create the folders and password file
sudo mkdir -p /path/to/appdata/technitium/config
cd /path/to/appdata/technitiumTechnitium can read its first admin password from a file, which keeps it out of your compose file and out of docker inspect. Create it with a strong password of your own:
printf '%s' 'CHANGE_ME_STRONG_PASSWORD' | sudo tee admin-password > /dev/null
sudo chmod 600 admin-password/path/to/appdata/technitium/
docker-compose.yml
admin-password # only used on the very first start
config/ # zones, settings, block lists, logs and statsprintf command and most shells will not save it to your history.STEP 3: The compose file
Create docker-compose.yml. Replace 192.168.1.53 with your host's real LAN IP in both port lines.
services:
technitium:
image: technitium/dns-server:15.5.1
container_name: technitium
hostname: dns1
restart: unless-stopped
ports:
# DNS for your network, bound to the host's LAN IP only
- "192.168.1.53:53:53/udp"
- "192.168.1.53:53:53/tcp"
# DNS for the host itself
- "127.0.0.1:53:53/udp"
- "127.0.0.1:53:53/tcp"
# Web console
- "5380:5380/tcp" # HTTP
- "53443:53443/tcp" # HTTPS (once enabled in settings)
environment:
- TZ=Europe/London
- DNS_SERVER_DOMAIN=dns1.home.arpa
- DNS_SERVER_ADMIN_PASSWORD_FILE=/run/secrets/admin-password
- DNS_SERVER_RECURSION=AllowOnlyForPrivateNetworks
volumes:
- ./config:/etc/dns
- ./admin-password:/run/secrets/admin-password:ro
sysctls:
- net.ipv4.ip_local_port_range=1024 65000
healthcheck:
test: ["CMD-SHELL", "curl -fs http://127.0.0.1:5380/api/dnsClient/healthCheck | grep -q '\"status\":\"ok\"'"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15sSTEP 4: The settings explained
| Setting | What it does |
|---|---|
image: ...:15.5.1 |
Pins the version. DNS is not something you want changing under you overnight. |
hostname |
The name the server shows in the console. Handy when you run two of them. |
192.168.1.53:53:53 |
Publishes DNS only on the LAN address, which avoids the clash with systemd-resolved on 127.0.0.53. |
127.0.0.1:53:53 |
Lets the host itself (and anything on it) use Technitium at 127.0.0.1. |
5380 / 53443 |
The web console over HTTP and HTTPS. |
DNS_SERVER_DOMAIN |
The server's own name. Use something under home.arpa, which is reserved for home networks. |
DNS_SERVER_ADMIN_PASSWORD_FILE |
Sets the admin password on first start only. Changing the file later does nothing. |
DNS_SERVER_RECURSION |
AllowOnlyForPrivateNetworks answers lookups for private IP ranges only, so you never become an open resolver by accident. |
./config:/etc/dns |
All settings, zones, block lists, logs and stats. This is the folder to back up. |
sysctls |
Widens the range of source ports used for outgoing queries, which makes cache-poisoning attacks harder. |
STEP 5: Start it and test
docker compose up -d
docker compose logs -fOnce the logs show the DNS and web services have started, press Ctrl+C and test a lookup from another machine on your network:
On Linux or macOS
dig @192.168.1.53 example.com +shortOn Windows
nslookup example.com 192.168.1.53If you get an IP address back, the server is working.
STEP 6: First-run setup
Open http://192.168.1.53:5380 and log in as admin with the password from your admin-password file.
- Change the password and turn on 2FA. Use the user menu (top right) to change the password, and set up two-factor authentication from your profile while you are there (supported since v14). Afterwards, delete the password file with
sudo rm admin-passwordand remove its two lines from the compose file; it is not needed any more. - Choose your upstreams. Go to Settings β Proxy & Forwarders. Leave it empty to resolve recursively from the root servers (most private), or use the Quick Select list to pick something like Cloudflare (DNS-over-HTTPS) or Quad9 (DNS-over-HTTPS) so your ISP cannot see your lookups. Click Save Settings.
- Turn on ad blocking. Go to Settings β Blocking, make sure Enable Blocking is ticked, then use Quick Add to add a block list or two. A single well-maintained list is plenty to start with. Save, and the lists download in the background.
- Add local names. Go to Zones β Add Zone, create a Primary Zone called
home.arpa, then add A records such asnasβ192.168.1.10andjellyfinβ192.168.1.20. Nownas.home.arpaworks on every device. - Enable HTTPS for the console (optional). Under Settings β Web Service, tick Enable HTTPS and Use a self-signed certificate, then use
https://192.168.1.53:53443from now on. If you put the console behind a reverse proxy you can skip this. - Back up your settings. Settings β Backup Settings downloads a zip of everything. Do this now, and again before any big change.
STEP 7: Point your network at it
In your router's DHCP settings, set the DNS server handed out to clients to 192.168.1.53 (plus your second DNS server if you have one; see below). Devices pick up the change when they renew their lease, so reconnect a laptop to test straight away.
1.1.1.1 as the "secondary" DNS in DHCP. Devices use both at random, so some lookups will skip your ad blocking and your local names will fail intermittently. Use a second Technitium instance instead.After a few minutes the Dashboard will start filling up with queries and blocked domains.
STEP 8: Reverse proxy for the web console
The web console is an admin panel for your entire network. Keep it on the LAN, behind a VPN, or protected by SSO.
Option A: Traefik (file provider)
To reach it at https://dns.example.com on your LAN, add a router to your Traefik dynamic config. Adding an IP allow-list middleware for your LAN range is a good idea.
http:
middlewares:
lan-only:
ipAllowList:
sourceRange:
- 192.168.1.0/24
routers:
technitium:
rule: Host(`dns.example.com`)
entryPoints:
- websecure
tls:
certResolver: cloudflare
middlewares:
- lan-only
service: technitium
services:
technitium:
loadBalancer:
servers:
- url: http://192.168.1.53:5380Option B: Cloudflare Tunnel
I would not publish the console through a tunnel. If you really need remote access, add a Cloudflare Access policy in front of the hostname so only you can reach the login page, or use a VPN.
Only the web console goes through a reverse proxy. DNS itself (port 53) is served directly by the container.
STEP 9: Add a second DNS server (recommended)
A single DNS server is a single point of failure. Run the same compose file on a second machine (with its own IP, say 192.168.1.54, and hostname: dns2), then pick one way to keep them in step:
- Clustering (v14 and newer). Technitium's built-in clustering syncs settings, block lists, allowed and blocked domains and apps between nodes, and lets you manage them all from one console. Initialise the cluster from the Administration section on the first server, then join the second one to it. All nodes must run the same version.
- Secondary zones. On the first server, allow zone transfers for your
home.arpazone to the second server's IP. On the second, add a Secondary Zone forhome.arpapointing at the first. Block lists and settings then need setting up on both.
Hand out both IPs from DHCP and you can reboot either box without anyone noticing.
Updating
- Read the change log. Major versions sometimes include breaking changes, and cluster nodes must be upgraded together.
- Download a fresh backup from Settings β Backup Settings, or copy the
configfolder. - Check the version in the bottom corner of the console and run a quick
digtest.
Change the tag in image: (for example from 15.5.1 to the newer version), then:
docker compose pull
docker compose up -d
docker image prune -fIf you run two servers, update one, check it, then do the other. That way the network never goes dark.
Troubleshooting
address already in use on port 53. Something else has port 53 on that address. Check with sudo ss -lunp 'sport = :53'. If it is systemd-resolved on 0.0.0.0, you have probably mapped 53:53 without an IP; add the LAN IP in front as shown in the compose file.
Lookups from clients come back REFUSED. Recursion is only allowed for private ranges. If clients reach the server from a public IP (over a VPN, for example), switch to Settings β Recursion β Use specified network ACL and add their range.
The dashboard shows every query coming from one Docker IP. Depending on how Docker is set up (rootless Docker, or IPv6 clients reaching an IPv4-only container, for example), the container can end up seeing Docker's gateway address instead of the real client. The reliable fix is to run the container with network_mode: host and remove the ports: and sysctls: sections.
Ad blocking is not working on some devices. Phones and browsers with "Private DNS" or "Secure DNS" turned on bypass your server completely. Turn that off on the device, or use your own DNS-over-HTTPS endpoint.
I forgot the admin password. The password file only works on first start. Stop the container, back up config, and follow the password-reset steps in the official docs, which involve editing a file in the config folder.
You now own your network's DNS. Fewer adverts, faster lookups and proper local hostnames. Lovely. π‘οΈ