Tech Expert & Vibe Coder

With 15+ years of experience, I specialize in self-hosting, AI automation, and Vibe Coding – building applications using AI-powered tools like Google Antigravity, Dyad, and Cline. From homelabs to enterprise solutions.

Building DNS-Based Service Discovery for Docker Swarm: Replacing Consul with Pi-hole Local DNS Records and Caddy Auto-Config

Why I Built This

I run a Docker Swarm cluster at home—three nodes handling everything from monitoring tools to media services. For months, I used Consul for service discovery. It worked, but it felt heavy. Every time I spun up a new service, I had to register it manually in Consul, then configure Caddy to read from Consul’s API. The whole setup required its own monitoring, backups, and occasional troubleshooting when Consul’s raft consensus got confused.

Then I realized: I already run Pi-hole for DNS across my network. It’s stable, simple, and I trust it. Why not use it for service discovery too?

The idea was straightforward—replace Consul with Pi-hole’s local DNS records and let Caddy read service locations directly from DNS. No external dependencies, no complex APIs, just DNS doing what it does best.

My Actual Setup

Here’s what I’m working with:

  • Three Proxmox nodes running Docker Swarm (one manager, two workers)
  • Pi-hole running in a separate container, handling all DNS for my home network
  • Caddy as my reverse proxy, deployed as a Swarm service
  • Multiple services spread across the cluster (n8n, Cronicle, monitoring dashboards, etc.)

Before this change, Consul sat between Pi-hole and Caddy. Services registered themselves in Consul, Caddy queried Consul’s HTTP API to find backends, and Pi-hole only handled external domain resolution.

How I Replaced Consul with Pi-hole

Adding Local DNS Records in Pi-hole

Pi-hole has a simple “Local DNS Records” section under its settings. I manually added A records for each service pointing to the Swarm node IPs where those services run. For example:

  • n8n.local192.168.1.101
  • cronicle.local192.168.1.102
  • grafana.local192.168.1.103

These are static entries. Docker Swarm doesn’t move services around unless a node fails, so this works fine for my use case. If a container restarts on the same node, the IP stays the same.

Configuring Caddy to Use DNS

Caddy supports DNS-based service discovery through its reverse_proxy directive. Instead of hardcoding IPs or querying Consul, I pointed Caddy at DNS names.

Here’s a simplified snippet from my Caddyfile:

n8n.vipinpg.com {
    reverse_proxy n8n.local:5678
}

cronicle.vipinpg.com {
    reverse_proxy cronicle.local:3012
}

Caddy resolves n8n.local through DNS (which Pi-hole answers), then proxies traffic to that IP on the specified port. No Consul API calls, no service registration logic—just DNS.

Handling Swarm Overlay Networks

One detail that tripped me up: services running on Swarm’s overlay network don’t expose their internal IPs outside the cluster. I had to ensure each service either published its port on the host (mode: host in the Compose file) or used Swarm’s ingress network properly.

For most services, I used published ports mapped to the host. This way, Caddy could reach them via the node’s IP without needing to be part of the overlay network itself.

What Worked

This setup has been running for several weeks now, and it’s simpler than what I had before:

  • No more Consul maintenance: I don’t have to worry about raft leader elections, storage backends, or API versioning.
  • Faster service changes: Adding a new service means updating Pi-hole’s DNS (which takes 10 seconds) and adding a Caddy block. That’s it.
  • Better visibility: I can query DNS directly with dig or nslookup to see where services are. No need to curl Consul’s API.
  • Fewer moving parts: Pi-hole was already critical infrastructure for me. Using it for service discovery removed one extra dependency.

Caddy’s auto-HTTPS still works perfectly. It handles certificates through Let’s Encrypt, and the DNS resolution happens transparently in the background.

What Didn’t Work

Dynamic Service Discovery

This approach is manual. If a service moves to a different node (which can happen if a node fails and Swarm reschedules it), I have to update the DNS record in Pi-hole. Consul handled this automatically through its service catalog and health checks.

For my setup, this isn’t a problem—node failures are rare, and I’m okay with a few seconds of manual intervention when they happen. But if you’re running a larger cluster with frequent rebalancing, this method won’t scale well.

Health Checks

Consul’s health checks were nice. If a service went down, Consul would stop routing traffic to it. DNS doesn’t do that—it just returns the IP, whether the service is healthy or not.

I compensated by adding health checks directly in Caddy using the health_uri option in my reverse proxy blocks. This way, Caddy pings each backend before sending traffic. It’s not as sophisticated as Consul’s checks, but it works for basic availability monitoring.

Load Balancing Across Replicas

If I scale a service to multiple replicas across different nodes, DNS-based discovery gets awkward. I’d need to either:

  • Add multiple A records for the same DNS name (round-robin DNS)
  • Use Swarm’s ingress network and publish the service on all nodes

I ended up using Swarm’s ingress for services that need multiple replicas. This way, any node can handle the request, and Swarm’s internal load balancer distributes traffic. Caddy just points to one node, and Swarm takes care of the rest.

Key Takeaways

  • DNS is enough for most home lab service discovery. You don’t need a full service mesh unless you’re running dozens of services with complex dependencies.
  • Pi-hole’s local DNS records are underused. Most people only use Pi-hole for ad blocking, but it’s a capable local DNS server.
  • Caddy’s DNS resolution is reliable. It caches lookups intelligently and re-resolves when needed.
  • Manual DNS updates are fine at small scale. If your cluster is stable and services don’t move often, the simplicity outweighs the automation.
  • Health checks still matter. Even with DNS-based discovery, you need a way to detect and handle backend failures.

When This Approach Makes Sense

This setup works well if:

  • You run a small to medium-sized home lab or self-hosted environment
  • Your services are relatively stable and don’t move between nodes frequently
  • You already use Pi-hole or another local DNS server
  • You prefer simplicity over full automation

It’s not suitable for production environments with high availability requirements, frequent deployments, or dynamic scaling. For those cases, stick with Consul, etcd, or a proper service mesh.

Current State

I’ve been running this for about a month now. The cluster handles daily traffic without issues, and I haven’t had to touch the DNS records since the initial setup. Caddy’s logs show clean resolution, and I haven’t seen any DNS-related timeouts or failures.

The main benefit isn’t performance—it’s operational simplicity. I removed a component that required its own care and feeding, and replaced it with something I was already maintaining anyway. That’s a win in my book.

Leave a Comment

Your email address will not be published. Required fields are marked *