Xcaddy + Cloudflare Tunnel + AdGuard Home: A Simpler Homelab Edge

By Anas Semesmieh · June 6, 2026 · Homelab, Security, Automation

I run a lot of services at home. Over time, my ingress and DNS setup became harder to reason about than the apps themselves. Traefik labels multiplied across compose files, per-service routing rules became noisy, and small changes required jumping between containers and dynamic files.

I wanted one place to read, one place to debug, and one policy for local and remote access. The stack I ended up with is native Caddy (built via Xcaddy) + Cloudflare Tunnel + AdGuard Home. This post is the architecture, why I moved, and exactly what changed.

This migration was not just "different tooling." It was an operability decision: fewer moving parts, cleaner config ownership, and faster troubleshooting under pressure.

Why I Moved Away from Traefik Labels and Pi-hole

Traefik worked, but label-driven routing became expensive to maintain at scale. A single service often required router, middleware, entrypoint, TLS, and service-port labels. Repeating that pattern across many stacks increased cognitive load and made reviews harder.

Caddy gave me a single-file model in /etc/caddy/Caddyfile. Each hostname is a readable block, and per-service tweaks live beside the route they affect. That reduced context switching immediately.

I also moved away from Pi-hole to AdGuard Home. The AdGuard UI is cleaner for daily operations, and its built-in filter ecosystem and rewrite workflow are smoother for my use case.

Architecture Overview

Core Components

Local Flow

  1. Client resolves service hostname using AdGuard Home
  2. AdGuard rewrite returns LAN endpoint
  3. Caddy handles HTTPS and proxies to the target service

Remote Flow

  1. Client reaches Cloudflare edge
  2. Cloudflare Tunnel forwards approved hostnames only
  3. Caddy routes request internally to upstream service
Path Entry Point Name Resolution Exposure Model
Local Caddy on LAN AdGuard rewrites Private by default
Remote Cloudflare edge + Tunnel Cloudflare DNS/hostname policy Explicit opt-in per hostname

Caddy in One File: What Simplified Operations

My current Caddy config starts with Cloudflare ACME DNS integration and then declares host routes in one place:

{
	acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}

home.semesmieh.com {
	reverse_proxy 192.168.20.69:3000
}

code.semesmieh.com {
	reverse_proxy http://192.168.20.69:8443
}

For edge cases, behavior stays close to the route. Example: self-signed upstreams use local transport overrides where needed:

portainer.semesmieh.com {
	reverse_proxy https://192.168.20.69:9443 {
		transport http {
			tls_insecure_skip_verify
		}
	}
}

That single-file readability is the main reason this became easier to operate than label-heavy dynamic wiring.

AdGuard Home: Cleaner DNS and Filter Management

AdGuard now handles DNS and filtering directly from the host config at /opt/AdGuardHome/AdGuardHome.yaml.

Example rewrite set from the running config:

filtering:
  rewrites:
    - domain: home.semesmieh.com
      answer: 192.168.20.69
      enabled: true
    - domain: code.semesmieh.com
      answer: 192.168.20.69
      enabled: true
    - domain: nvr.semesmieh.com
      answer: 192.168.20.69
      enabled: true

The built-in filter integrations were another practical win. Instead of maintaining fragmented external list workflows, I can manage a large set of curated feeds directly in AdGuard with better day-to-day visibility.

Why I Run Caddy and AdGuard Natively (Not Docker)

I deliberately run both as host-native services. In my environment, this improves responsiveness and reduces runtime overhead around critical network paths.

Containers still make sense for many app workloads. For edge and DNS, native services gave me a cleaner operational model.

What I Lost and What I Gained

Area Lost Gained
Ingress management Label-driven service discovery convenience Single-file config clarity and lower cognitive overhead
DNS operations Pi-hole familiarity and legacy setup Cleaner UI, richer integrated filter workflow, simpler rewrites
Runtime model Uniform container pattern for everything Native performance and simpler control for edge services

Official Documentation

Closing Thought

This migration was less about chasing new tools and more about reducing operational drag. Caddy gave me a one-file ingress model, AdGuard gave me cleaner DNS operations, and native deployment kept critical paths fast and straightforward.

If your homelab is growing and your edge stack feels harder to reason about than your actual apps, this pattern is worth a look.