Skip to content

Installing Technitium DNS Server on an Ubuntu LXC

This page covers a clean install of Technitium DNS Server in a Proxmox LXC and the initial setup: web console, forwarders, recursion, ad blocking, DHCP, and zones.

By the end you have a DNS server that hands out its own address over DHCP, filters ads for every client on the network, resolves an internal Active Directory domain through a conditional forwarder, and forwards everything else upstream over DNS-over-HTTPS.

Split-horizon DNS and the Nginx Proxy Manager integration are covered on separate pages, Split-Horizon DNS and Nginx Proxy Manager. Get this page working first.

All addresses and domain names on this page are examples. Substitute your own.

Example value What it is
10.10.0.0/24 The lab LAN
10.10.0.3 The Technitium container
10.10.0.7 Nginx Proxy Manager
10.10.0.100, 10.10.0.99 Domain controllers
example.com The public domain, also served internally
lab.local The internal Active Directory domain

What you need first

  • A Proxmox host and an Ubuntu 24.04 LXC template
  • A static IP for the container. A DNS server cannot use DHCP for its own address.
  • Nothing else on the network already listening on port 53
  • A decision about DHCP. If a router or firewall currently runs DHCP, that has to be turned off before Technitium takes over. Two DHCP servers on one subnet hand out conflicting leases and produce failures that are hard to trace.

Container

Create an unprivileged LXC from the Ubuntu 24.04 template.

Setting Value
CT ID 101
Hostname technitium
Node pve1
Template Ubuntu 24.04
Unprivileged Yes
CPU 2 cores
RAM 1024 MB
Swap 2048 MB
Disk 16 GB
Network Bridged to the LAN, static IP 10.10.0.3
Start at boot Yes
Tags linux, prod

Start at boot is not optional. If this container does not come up automatically, nothing on the network resolves after a host reboot, including the Proxmox web interface by name. Set it when you build the container, not after the first outage.

Sizing. These numbers come from a container that has been running the full setup described on this page: DNS, DHCP, three blocklists, and query logging.

Resource Allocated Actual use
CPU 2 cores Under 1% at idle
Memory 1 GiB Around 240 MiB, roughly 23%
Swap 2 GiB Around 160 MiB
Disk 16 GB Around 2.2 GB

Technitium will run on 512 MB, but 1 GB gives room for blocklists and cache without touching swap under load. Do not go below 1 GB if you enable query logging.

Disk is where it is easy to overspend. The application, the OS, and the blocklists together sit near 2 GB. The reason to allocate more than that is the SQLite query log, which grows with query volume and is the only part of this install that gets meaningfully larger over time. 16 GB is comfortable. Allocating 50 GB is wasted space on the Proxmox storage pool, and growing an LXC disk later is a single command, so start small.

Tags. Proxmox tags cost nothing and make the container list readable once the lab passes ten guests. A prod tag on the DNS server is a useful reminder that a careless snapshot rollback here takes the whole network down with it.

DHCP has to reach clients by broadcast, so the container needs a bridged veth on the same bridge as the clients it serves. It will not work across a NAT'd or isolated bridge.

Free up port 53

Ubuntu runs systemd-resolved, which binds a stub listener on 127.0.0.53:53. Technitium cannot start until that is out of the way.

systemctl disable --now systemd-resolved
rm -f /etc/resolv.conf
echo "nameserver 1.1.1.1" > /etc/resolv.conf

Confirm the port is free. This should return nothing:

ss -lntup | grep ':53'

Install

The official installer handles the .NET runtime, the application files, and the systemd service.

curl -sSL https://download.technitium.com/dns/install.sh | sudo bash

When it finishes you will have:

/opt/technitium/dns     application files, install.log, start.sh, systemd.service
/opt/dotnet             bundled .NET runtime

The installer also drops a resolv.conf.bak in the application directory, so the original resolver config is recoverable if you need it.

Check the service:

systemctl status dns.service
journalctl -u dns -f
ss -lntup | grep ':53'

First login

Open http://10.10.0.3:5380.

Default credentials are admin / admin. Change the password immediately under the account menu. This server controls name resolution for every client on the network.

The dashboard shows a version banner in the top right when an update is available. Updating is covered at the end of this page.

Settings

General

Field Value Why
DNS Server Domain ns01.example.com The name this server uses to identify itself. It shows up in SOA and NS records, so pick it before creating zones.
DNS Server Local End Points 10.10.0.3:53 Bind to the LAN address rather than everything.
DNS Server IPv4 Source Addresses 0.0.0.0 Default.
Default Record TTL 600 Ten minutes. Short enough that a record change propagates quickly while you are still building the lab.
Default NS Record TTL 14400 Default.

