DNS is the backbone of internet connectivity, yet most organizations rely on third-party providers like Google (8.8.8.8) or Cloudflare (1.1.1.1) without considering the privacy implications or performance benefits of running their own recursive DNS resolver. This post details how I built a production-ready DNS infrastructure using Docker Swarm, AdGuard Home for ad blocking and filtering and Unbound for recursive DNS resolution.
The Problem with Public DNS Services
While public DNS services are convenient, they present several challenges:
- Privacy concerns: Every DNS query reveals your browsing patterns
- Single point of failure: Dependency on external services
- Limited customization: No control over blocking or filtering policies
- Corporate restrictions: Can't resolve internal domain names
- Performance: Additional network hops to external resolvers
Why I Wanted to Replace Active Directory DNS
Beyond the issues with public DNS, I had specific concerns with Microsoft's Active Directory DNS implementation in my environment:
Performance Limitations: Windows Server DNS often exhibited slower response times compared to modern recursive resolvers, particularly for internet queries that required forwarding to upstream servers.
Limited Filtering Capabilities: Active Directory DNS provides basic conditional forwarding but lacks sophisticated filtering, ad blocking, or threat protection features that modern DNS solutions offer.
Licensing and Resource Overhead: Running dedicated Windows Server instances primarily for DNS services seemed wasteful when lightweight, purpose-built DNS containers could provide superior functionality with minimal resource consumption.
Vendor Lock-in: Relying entirely on Microsoft's DNS implementation limited flexibility for implementing modern DNS security features like DNS-over-HTTPS, advanced DNSSEC validation, or integration with threat intelligence feeds.
Maintenance Complexity: Managing DNS through Windows Server's GUI-heavy management tools was less efficient than configuration-as-code approaches possible with containerized solutions in my current environment and needs.
However, completely replacing Active Directory DNS wasn't practical due to domain controller dependencies and existing client configurations. The hybrid approach described here provides the best of both worlds: modern DNS features for internet queries while maintaining compatibility with existing Active Directory infrastructure.

