Why I Needed This
I run multiple internal services in my homelab—n8n, Synology apps, Proxmox web UI, monitoring dashboards. For years, I accessed them over plain HTTP or dealt with browser certificate warnings. It worked, but it felt sloppy. More importantly, some tools (like modern browsers and certain APIs) started complaining or blocking HTTP connections entirely.
I wanted proper HTTPS everywhere, but I didn’t want to manually manage certificates or deal with Let’s Encrypt rate limits. My homelab changes often—I spin up new containers, test services, and tear things down. If I used Let’s Encrypt directly for every internal hostname, I’d hit their limits fast. I needed something that just worked without constant babysitting.
My Setup
I’m running Caddy in a Docker container on my Proxmox host. All my internal services sit behind it—some in other containers, some on separate VMs, one on my Synology NAS. I use a local DNS server (Pi-hole) to resolve internal domain names like n8n.home.local and proxmox.home.local to internal IPs.
Caddy handles all HTTPS termination. Services behind it don’t need to know anything about certificates—they just run on HTTP internally, and Caddy translates requests.
Caddy Container
I run Caddy using Docker Compose. My docker-compose.yml looks like this:
version: '3.8'
services:
caddy:
image: caddy:latest
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- homelab
volumes:
caddy_data:
caddy_config:
networks:
homelab:
external: true
The caddy_data volume is critical. That’s where Caddy stores certificates and its internal CA. If you lose this volume, you’ll have to reinstall Caddy’s root certificate on every client again.
How Caddy Handles Internal Certificates
Caddy has a feature called “Local HTTPS” that solves the rate limit problem completely. When you give it a hostname that isn’t publicly routable—like .local, .internal, or localhost—it doesn’t try to use Let’s Encrypt at all.
Instead, Caddy generates its own Certificate Authority (CA) on first run. It creates a root certificate and an intermediate certificate, stores them in /data/caddy/pki/authorities/local, and uses them to sign certificates for your internal services.
The first time Caddy runs, it tries to install this root CA into your system’s trust store. On Linux, it prompted me for a password. On macOS, it opened a system dialog. On Windows, it did the same. This is a one-time thing per machine.
Once installed, every browser and tool on that machine trusts certificates signed by Caddy’s CA. No warnings, no exceptions, just green padlocks.
My Caddyfile
Here’s a simplified version of what I actually use:
n8n.home.local {
reverse_proxy n8n:5678
}
proxmox.home.local {
reverse_proxy 192.168.1.50:8006 {
transport http {
tls_insecure_skip_verify
}
}
}
synology.home.local {
reverse_proxy 192.168.1.60:5000
}
Each block defines a service. Caddy sees the .home.local suffix and automatically generates a certificate using its internal CA. No ACME challenges, no DNS validation, no external dependencies.
The Proxmox block has tls_insecure_skip_verify because Proxmox’s internal certificate is self-signed. Caddy doesn’t care—it just proxies the connection and presents its own valid certificate to the client.
What Worked
This setup has been running for over a year now. I’ve added and removed services dozens of times. Caddy generates new certificates instantly—there’s no delay, no approval process, no rate limits to worry about.
Certificates renew automatically. Caddy’s intermediate certificate has a short lifetime (I think 7 days by default), but it renews itself silently in the background. I’ve never had to touch it.
The root CA installation worked smoothly on most of my devices. I installed it once on my laptop, once on my phone (manually exported the cert from Caddy’s data directory), and once on a Windows desktop. After that, every service just worked.
Mobile Access
Getting the root CA onto my phone took a few extra steps. I copied /data/caddy/pki/authorities/local/root.crt from the Caddy container to my laptop, then transferred it to my phone via email. On iOS, I had to install it through Settings → General → VPN & Device Management, then trust it under Settings → General → About → Certificate Trust Settings. Android was similar but required navigating to Settings → Security → Install from storage.
Once done, I could access all my internal services from my phone over HTTPS without warnings.
What Didn’t Work
The automatic trust store installation failed on one of my Linux VMs. Caddy couldn’t write to /usr/local/share/ca-certificates/ because it was running as a non-root user inside the container. I had to manually run docker exec -it caddy caddy trust and enter my sudo password to fix it.
I also ran into an issue where I accidentally deleted the caddy_data volume during a cleanup. Caddy regenerated everything, but the new root CA was different, so all my clients started showing certificate errors. I had to reinstall the new root on every device. That taught me to back up the volume.
Another mistake: I initially tried to use .home as my internal TLD. Caddy treated it as a public domain and tried to get Let’s Encrypt certificates, which failed because my DNS wasn’t publicly resolvable. Switching to .home.local fixed it immediately.
Wildcard Confusion
I tried using a wildcard certificate (*.home.local) thinking it would simplify things. Caddy generated the cert, but it didn’t work for nested subdomains like api.n8n.home.local. I ended up just listing each service explicitly in the Caddyfile. It’s more lines, but it’s also clearer and easier to debug.
Key Takeaways
Use .local, .internal, or another reserved TLD for internal services. Caddy will automatically use its internal CA instead of trying ACME.
Back up the caddy_data volume. Losing it means regenerating the CA and reinstalling the root certificate everywhere.
Trust store installation is mostly automatic, but expect to manually handle it on some systems—especially headless servers or containers.
Keep the Caddyfile simple. Explicit service definitions are easier to manage than wildcards or complex routing logic.
If you need to inspect or export Caddy’s root CA, it’s stored at /data/caddy/pki/authorities/local/root.crt inside the container. You’ll need this for mobile devices or systems where automatic installation failed.
Caddy’s internal CA certificates have short lifetimes by design, but renewal is completely automatic. I’ve never seen a renewal fail or cause downtime.
This approach scales well for homelabs. I’m currently proxying about 15 services, and Caddy handles it without breaking a sweat. No rate limits, no external dependencies, no manual certificate management.