HomeThe Stash

Run Your Own DNS Resolver With Unbound in Docker

LabSeptember 20, 2026

BeginnerTime ~25 minNeeds DockerCost freeNo sign-up, runs local

0 / 7 steps

The goal

Run a real recursive DNS resolver — Unbound — in a container, resolve names through it, and prove it is validating DNSSEC. By the end you will understand how DNS actually resolves, why running your own resolver matters for privacy, and why DNS is a channel defenders have to watch. It is also the perfect small first service to log into a SIEM.

Port 53 is usually taken. On Linux, systemd-resolved already listens on port 53, so this lab maps Unbound to 5335 instead — the same port the popular Pi-hole + Unbound setup uses. Keep the resolver on your own machine; do not expose it to the internet (open resolvers get abused for DNS amplification).

New to Docker? Start with Build a Security Lab With Docker — this lab assumes you can run and stop a container.

Steps

01Confirm Docker and get the dig tool

You need Docker running and the dig DNS lookup tool to test your resolver. Check Docker first:

$ docker --version

If dig is missing, install it — it lives in dnsutils (Debian/Ubuntu) or bind-utils (Fedora/RHEL):

$ sudo apt-get install -y dnsutils

On macOS dig ships with brew install bind; on Windows, run these commands from inside WSL.

Why dig. It lets you send a query to a specific resolver and read the raw answer — flags, TTLs, timing — which is exactly what you need to prove your Unbound is doing its job.

02Run Unbound in a container

Pull and start the widely-used Unbound image, mapping container port 53 to 5335 on your machine for both TCP and UDP:

$ docker run -d --name unbound -p 5335:53/tcp -p 5335:53/udp mvance/unbound:latest

That is the whole install. Unbound comes pre-configured as a caching, DNSSEC-validating recursive resolver — no config file to edit to get started.

Recursive vs forwarding. A forwarding resolver just relays your queries to Google or Cloudflare. This is a recursive resolver: it walks the DNS tree itself, from the root servers down, so no third party sees your full browsing in one place.

03Check it started cleanly

Confirm the container is up and read its startup log:

$ docker ps --filter name=unbound
$ docker logs unbound

You want a line like start of service and no fatal errors. If the run in step 2 failed with a port already allocated message, another service holds 5335 — pick a different host port (say 5336:53) and use that below.

Read the logs first. A resolver that started but cannot reach the root servers will look “up” in docker ps yet fail every query. The log tells you which it is.

04Resolve a name through your resolver

Now ask your Unbound to look up a domain. The @127.0.0.1 -p 5335 part points dig at your container instead of your system default:

$ dig @127.0.0.1 -p 5335 stashgrid.ai +short

You should get back one or more IP addresses. That answer was resolved recursively by the container you just started — not by your ISP.

What +short does. It trims dig’s output to just the answer. Drop it to see the full response — the section flags in the next step live in that longer output.

05Prove DNSSEC validation is working

DNSSEC lets a resolver cryptographically verify that an answer really came from the zone’s owner. Ask for a DNSSEC-signed name and look at the flags:

$ dig @127.0.0.1 -p 5335 +dnssec cloudflare.com

In the flags: line you want to see adAuthenticated Data. That means Unbound checked the signatures and they hold. Now the negative test, against a domain deliberately broken to fail validation:

$ dig @127.0.0.1 -p 5335 dnssec-failed.org

A validating resolver returns SERVFAIL here and gives you no address — it is refusing to hand you an answer it cannot trust. That refusal is the whole point.

Why this matters. Without DNSSEC, an attacker who can tamper with DNS responses can send you to a server they control. The ad flag and that SERVFAIL are your resolver stopping exactly that.

06Watch the cache work

Run the same lookup twice and watch the timing:

$ dig @127.0.0.1 -p 5335 wikipedia.org

Note the Query time at the bottom. Run it again immediately and the time drops to 0 msec — the second answer came straight from Unbound’s cache instead of another full recursive walk.

Caching is speed and signal. It makes repeat lookups instant, and because every query passes through here, this is where a defender would log DNS — the raw material for spotting tunneling, exfiltration and command-and-control.

07Stop and clean up

When you are done, stop and remove just this container:

$ docker stop unbound && docker rm unbound

To actually use Unbound day to day, you would point your OS or router at it — but for a lab, tearing it down clean is the right move.

Named containers are tidy. Because you started it with --name unbound, stop and rm touch only this one — nothing else on your machine is affected.

Lab complete

You ran your own recursive, DNSSEC-validating DNS resolver, resolved and verified real names through it, and saw the cache in action. You now understand DNS from the resolver’s side — the side defenders care about.

What you learned

  • Recursive vs forwarding resolvers — and why running your own changes who sees your DNS.
  • DNSSEC validation — the ad flag and a SERVFAIL on broken signatures.
  • Reading dig output — flags, answers, TTLs and query time.
  • DNS caching — why the second lookup is instant.
  • Why DNS is a security signal — every query through one place is a log a SIEM can watch.

NextFeed DNS logs into a free SIEM — the Wazuh lab

Go deeper

Part of the StashGrid Docker lab track. Image tag or flag changed? Tell me and I will fix it.