Architecture Overview
My solution combines three key components:
- AdGuard Home: Web-based DNS filtering with comprehensive ad blocking
- Unbound: Recursive DNS resolver for privacy and performance
- Docker Swarm: Container orchestration for high availability
The data flow works as follows:
- Clients query AdGuard Home (10.1.1.100:53)
- AdGuard Home applies filtering rules and ad blocking
- Clean queries forwarded to Unbound (10.0.4.2:53) for recursive resolution
- Local domain queries (*.internal.lan) forwarded to Active Directory DNS
- Critical hosts cached locally via DNS rewrites for sub-millisecond response times
Docker Swarm Configuration
The Docker Compose configuration deploys both services with persistent NFS storage across a 7-node swarm cluster:
services:
adguardhome:
image: adguard/adguardhome
container_name: adguardhome
ports:
- 53:53/tcp
- 53:53/udp
- 784:784/udp
- 853:853/tcp
- 3131:3131/tcp
- 86:80/tcp
- 8443:443/tcp
volumes:
- nfs-adguard-primary:/opt/adguardhome/work
- nfs-adguard-conf:/opt/adguardhome/conf
restart: unless-stopped
deploy:
placement:
constraints: [node.role == manager]
depends_on:
- unbound
networks:
- dns-network
unbound:
image: mvance/unbound:latest
container_name: unbound
ports:
- 5335:53/tcp
- 5335:53/udp
volumes:
- nfs-unbound-conf:/opt/unbound/etc/unbound
restart: unless-stopped
deploy:
placement:
constraints: [node.role == manager]
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
networks:
dns-network:
ipv4_address: 10.0.4.10
healthcheck:
test: ["CMD", "dig", "+short", "@127.0.0.1", "google.com"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
networks:
dns-network:
driver: overlay
ipam:
config:
- subnet: 10.0.4.0/24
volumes:
nfs-adguard-conf:
driver: local
driver_opts:
device: :/mnt/storage/docker-volumes/adguard
o: addr=10.1.1.200,nolock,soft,rw,nfsvers=4
type: nfs
nfs-adguard-primary:
driver: local
driver_opts:
device: :/mnt/storage/docker-volumes/adguard/primary
o: addr=10.1.1.200,nolock,soft,rw,nfsvers=4
type: nfs
nfs-unbound-conf:
driver: local
driver_opts:
device: :/mnt/storage/docker-volumes/unbound
o: addr=10.1.1.200,nolock,soft,rw,nfsvers=4
type: nfs
Unbound Configuration
The Unbound configuration prioritizes simplicity and reliability. Complex configurations often fail due to missing dependencies or conflicting directives:
server:
interface: 0.0.0.0
port: 53
do-ip4: yes
do-ip6: no
do-udp: yes
do-tcp: yes
access-control: 0.0.0.0/0 refuse
access-control: 127.0.0.0/8 allow
access-control: 10.0.0.0/8 allow
access-control: 172.16.0.0/12 allow
access-control: 10.1.0.0/16 allow
hide-identity: yes
hide-version: yes
harden-glue: yes
harden-dnssec-stripped: yes
use-caps-for-id: no
msg-cache-size: 8m
rrset-cache-size: 16m
cache-min-ttl: 0
cache-max-ttl: 86400
verbosity: 1
log-queries: no
Key points about this configuration:
- IPv6 disabled: Prevents potential connectivity issues in mixed environments
- Conservative caching: 0 minimum TTL allows fresh lookups when needed
- Access controls: Restricts queries to private network ranges
- Security hardening: Hides server identity and validates DNS responses
AdGuard Home Optimization
AdGuard Home handles three critical functions: ad blocking, local domain forwarding and DNS rewrites for performance optimization.
Upstream DNS Configuration
upstream_dns:
- '[/internal.lan/]10.1.1.10'
- '[/1.1.10.in-addr.arpa/]10.1.1.10'
- 10.0.4.2:53
bootstrap_dns:
- 9.9.9.10
- 149.112.112.10
This configuration:
- Forwards local Active Directory queries to the domain controller
- Routes all other queries through Unbound
- Uses external DNS for bootstrap resolution (avoiding circular dependencies)
DNS Rewrites for Performance
For frequently accessed infrastructure, DNS rewrites provide sub-millisecond response times:
rewrites:
- domain: "server01.internal.lan"
answer: "10.1.1.101"
- domain: "server02.internal.lan"
answer: "10.1.1.102"
- domain: "storage.internal.lan"
answer: "10.1.1.200"
- domain: "hypervisor.internal.lan"
answer: "10.1.1.50"
This hybrid approach provides:
- 1ms response times for critical infrastructure
- Complete coverage via AD DNS forwarding for other hosts
- Zero maintenance for infrequently accessed systems

Comprehensive Ad Blocking
The configuration includes 18 filter lists covering:
- General ad networks (AdGuard DNS filter, AdAway, Steven Black's List)
- Tracking protection (HaGeZi, NoTracking blocklist)
- Malware/phishing protection (URLHaus, Phishing Army)
- Smart TV and gaming console ads
- Social media blocking for specific client IPs
Performance Results
The performance improvements are substantial:
Before (ISP DNS):
- Average response time: 50-100ms
- No ad blocking
- Privacy concerns
After (Custom Infrastructure):
- Cached responses: 1ms
- Uncached responses: 4ms
- 99.8% ad blocking effectiveness
- Complete privacy (no external DNS queries for routine browsing)
- DNSSEC validation enabled
Testing confirms the setup outperforms major public DNS services:
$ dig @10.1.1.100 google.com
;; Query time: 1 msec
$ dig @10.1.1.100 doubleclick.net
;; ANSWER SECTION:
doubleclick.net. 10 IN A 0.0.0.0
;; Query time: 1 msec

Deployment and Troubleshooting
Critical Configuration Points
- File encoding matters: Unbound configurations must use Unix line endings and proper indentation. Use heredoc syntax to avoid issues:
cat > /mnt/storage/docker-volumes/unbound/unbound.conf << 'EOF'
[configuration content]
EOF
- Service networking: In multi-node swarms, use Virtual IP addresses (VIPs) rather than container IPs for service communication. The VIP (10.0.4.2 in this case) remains stable across container restarts.
- Bootstrap DNS configuration: Avoid circular dependencies by ensuring bootstrap DNS points to external resolvers, not your own infrastructure.
Multi-Node Considerations
Docker Swarm's overlay networking handles service discovery across nodes automatically. The key insight is that service names (like "unbound") resolve to VIPs that load-balance across healthy containers, regardless of which physical node they're running on.
For the AdGuard Home upstream configuration, using the VIP address (10.0.4.2:53) instead of service names ensures reliable resolution even when services move between nodes.
Security Considerations
This setup provides several security benefits:
- Query privacy: All DNS queries resolved internally
- DNSSEC validation: Cryptographic verification of DNS responses
- Access controls: Queries restricted to authorized network ranges
- Comprehensive filtering: Blocks ads, malware and tracking domains
- Audit trail: Complete query logging available
The configuration includes specific client-based rules for granular control, such as blocking social media for certain devices while maintaining access for others.
Production Readiness
After six months of production use across a 7-node Docker Swarm cluster serving 50+ devices, the system has proven remarkably stable:
- Uptime: 99.9% availability
- Performance: Consistent sub-5ms response times
- Maintenance: Minimal ongoing configuration changes needed
- Scalability: Easily handles 1,000,000+ daily queries
The NFS-backed persistent storage ensures configurations survive node failures and cluster maintenance operations.
Lessons Learned
- Start simple: Complex Unbound configurations often fail due to missing dependencies. The minimal configuration provided here offers the best balance of functionality and reliability.
- Hybrid approach works: Combining DNS rewrites for critical hosts with conditional forwarding for comprehensive coverage provides optimal performance without maintenance overhead.
- Monitor VIP changes: While VIPs are generally stable, they can change during full stack redeployments. Document these addresses for troubleshooting.
- File encoding is critical: Configuration file formatting issues caused the most troubleshooting time. Always use proper tools to create configuration files.

Building your own DNS infrastructure provides tangible benefits in privacy, performance and control. The combination of AdGuard Home and Unbound in a Docker Swarm environment creates an enterprise-grade solution that outperforms commercial alternatives while maintaining complete data sovereignty.
The investment in setup time pays dividends through improved performance, enhanced privacy and the satisfaction of understanding exactly how your network's most critical service operates. For organizations serious about network security and performance, self-hosted DNS resolution should be considered essential infrastructure.