Web Service

Field Value
Web Service HTTP Port 5380
Enable HTTPS On
Enable HTTP to HTTPS Redirection On
Use A Self Signed TLS Certificate When TLS Certificate File Path Is Unspecified On
Web Service HTTPS Port 53443
Real IP Header X-Real-IP

The console is then reachable at https://10.10.0.3:53443. The browser will warn about the self-signed certificate, which is expected on a LAN-only console.

The Real IP Header setting only matters if you later put the console behind a reverse proxy. Without it, every entry in the admin logs shows the proxy's address instead of the actual client.

Optional Protocols

Enable DNS-over-HTTPS if you want clients to be able to query this server over encrypted DNS. Leave the rest off unless you have a reason. Each protocol you enable is another listener to secure.

Recursion

Field Value
Recursion Allow Recursion Only For Private Networks
QNAME Minimization On

Do not select Allow Recursion. That turns the server into an open resolver. If it is ever reachable from the internet, it will be found and used for DNS amplification attacks. The private-networks option is the correct choice for a lab resolver.

QNAME minimization sends only the label the upstream server needs at each step instead of the full name, so intermediate servers see less of what you are looking up.

Proxy & Forwarders

Field Value
Forwarders https://1.1.1.1/dns-query
https://1.0.0.1/dns-query
Forwarder Protocol DNS-over-HTTPS
Enable Concurrent Forwarding On
Forwarder Concurrency 2

Anything the server is not authoritative for and cannot answer from cache goes here. DNS-over-HTTPS means the upstream lookups are encrypted, so the ISP sees a TLS connection instead of a stream of plaintext queries.

Concurrent forwarding queries both forwarders at once and takes whichever answers first, rather than working through them in order. It costs a little extra upstream traffic and removes the timeout delay when one forwarder is slow.

Blocking

Field Value
Enable Blocking On
Allow TXT Blocking Report On
Blocking Type NX Domain
Blocking Answer TTL 30

Add blocklist URLs under Allow / Block List URLs. The StevenBlack hosts lists are a reasonable starting point, and the repository publishes themed variants so you can pick how aggressive the filtering is:

https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/porn/hosts
https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-porn/hosts

NX Domain tells the client the name does not exist, which most applications handle cleanly. The alternative, returning 0.0.0.0, leaves the client trying to connect to an address that will never answer and often produces a slower timeout instead of a fast failure.

The short blocking TTL of 30 seconds matters when you have to unblock something. Add the domain to the allowed list and clients pick up the change within half a minute instead of holding a bad answer for hours.

When an application breaks right after you enable blocking, check Dashboard > Top Blocked Domains before assuming the application is at fault. Blocklists routinely take out click-tracking links in email and telemetry endpoints that some apps treat as a hard dependency.

DHCP

DHCP > Scopes > Add Scope

Field Value
Name Lab
Scope range 10.10.0.20 to 10.10.0.70
Subnet mask 255.255.255.0
Router 10.10.0.1
DNS servers 10.10.0.3
Domain name example.com
Interface 10.10.0.3

The DNS servers field is the point of running DHCP here. It puts every client on Technitium without touching individual machines.

Reserve the space above and below the scope range. Static infrastructure lives outside it. A range of .20 to .70 on a /24 leaves the low addresses for servers and the high addresses for anything you add later.

Technitium registers leases as A records in the matching forward zone automatically, and the records carry a comment noting which scope created them. Those show up alongside your manual records in the zone view, which is normal. Do not delete them by hand. They expire with the lease.

Turn off the old DHCP server before enabling the scope, not after.

Zones

Zone type is the decision that matters most on this page. The wrong type causes outages that do not look like DNS problems.

Primary zone

A primary zone makes Technitium authoritative for that domain on your network.

Zones > Add Zone > Primary Zone, name example.com.

Two things to know before creating a zone for a domain you also own publicly:

  1. Technitium now answers for every name under it. Anything not defined in the local zone returns an empty answer instead of falling through to public DNS.
  2. Names that were working purely because public DNS answered for them will stop resolving on the LAN. Webmail, admin interfaces, and any published subdomain all need an A record in the internal zone.

