Why Default DNS Is a Problem
Every time your browser loads a page, your operating system sends a DNS query to translate the domain name (like google.com) into an IP address. By default, that query goes to your ISP's DNS servers.
Here's what that means in practice:
- Your ISP can see every domain you visit, even if the site itself uses HTTPS. The DNS query is sent in plaintext over UDP port 53.
- Many ISPs log these queries and sell aggregated data to advertising networks.
- Some ISPs inject their own responses — if you mistype a domain, instead of getting an NXDOMAIN error, you get redirected to a search/ad page they control.
- ISP DNS servers are often slow. They're shared across thousands of customers and rarely optimized.
- You have zero visibility into what's happening. No logs, no stats, no control.
Running your own DNS server fixes all of this. Your queries stay on your local network, you control the upstream resolvers, you get full query logging, and you can add features like ad-blocking and local domain resolution.
What We're Building
By the end of this guide, you'll have:
1. Technitium DNS Server running in a Docker container on your LAN
2. systemd-resolved disabled so it doesn't conflict with port 53
3. Cloudflare (1.1.1.1) and Google (8.8.8.8) configured as upstream forwarders over DNS-over-HTTPS (encrypted)
4. Your machine configured to use Technitium for all DNS resolution
5. A web dashboard at port 5380 where you can see every query, manage zones, and configure blocking
Technitium was chosen over Pi-hole or AdGuard Home because it's a full-featured authoritative + recursive DNS server, not just a forwarder with ad-blocking. You can host your own zones, set up conditional forwarding for local domains, and it handles DNSSEC natively. It's also actively maintained and the web UI is genuinely good.
Step 1: Check What's Using Port 53
Before we deploy anything, we need to know if port 53 is already in use. On most Ubuntu systems, systemd-resolved runs a local DNS stub listener on 127.0.0.53:53. If this is running, Docker won't be able to bind to port 53 and the container will fail to start.
Run this command to check:
sudo ss -tulnp | grep ':53 'Understanding the Output
If you see output like this:
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=XXX,fd=XX))
tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=XXX,fd=XX))
That's systemd-resolved. We need to disable it. If you see nothing, you're clear and can skip to Step 3.
If you see something OTHER than systemd-resolved (like dnsmasq, bind9, or another DNS server), you'll need to stop and disable that service first:
sudo systemctl stop <service-name>
sudo systemctl disable <service-name>
Step 2: Disable systemd-resolved
systemd-resolved is Ubuntu's built-in DNS management service. It binds to port 53, manages /etc/resolv.conf, and generally gets in the way when you want to run your own DNS. We're going to disable it and set a static resolv.conf.
First, stop and disable the service:
sudo systemctl stop systemd-resolved
sudo systemctl disable systemd-resolvedStep 2b: Fix /etc/resolv.conf
After disabling systemd-resolved, /etc/resolv.conf is probably a broken symlink pointing to a file that no longer exists. We need to replace it with a real file.
First, remove the old symlink and create a new resolv.conf. We'll temporarily point to a public DNS so you don't lose internet while setting up Technitium:
sudo rm /etc/resolv.conf
echo -e "nameserver 1.1.1.1\nnameserver 8.8.8.8" | sudo tee /etc/resolv.confStep 2c: Verify You Still Have DNS
Make sure you can still resolve domains after the change. If this fails, something went wrong with resolv.conf:
dig google.com +shortValidate: What You Should See
You should see one or more IP addresses returned, something like:
142.250.80.46
If you get "connection timed out; no servers could be reached" — check that /etc/resolv.conf exists and contains valid nameserver lines. Run: cat /etc/resolv.conf
Also verify port 53 is now free:
sudo ss -tulnp | grep ':53 'Expected Output
This should return NOTHING — an empty result. If port 53 is still occupied, something else grabbed it. Check what it is with the full ss output and stop that service before proceeding.
Step 3: Create the Docker Compose File
Create a directory to hold the Technitium config and compose file. We're using a named volume so the DNS data (zones, settings, logs) persists across container restarts and updates.
Create the project directory:
mkdir -p ~/docker/technitium && cd ~/docker/technitiumStep 3b: Write the Compose File
Create docker-compose.yml with the following content. This maps port 53 (DNS) and port 5380 (web dashboard) from the container to the host. The DNS_SERVER_DOMAIN is optional but recommended — it sets the server's identity in the dashboard.
Replace YOUR_SERVER_IP with the static IP of the machine you're installing this on (e.g., 192.168.1.12):
cat > docker-compose.yml << 'COMPOSE'
services:
technitium:
image: technitium/dns-server:latest
container_name: technitium
restart: unless-stopped
hostname: dns-server
ports:
- "53:53/udp"
- "53:53/tcp"
- "5380:5380/tcp"
environment:
- DNS_SERVER_DOMAIN=dns-server
- DNS_SERVER_ADMIN_PASSWORD=changeme
- DNS_SERVER_PREFER_IPV6=false
volumes:
- technitium-data:/etc/dns/config
networks:
- dns-net
networks:
dns-net:
driver: bridge
volumes:
technitium-data:
COMPOSEImportant: Change the Admin Password
The DNS_SERVER_ADMIN_PASSWORD environment variable sets the initial admin password for the web UI. The example above uses "changeme" — you MUST change this to a real password before deploying. This password is only used on first run; after that, you change it through the web UI.
If you forget to change it, you can update it later in the dashboard under Settings > Administration.
Step 4: Start Technitium
Bring up the container from the directory where you created the compose file:
cd ~/docker/technitium && docker compose up -dStep 4b: Verify the Container Is Running
Check that the container started successfully and isn't restarting in a loop:
docker ps --filter name=technitium --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'Validate: What You Should See
You should see something like:
NAMES STATUS PORTS
technitium Up 10 seconds 0.0.0.0:53->53/tcp, 0.0.0.0:53->53/udp, 0.0.0.0:5380->5380/tcp
If the STATUS column shows "Restarting" or the container isn't listed at all, check the logs:
docker logs technitium
The most common failure is port 53 still being in use. Go back to Step 1 and make sure nothing else is listening on port 53.
Another common issue: if you see "permission denied" errors in the logs, make sure you ran docker compose with sudo, or that your user is in the docker group (sudo usermod -aG docker $USER — then log out and back in).
Step 5: Test DNS Resolution Through Technitium
Now let's verify that Technitium is actually resolving DNS queries. Replace YOUR_SERVER_IP with the IP of the machine running the container:
dig @127.0.0.1 google.com +shortValidate: What You Should See
You should get back one or more IP addresses:
142.250.80.46
If you get "connection refused" — the container isn't running or port 53 isn't mapped correctly. Run: docker ps
If you get "connection timed out" — a firewall may be blocking UDP port 53. Check: sudo ufw status
Also test from another machine on your LAN (replace with your server's actual IP):
dig @192.168.1.12 google.com +short
If this fails but localhost works, it's a firewall issue. Allow port 53:
sudo ufw allow 53/tcp
sudo ufw allow 53/udp
Step 6: Access the Web Dashboard
Open a browser and navigate to:
http://YOUR_SERVER_IP:5380
Log in with:
Username: admin
Password: (whatever you set in DNS_SERVER_ADMIN_PASSWORD)
If the page doesn't load:
- Check the container is running: docker ps
- Check port 5380 is mapped: docker port technitium
- Check your firewall: sudo ufw allow 5380/tcp
- Try from the server itself first: curl -I http://127.0.0.1:5380
Step 7: Configure Upstream Forwarders
By default, Technitium uses root hints to resolve queries recursively — it talks directly to the root DNS servers and walks the chain. This works, but it's slower for the first query to each domain and sends your queries in plaintext to multiple servers along the chain.
We're going to configure DNS-over-HTTPS (DoH) forwarders to Cloudflare and Google. This means all upstream queries from your server are encrypted.
In the Technitium web dashboard:
1. Go to Settings (gear icon) > Proxy & Forwarders
2. Under "Forwarders", click "Quick Select" and choose Cloudflare (DNS-over-HTTPS)
3. Add a second forwarder: https://dns.google/dns-query (protocol: DNS-over-HTTPS)
4. Check "Enable Forwarders" and set the forwarder protocol to DNS-over-HTTPS
5. Click Save Settings
After saving, all outbound DNS queries from your server will be encrypted. Your ISP can no longer see which domains you're resolving.
Step 8: Point Your Machine at Technitium Permanently
Now update /etc/resolv.conf to use your Technitium server as the primary DNS. We'll keep a public fallback in case Technitium goes down:
IMPORTANT: If you're running Technitium on this same machine, use 127.0.0.1. If it's on a different machine, use that machine's LAN IP.
echo -e "nameserver 127.0.0.1\nnameserver 1.1.1.1" | sudo tee /etc/resolv.confStep 8b: Prevent resolv.conf From Being Overwritten
Some systems (especially those using DHCP) will overwrite /etc/resolv.conf on network changes or reboots. Lock it down with the immutable flag:
WARNING: This means nothing can modify this file until you remove the flag. If you need to change DNS settings later, you'll need to: sudo chattr -i /etc/resolv.conf
sudo chattr +i /etc/resolv.confStep 9: Final Validation
Run through these checks to confirm everything is working end-to-end:
echo '=== Check 1: resolv.conf ===' && cat /etc/resolv.conf && echo '' && echo '=== Check 2: DNS resolution ===' && dig google.com +short && echo '' && echo '=== Check 3: Technitium is answering ===' && dig @127.0.0.1 google.com +short && echo '' && echo '=== Check 4: Container is running ===' && docker ps --filter name=technitium --format '{{.Names}} {{.Status}}' && echo '' && echo '=== Check 5: Port 53 is bound ===' && sudo ss -tulnp | grep ':53 'Expected Final State
After all checks pass, you should see:
1. /etc/resolv.conf showing nameserver 127.0.0.1 (or your server's IP)
2. dig returning IP addresses for google.com
3. dig @127.0.0.1 returning the same results
4. technitium container showing "Up" status
5. Port 53 bound to the Docker proxy process
If any check fails, go back to the relevant step. The most common issues are:
- Port 53 conflict (systemd-resolved wasn't fully disabled)
- Firewall blocking port 53 (check ufw status)
- Container not running (check docker logs technitium)
- resolv.conf reverted (check if chattr +i was applied)
Optional: Set Up Ad Blocking
Technitium supports DNS-based ad blocking through block lists — similar to Pi-hole but built into the DNS server.
In the dashboard:
1. Go to Settings > Blocking
2. Under "Block List URLs", add these well-maintained lists:
- https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
- https://adaway.org/hosts.txt
3. Set the blocking response type to "Custom Address" and point it to 0.0.0.0
4. Click Save and then click "Update Now" to download the lists
This blocks ads and known malware domains at the DNS level for every device on your network that uses this DNS server.
Optional: Point Other Machines and Your Router
To get the benefit across your whole network, you have two options:
Option A: Configure your router's DHCP to hand out your Technitium server's IP as the DNS server. This way every device on your network automatically uses it. The setting is usually under DHCP > DNS Server in your router's admin panel.
Option B: Set DNS per-device. On each Linux machine:
echo "nameserver YOUR_SERVER_IP" | sudo tee /etc/resolv.conf
sudo chattr +i /etc/resolv.conf
On Windows: Settings > Network > your adapter > Edit > DNS > Manual > enter your server's IP
On macOS: System Settings > Network > your adapter > Details > DNS > add your server's IP
On Android/iOS: Typically under WiFi > your network > Advanced > DNS
Option A is preferred because it covers every device including phones, smart TVs, and IoT devices without touching each one individually.
How to Undo Everything
If something goes wrong and you need to revert to your original DNS setup:
1. Remove the immutable flag from resolv.conf:
sudo chattr -i /etc/resolv.conf
2. Restore public DNS:
echo -e "nameserver 1.1.1.1\nnameserver 8.8.8.8" | sudo tee /etc/resolv.conf
3. Stop and remove the container:
cd ~/docker/technitium && docker compose down
4. Re-enable systemd-resolved (if you want it back):
sudo systemctl enable --now systemd-resolved
sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
5. Verify DNS works again:
dig google.com +short
This puts you back exactly where you started.