Building Enterprise-Grade DNS Infrastructure with Docker Swarm, AdGuard Home and Unbound

Built a DNS infrastructure with Docker Swarm, AdGuard Home and Unbound delivering 1ms response times, comprehensive ad blocking and complete privacy while maintaining Active Directory compatibility.

7 min read
Building Enterprise-Grade DNS Infrastructure with Docker Swarm, AdGuard Home and Unbound

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:

  1. AdGuard Home: Web-based DNS filtering with comprehensive ad blocking
  2. Unbound: Recursive DNS resolver for privacy and performance
  3. 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

  1. 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
  1. 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.
  2. 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

  1. Start simple: Complex Unbound configurations often fail due to missing dependencies. The minimal configuration provided here offers the best balance of functionality and reliability.
  2. Hybrid approach works: Combining DNS rewrites for critical hosts with conditional forwarding for comprehensive coverage provides optimal performance without maintenance overhead.
  3. Monitor VIP changes: While VIPs are generally stable, they can change during full stack redeployments. Document these addresses for troubleshooting.
  4. 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.