If you’re like me, you love self-hosting your own projects. But as the number of services you run grows, so does the list of DNS records.
I used to log into my Cloudflare dashboard constantly, adding A record after A record. It felt messy, and I ended up with dozens of entries for my main domain.
I wanted a cleaner solution – a way to isolate my self-hosted projects (like Beszel) from my main site, gain full control over their DNS, and maybe even learn a thing or two about how DNS really works under the hood.
That’s where CoreDNS comes in. It’s a tiny, incredibly fast, and easy-to-configure DNS server that’s perfect for this. With a single configuration file and a lightweight Docker container, you can build your own authoritative DNS server.
In this guide, I'll walk you through the exact steps I took to set up my own authoritative DNS server for a subdomain.
We’re not just forwarding queries; we’re building the official source of truth for a dedicated zone, like lab.yourdomain.com. It’s a game-changer for managing a growing collection of self-hosted services.
Don’t worry if this is new – I'll walk you through it, step by step.
If you want more write-ups like this one, subscribe to my newsletter – I send an email when I publish new guides and walkthroughs.
The Big Picture: What We're Building
Before we get our hands dirty, let’s zoom out and look at the goal.
We’re going to set up a brand new, authoritative DNS server for a dedicated subdomain. For this guide, I’ll be using lab.ivansalloum.com, but you can use any subdomain you like.
Our CoreDNS server will live at ns1.lab.ivansalloum.com. This is what’s known as an "in-bailiwick" nameserver, which is a fancy way of saying the nameserver (ns1.lab...) resides within the same subdomain (lab...) that it manages.
It’s a clean, tidy, and scalable way to structure your DNS.
If you decide to create another environment later (like dev.yourdomain.com), you can repeat the pattern without everything getting tangled up.
A quick note on SEO: A common worry is whether running projects on a subdomain will hurt your main site’s search engine ranking. Don’t worry, it won’t. Google and other search engines treat subdomains as separate properties. As long as your projects don’t create security issues, your main domain’s SEO will be completely unaffected.
By the end of this guide, any DNS query for *.lab.yourdomain.com will be answered directly by your CoreDNS server, giving you instant control to create, update, and manage records.
Preparing Your Server
Before we dive into CoreDNS specifics, we need to get your server ready.
I’m writing this guide based on my experience with an Ubuntu 24.04 LTS server hosted on a Hetzner VPS, but the steps should be broadly applicable to most Linux distributions.
Set Your Server's Hostname
It’s good practice to give your server a meaningful hostname.
For this setup, it makes sense to use the nameserver's own FQDN (Fully Qualified Domain Name):
sudo hostnamectl set-hostname ns1.lab.ivansalloum.comThis command updates your server's hostname to reflect its role as the nameserver for your lab environment.
Remember to replace ivansalloum.com with your actual domain!
Register Your Nameserver's IP (Glue Records)
It's absolutely crucial to register the IP addresses for your nameserver with your domain registrar or DNS provider. These are often called "glue records" and they tell the internet where ns1.lab.ivansalloum.com actually lives.
Without them, no one will be able to find your custom DNS server.
You'll need to create A and optionally AAAA records for your nameserver's hostname (ns1.lab.ivansalloum.com) at your DNS provider.
Ensure these records are set up to directly point to your server's IP addresses and are not proxied if your provider offers that option (e.g., Cloudflare's orange cloud). DNS traffic for nameservers must hit your server directly.
Also, do not add an A record for just lab at this stage; we are only creating records for ns1.lab itself. The lab subdomain will be delegated later.
Free Up Port 53 (Stop systemd-resolved)
DNS services communicate primarily over port 53.
systemd-resolved (Ubuntu’s local DNS stub) might already be listening on this port, preventing CoreDNS from binding to it. We need to disable its stub listener.
You can check if port 53 is in use with:
sudo lsof -i :53If you see systemd-resolved listed, follow these steps to disable it.
First, create the directory for systemd-resolved overrides:
sudo mkdir -p /etc/systemd/resolved.conf.d/Then, set appropriate permissions for the newly created directory:
sudo chmod 755 /etc/systemd/resolved.conf.d/Next, disable the DNSStubListener by writing the configuration to a file:
printf "[Resolve]\nDNSStubListener=no\n" | sudo tee /etc/systemd/resolved.conf.d/noresolved.confFinally, restart systemd-resolved to apply the changes:
sudo systemctl restart systemd-resolved.serviceAfter restarting, sudo lsof -i :53 should no longer show systemd-resolved listening.
Configure Your Firewall (UFW)
For your CoreDNS server to receive queries, you'll need to open the necessary ports in your firewall.
DNS traffic primarily uses UDP port 53, but TCP port 53 is also essential:
sudo ufw allow 53/udp
sudo ufw allow 53/tcp
sudo ufw enable # If UFW is not already enabledAlmost all DNS queries use UDP because it's fast and lightweight. However, if a DNS response is too large, or for critical operations like zone transfers (especially important if you ever add a secondary DNS server), TCP is used.
Install Docker
We'll be running CoreDNS inside a Docker container for ease of deployment and management.
It's crucial to use up-to-date Docker Engine and Docker Compose versions because Ubuntu's default repositories often contain outdated packages.
For optimal performance and the latest features, always rely on Docker's official repositories.
Create CoreDNS Directories
We need a dedicated place for CoreDNS's configuration and zone files:
sudo mkdir -p /opt/coredns/{config,zones}
sudo chmod -R 755 /opt/corednsWith these preparations done, your server is ready to host your authoritative CoreDNS instance!
Creating Your First Zone File
Now for the fun part. We need to create a zone file. This is a plain text file that acts as the database for your subdomain.
It contains all the DNS records – like A, AAAA, CNAME, etc. – that tell the world where to direct traffic for hosts within your zone.
First, navigate to the zones directory we created earlier:
cd /opt/coredns/zonesNext, create and open the zone file. It's a common convention to name this file db. followed by your zone name. In my case, it's db.lab.ivansalloum.com:
sudo vim db.lab.ivansalloum.comdb.lab.ivansalloum.com?The name is purely for convention and readability. The
db prefix is a holdover from BIND, one of the oldest DNS servers, where db stood for "database." You could name it records.txt if you wanted, as long as you point to the correct file in the CoreDNS configuration later. Sticking to convention makes your setup instantly familiar to other sysadmins (and your future self).Now, paste the following template into the file. This is the heart of your DNS setup:
$ORIGIN lab.ivansalloum.com.
$TTL 3600
@ IN SOA ns1.lab.ivansalloum.com. admin.ivansalloum.com. (
2025120601 ; Serial (YYYYMMDDnn) - CHANGE THIS ON EVERY EDIT!
7200 ; Refresh (2 hours)
120 ; Retry (2 minutes)
2419200 ; Expire (28 days)
3600 ; Negative Cache TTL (1 hour)
)
IN NS ns1.lab.ivansalloum.com.
ns1 IN A YOUR_SERVER_IPV4
ns1 IN AAAA YOUR_SERVER_IPV6
; --- Your Custom Records Go Here ---
beszel IN A 192.168.1.100
hestia IN A 192.168.1.1
; --- Optional Wildcard For Instant Testing ---
* IN A 192.168.0.10Once you've pasted the template, here are the three critical edits you must make before saving:
- Go through the template and replace
lab.ivansalloum.com.,ns1.lab.ivansalloum.com.,- and
admin.ivansalloum.com. - with your actual subdomain, nameserver FQDN, and admin email.
- Replace
YOUR_SERVER_IPV4andYOUR_SERVER_IPV6with the actual public IP addresses of your CoreDNS server. If you don't use IPv6, simply delete that line entirely. - The line
2025120601is the zone's version number. It's crucial that you increment this number every time you edit the file. A common format isYYYYMMDDnn, wherennis the revision number for the day. For this first setup, I recommend setting it to today's date inYYYYMMDDnnformat. This makes it easy to track your initial zone version.
Save the file and exit the editor (:wq in vim).
What Did We Just Create?
After saving your new zone file, you've essentially created the instruction manual for your lab.ivansalloum.com subdomain.
Let's quickly walk through what we've put in there.
Think of your zone file as doing two main jobs.
First, it declares your server's authority. The SOA (Start of Authority) and NS (Nameserver) records are like official stamps, announcing that your CoreDNS server is the definitive source for all information within lab.ivansalloum.com.
The SOA record specifies who's in charge and crucial timing parameters, while the NS record simply states which server is the designated nameserver.
Second, it builds your subdomain's address book. The A and AAAA records for ns1 are particularly special; they're called glue records, providing the IP address for your nameserver so the internet knows where to find it.
Below these are your custom records (like beszel and hestia), which point your services to their respective IP addresses.
It's like setting up the front desk (SOA, NS) and then filling out the directory (A, AAAA, custom records) for your new DNS office.
Pretty neat, right?
Configuring CoreDNS with the Corefile
Our zone file is ready, holding all our DNS records.
But how do we tell CoreDNS to actually use it? This is where the Corefile comes in, and honestly, its simplicity is the main reason I fell in love with CoreDNS.
Instead of wrestling with complex, multi-file setups, CoreDNS uses one single, human-readable file to manage everything. It’s where we tell CoreDNS what zones to serve, what plugins to use, and how to behave.
Let's get it set up.
Navigate to the config directory you created earlier:
cd /opt/coredns/configNow, create and open the Corefile itself:
sudo vim CorefilePaste this small block of text in. This is all you need to get a basic authoritative server running:
lab.ivansalloum.com:53 {
log
errors
file /zones/db.lab.ivansalloum.com
}Save and close the file.
Let's quickly dig into what this does:
lab.ivansalloum.com:53 { ... }: This is the server block. It tells CoreDNS, "Hey, for any queries that come in forlab.ivansalloum.comon port 53, this is the rulebook."log&errors: I always add these two plugins right away.errorsmakes sure any issues are clearly reported, andloggives us a live feed of all the DNS queries our server receives. It’s perfect for debugging and just seeing your new server in action.file /zones/db.lab.ivansalloum.com: This is the heart of our setup. Thefileplugin points to our zone file, making CoreDNS the official, authoritative source for all the records within it. The path here is relative to the inside of our future Docker container, where/zoneswill be the directory we created.
And that's it. We've defined our records and told CoreDNS how to serve them.
The last piece of the puzzle is to package it all up and run it.
Docker Compose Configuration
Now that we have our zone file and Corefile configured, it's time to bring our CoreDNS server to life. For this, I absolutely love using Docker.
We'll use docker compose to define and run our CoreDNS service. This allows us to set up all the necessary parameters – like ports, volumes, and restart policies – in a single, easy-to-read file.
Let's create our docker-compose.yml file in the main /opt/coredns directory:
sudo vim /opt/coredns/docker-compose.ymlPaste the following content into the file:
services:
coredns:
image: coredns/coredns:latest
container_name: coredns
restart: unless-stopped
ports:
- "53:53/udp"
- "53:53/tcp"
volumes:
- ./config/Corefile:/Corefile:ro
- ./zones:/zones:roSave and close the file.
This docker-compose file defines how our CoreDNS server will run. In short, it:
- Pulls the official CoreDNS image and names the container
coredns. - Sets the
restartpolicy tounless-stopped, which is a must-have for any self-hosted service to ensure it automatically starts up after a reboot. - Maps the DNS ports (
53/udpand53/tcp) from your host server to the container. - Mounts our configuration files (the
Corefileand ourzonesdirectory) into the container in read-only mode, so CoreDNS can use them.
With this docker-compose.yml in place, we're just one command away from launching our very own authoritative DNS server!
Launching CoreDNS and Local Testing
This is the moment of truth!
We've prepared our server, crafted our zone file, and configured CoreDNS.
Now, let's fire up our container and make sure everything is working as expected.
First, navigate to the main /opt/coredns directory where your docker-compose.yml file is located:
cd /opt/corednsNow, launch CoreDNS with Docker Compose in detached mode (-d), meaning it will run in the background:
sudo docker compose up -dYou should see output indicating that the coredns container has been created and started.
With our CoreDNS server now running, let's immediately verify it's answering queries locally before we go live. This local verification step is crucial as it confirms our setup works before we expose it to the internet.
We'll use the dig command-line tool to do this. The trick is to query your own server's IP address directly, bypassing any other DNS resolvers. Remember to replace YOUR_SERVER_IPV4 with your server's actual public IPv4 address.
First, let's confirm our server is authoritative for the zone by querying the SOA record:
dig @YOUR_SERVER_IPV4 lab.ivansalloum.com SOAYou should see your SOA record returned, matching the details you put in db.lab.ivansalloum.com.
Next, let's query one of the specific test records we added, like beszel:
dig @YOUR_SERVER_IPV4 beszel.lab.ivansalloum.comThis should return the A record for beszel (e.g., 192.168.1.100).
Finally, if you included the optional wildcard record, test it:
dig @YOUR_SERVER_IPV4 anything.lab.ivansalloum.comThis should resolve to the IP address defined in your wildcard entry.
If all these dig commands return the expected answers, congratulations! Your authoritative DNS server is fully functional and ready to serve requests for your subdomain.
The next step is to tell the rest of the internet to start asking your server for these records.
We're almost there!
Delegating Your Subdomain
Our CoreDNS server is humming along, locally serving up records like a champ.
But right now, only your server knows about it. The final, exhilarating step is to tell the world – specifically, through your domain registrar or DNS provider (in my case, Cloudflare) – that for your chosen subdomain, your CoreDNS server is now the one in charge.
This is called delegation.
It's a powerful moment, making your self-hosted DNS server officially responsible for lab.ivansalloum.com (or whatever subdomain you chose).
Head over to your domain registrar or DNS provider and add a new NS (Nameserver) record for your subdomain:
- Type:
NS - Name:
lab(or your chosen subdomain part, e.g.,lab.ivansalloum.com) - Value:
ns1.lab.ivansalloum.com.(your nameserver's FQDN, with a trailing dot!) - TTL:
Autois usually fine.
Crucial Sanity Checks:
- NS Record must be "DNS Only": If you use a provider like Cloudflare, ensure this NS record is not proxied (i.e., it should have a grey cloud).
- No conflicting A record: Make sure you do NOT have an
Arecord forlabitself. This conflicts with delegation because a domain can either be a delegation (NS record) or point to an IP (A record), but not both. You should only have theNSrecord forlab.
This step will only work if you've already created the
A and AAAA records for ns1.lab.ivansalloum.com as we did in the "Preparing Your Server" section. These glue records are what allow the internet to find your nameserver in the first place.Public Validation: Confirming Your Delegation
The CoreDNS server is running, the zone file is configured, and your DNS provider has been updated to delegate your subdomain.
Now comes the exciting part: confirming that the entire internet sees your CoreDNS server as the authoritative source for your subdomain.
While DNS changes can sometimes take a while to propagate, subdomain delegation is usually quite fast – I've often seen it go live within 1 to 10 minutes.
Once you've given it a moment, you can verify your setup from any machine other than your CoreDNS server. This ensures you're querying external DNS resolvers, just like the rest of the world. We'll use dig for this.
First, let's ask a public resolver (like 1.1.1.1) to confirm that our new nameserver is correctly set up for the subdomain:
dig lab.ivansalloum.com NS @1.1.1.1You should see your nameserver (ns1.lab.ivansalloum.com.) in the answer section, which confirms the delegation is visible.
Next, let's test that your CoreDNS server is serving the actual records. Try querying one of the custom records you created in your zone file:
dig beszel.lab.ivansalloum.com @1.1.1.1This should return the A record for beszel that you defined earlier.
If both of these commands work, your authoritative DNS server is officially live and serving records to the internet!
Managing Your DNS Records
Now that your authoritative DNS server is live, the best part is how easy it is to manage your records. The process is simple and gives you full control.
Here’s the step-by-step workflow to update or add new records.
Step 1: Edit Your Zone File
First, open your zone file:
sudo vim /opt/coredns/zones/db.lab.ivansalloum.comNow, you can either change an existing record or add new ones.
CoreDNS supports all standard DNS record types, including A, AAAA, CNAME, TXT, and more.
For example, to add new A, CNAME, and TXT records, you would add lines similar to these:
; --- Your Custom Records Go Here ---
beszel IN A 192.168.1.100
hestia IN A 192.168.1.1
wordpress IN A 192.168.1.105 ; A record for our new WordPress instance
www.beszel IN CNAME beszel.lab.ivansalloum.com. ; CNAME for www.beszel pointing to beszel
@ IN TXT "v=spf1 include:_spf.example.com ~all" ; TXT record for SPFRemember to replace the example values with your own service names and data.
Step 2: Increment the Serial Number (Crucial!)
This is a critical step, primarily for secondary DNS servers (if you ever add them) to know your zone file has changed and needs updating.
The only strict rule is that the serial number must be a single number that strictly increases every time you make a change. However, using the YYYYMMDDnn format (Year, Month, Day, and a two-digit revision for changes on that day) is a widely-adopted best practice for human readability and sanity.
Here's how to manage it:
- First change of the day: Update the date part to today's date and set the two-digit counter to
01. For example, if your old serial was2025120202and today is December 7th, 2025, your new serial would be2025120701. - Subsequent changes on the same day: Only increment the last two digits. For example, a second change on December 7th would make it
2025120702.
Always remember to increment this number whenever you modify your zone file.
Step 3: Restart the CoreDNS Container
After saving and closing the file, you need to restart the CoreDNS container so it reloads your updated zone file.
Navigate to the directory where your docker-compose.yml file is located:
cd /opt/corednsThen, simply run the restart command:
sudo docker compose restartThis command will gracefully restart the coredns service. It's very fast, usually taking just a second or two.
Once it restarts, CoreDNS will have loaded your new records.
Step 4: Verify Your Changes
Finally, to be sure everything worked, test your new record with dig from your local machine:
dig wordpress.lab.ivansalloum.com @1.1.1.1You should see the new IP address (192.168.1.105) reflected in the answer.
That's it! You can repeat this process any time you need to add, update, or remove a DNS record.
Conclusion and Final Thoughts
Congratulations! You've successfully deployed your very own authoritative DNS server with CoreDNS, giving you unparalleled control over your subdomains.
This setup empowers you to:
- Isolate your self-hosted projects: Keep your main domain's DNS clean and separate.
- Enjoy instant control: Add, update, or remove records in seconds, without waiting for third-party providers.
- Deepen your DNS understanding: You've walked through the core concepts of DNS delegation, zone files, and record management.
This journey into self-hosting your DNS is a fantastic step towards greater independence and control in your personal infrastructure.
Keep experimenting, keep learning, and enjoy the power you now have at your fingertips.
If you run into any issues or need further help, feel free to revisit this guide or reach out for assistance.
If you found this guide helpful, consider subscribing to my newsletter (beneath the “Read next” section) for more tips and walkthroughs on self-hosting, security, and everything in between!
And if have thoughts and feedback, drop them in the discussion section; I always enjoy seeing how others build on this.
Discussion