Be precise about what actually needs recreating. SPF, DKIM, and DMARC do not. Those are read by the receiving mail server using public DNS, so your internal resolver never participates and mail authentication is unaffected. An MX record only matters if something on the LAN resolves mail delivery by MX rather than through a configured relay host, which most applications do not.

What breaks is plain hostname resolution. Walk your public records after creating the zone and recreate the names clients need to reach.

None of this applies if you name the zone something that does not exist publicly, such as lab.internal. Choosing the real domain is a deliberate trade: internal and external hostnames stay identical, and in exchange you own the internal copy of every name. The split-horizon page covers that trade in full. Make the choice before you create the zone, because changing it later means reissuing certificates and updating every application that has the hostname baked into its configuration.

A reverse zone is worth creating alongside it. Add Zone > Primary Zone, name 0.10.10.in-addr.arpa. This gives you working PTR lookups, which makes logs and monitoring readable.

Conditional forwarder zone

A conditional forwarder sends queries for one domain to a specific server without making Technitium authoritative for it.

Zones > Add Zone > Conditional Forwarder Zone, name lab.local.

Inside the zone, add FWD records:

Name Type Protocol Forwarder
@ FWD UDP 10.10.0.100
dc02 FWD UDP 10.10.0.99

The @ record forwards the whole domain to the first domain controller. A named record forwards one specific host to a different server, which is useful when a second DC is the faster or more reliable answer for its own name.

Do not create a primary zone for an Active Directory domain. The domain controller has to stay authoritative for it. AD registers its own SRV records dynamically, and those records are how domain-joined machines locate the DC, find the global catalog, and complete Kerberos authentication. If Technitium answers for the AD domain instead of forwarding to the DC, those lookups return nothing and domain-joined machines stop authenticating.

With the conditional forwarder in place, the DCs, SCCM server, and domain-joined clients resolve normally, and everything outside the AD domain still goes out through the regular forwarders.

Zone options

Open a primary zone and click Options.

Tab Setting Value
Query Access Allow Only Private Networks
Zone Transfer Allow Only Name Servers In Zone
Dynamic Updates Deny (default)

Query Access set to private networks only means the zone is never served to a public client, even if the server somehow becomes reachable from outside. On a zone that holds internal addresses, this is the setting that keeps your internal layout from leaking.

Zone Transfer restricted to name servers in the zone stops anyone from pulling the entire zone in one request. An unrestricted zone transfer hands over a complete inventory of every host you have named.

Dynamic Updates set to deny blocks RFC 2136 updates from clients. This does not affect the DHCP scope registering leases, which happens inside the server rather than over the wire.

Apps

Apps > App Store installs DNS applications that extend the server.

Two worth installing:

  • Split Horizon. Returns different answers depending on which network the client is on. This is the basis of the split-horizon page.
  • Query Logs (Sqlite). Writes query logs to a local database so the Logs tab becomes searchable. Useful when tracking down which client is asking for what. It does add write load, so on a busy server watch disk usage.

Verify

From a Windows client, confirm it actually picked up the new resolver:

ipconfig /all
Get-DnsClientServerAddress -AddressFamily IPv4
Clear-DnsClientCache

Then test each resolution path:

# Recursive lookup through the forwarders
dig @10.10.0.3 example.org +short

# Internal name from the primary zone
dig @10.10.0.3 npm.example.com +short

# AD domain, answered by the domain controller through the conditional forwarder
dig @10.10.0.3 lab.local SOA +short

# AD service record, proves the forwarder is working properly
dig @10.10.0.3 _ldap._tcp.lab.local SRV +short

# Blocked domain, should return NXDOMAIN
dig @10.10.0.3 doubleclick.net

# Reverse lookup
dig @10.10.0.3 -x 10.10.0.7 +short

The dashboard shows live query counts and a client total. If it stays near zero after leases renew, clients are still pointed somewhere else.

Maintenance

systemctl restart dns.service
systemctl status dns.service
journalctl -u dns -n 100 --no-pager

Updating. The dashboard shows a banner when a new version is available. Re-run the same install script. It updates in place and keeps the configuration.

curl -sSL https://download.technitium.com/dns/install.sh | sudo bash

Backup. Use Administration > Backup in the console to export a zip containing settings, zones, apps, and blocklists. Do that before any zone change. The on-disk config lives outside the application directory:

ls -la /etc/dns

Back up the container itself too. Proxmox Backup Server handles this at the LXC level, which is faster to restore than rebuilding the service.

Next

The split-horizon setup with Nginx Proxy Manager builds directly on this configuration.


References