Caddy as a Homelab Reverse Proxy
Every homelab eventually reaches the same inflection point. You have five services running on five different ports — Proxmox on 8006, Grafana on 3000, Immich on 2283, Forgejo on 3001, Home Assistant on 8123 — and you are tired of remembering which port maps to what. You want real domain names. You want HTTPS. You do not want to think about certificate management ever again.
If you have been doing this for a while, your first instinct is probably Nginx. Nginx is battle-tested, fast, and well-documented. The problem is that Nginx has no concept of TLS certificate automation. You configure Nginx to serve HTTPS, then you separately run Certbot to get the certificate, then you write a renewal cron job, then you reload Nginx after renewal. Four separate systems, four places for things to break. Every six months your certs expire and you spend forty minutes debugging which piece of the chain failed.
Traefik solves some of this in Docker environments but adds its own complexity — label hell, the dashboard is opaque when routes are not loading, and the static versus dynamic configuration split trips up everyone at least once.
Caddy takes a different approach entirely. The philosophy is that HTTPS should be the default, not an option you configure. You give Caddy a domain name, and it handles everything: ACME account registration, certificate issuance from Let’s Encrypt or ZeroSSL, automatic renewal, OCSP stapling, and HSTS headers. The Caddyfile for a basic HTTPS reverse proxy is three lines. Not three hundred lines — three.
This guide covers Caddy in depth for homelab use: installation, the Caddyfile syntax and JSON API, automatic certificate management including DNS-01 challenges for internal services, reverse proxying with health checks, static file serving, rate limiting, and an honest comparison with Nginx and Traefik including when Caddy is not the right tool.
Everything here applies to Caddy v2, which is the current series (the latest stable release at time of writing is 2.11.3). Caddy v1 reached end-of-life years ago and the config model is completely different — if you find v1 tutorials, ignore them.
Why Caddy Exists and What Makes It Different
The traditional approach to HTTPS involves assembling a toolchain. Nginx serves your traffic. Certbot (or acme.sh) handles certificate issuance. A cron job or systemd timer handles renewal. Post-renewal hooks reload Nginx. Each piece works but none of them know about each other. When Certbot changes where it stores certs, your Nginx config breaks. When your DNS propagation is slow during renewal, the HTTP-01 challenge fails silently. When you add a new virtualhost, you have to register it in three places.
Caddy was written to make this entire toolchain unnecessary. Matt Holt started the project in 2015 with a simple observation: the configuration you write for a web server almost always implies what certificates you need. If you tell Caddy to serve photos.example.com, it can figure out on its own that it needs a certificate for photos.example.com. There is no reason to make this a separate step.
Caddy is written in Go and ships as a single statically-linked binary with no external dependencies. No OpenSSL version conflicts, no module path confusion, no shared library hell. You copy the binary, point it at a Caddyfile, and it works. The binary includes its own ACME client (based on the certmagic library, which Holt also wrote), its own certificate storage layer, its own OCSP stapling implementation, and its own local CA for development use.
The v2 rewrite, released in 2020, replaced the v1 config model completely. v2 has two configuration formats: the Caddyfile, which is a human-readable domain-specific language, and a JSON format that is Caddy’s actual native config. The Caddyfile is compiled to JSON internally — when you run caddy adapt --config Caddyfile, you see exactly what JSON Caddy is running. This dual-format design means you can use the simple Caddyfile for 95% of cases and drop down to JSON for complex programmatic configuration when needed.
Recent releases have pushed Caddy further into privacy-forward TLS territory. Caddy 2.10 added fully-automated Encrypted ClientHello (ECH), which encrypts the SNI field in TLS handshakes — the last plaintext piece that reveals which domain a client is connecting to. It also added post-quantum key exchange via x25519mlkem768 as a default, and support for ACME profiles including 6-day certificate lifetimes from Let’s Encrypt. Caddy 2.11 added automatic ECH key rotation, time-rolling log options, and improved reverse proxy behavior around Host header rewriting for HTTPS upstreams. These are not features you typically see in a web server’s release notes — they reflect a project that treats TLS as its primary concern, not an afterthought.
Installation
Package Repository (Recommended)
The Caddy project maintains its own package repository for Debian/Ubuntu, which is the preferred installation method because it sets up a proper systemd service, creates the caddy system user, and establishes the correct directory layout.
|
|
After installation, the directory layout is:
/etc/caddy/Caddyfile # Your configuration file
/var/lib/caddy/ # Caddy's working directory (owned by caddy user)
/var/lib/caddy/.local/share/caddy/certificates/ # Certificate storage
/var/log/caddy/ # Log output
/usr/bin/caddy # The binary
The caddy user owns /var/lib/caddy/ and Caddy runs as that user under systemd. This matters for certificate storage permissions and for binding to ports 80 and 443 — the systemd service grants the binary the CAP_NET_BIND_SERVICE capability so it can bind privileged ports without running as root.
Docker
|
|
The UDP port 443 binding is for HTTP/3 (QUIC), which Caddy enables by default when serving HTTPS.
Building with Plugins (xcaddy)
The standard Caddy binary does not include DNS provider modules or the rate limiting module. If you need DNS-01 certificate challenges (required for internal services and wildcard certs), you must build a custom binary with the appropriate DNS provider plugin compiled in.
|
|
Alternatively, the Caddy download page at caddyserver.com/download lets you select plugins and download a pre-built binary without needing a Go toolchain.
An important note for DNS plugin users: Caddy 2.10 upgraded to libdns 1.0, which broke compatibility with DNS provider modules that had not updated. If you are running Caddy 2.10 or later, ensure your DNS provider module is version 1.6 or newer (for the Cloudflare module: github.com/caddy-dns/cloudflare@v1.6.0 or later).
Managing the Caddy Process
|
|
caddy reload is what you want in production. It sends the new configuration to the running process, which validates it and swaps it in atomically. No connections are dropped, no certificates need to be re-read from disk, no listeners bounce. This is the same mechanism the JSON admin API uses under the hood.
The Caddyfile — Syntax and Structure
The Caddyfile is designed to be readable by humans who are not configuration language experts. A minimal example:
|
|
That is a complete, production-ready HTTPS reverse proxy configuration. Caddy will obtain a Let’s Encrypt certificate for photos.example.com automatically, redirect HTTP to HTTPS, and proxy all traffic to your Immich instance on port 2283.
Global Options Block
The global options block, if present, must come first. It controls Caddy-wide behavior.
|
|
Site Addresses
Caddy interprets the site address to decide what TLS mode to use:
| Address format | TLS behavior |
|---|---|
example.com |
Automatic HTTPS via ACME (Let’s Encrypt/ZeroSSL) |
*.example.com |
Wildcard cert via ACME DNS-01 challenge |
http://example.com |
No TLS, HTTP only |
https://example.com |
TLS required, ACME or manual cert |
localhost |
Internal CA cert via tls internal |
:8080 |
HTTP on port 8080, no TLS |
192.168.1.10 |
No automatic TLS for bare IP addresses |
Snippets and Imports
Snippets let you define reusable blocks of directives, which is essential for avoiding copy-paste across multiple site blocks:
|
|
Named Matchers
Named matchers allow conditional request handling. The @name syntax defines a matcher that can be reused:
|
|
Environment Variable Substitution
Sensitive values like API tokens should never live directly in the Caddyfile. Caddy supports environment variable substitution with {env.VAR_NAME}:
|
|
When running under systemd, inject environment variables via an override file:
|
|
|
|
A Complete Annotated Multi-Service Caddyfile
|
|
Automatic HTTPS — How It Works
Caddy’s certificate automation is built on the ACME protocol (RFC 8555). When Caddy starts and sees a hostname in your configuration, it checks whether a valid certificate exists in its storage. If not, it initiates an ACME order with Let’s Encrypt or ZeroSSL.
The Three ACME Challenge Types
HTTP-01: The most common challenge. Caddy creates a token file and serves it at http://<domain>/.well-known/acme-challenge/<token>. The CA fetches that URL from the public internet to verify you control the domain. Requirements: port 80 must be open and reachable from the internet. This is the default when port 80 is accessible.
TLS-ALPN-01: Caddy answers a special TLS handshake on port 443 using the ALPN extension. Requires port 443 reachable from the internet. Useful when port 80 is blocked.
DNS-01: Caddy creates a _acme-challenge.your-domain TXT record via your DNS provider’s API. The CA validates by querying DNS rather than making an HTTP request. No port exposure required, works for private/internal domains, and is the only way to obtain wildcard certificates.
Let’s Encrypt vs ZeroSSL
Caddy registers ACME accounts with both Let’s Encrypt and ZeroSSL by default and rotates between them. If Let’s Encrypt is experiencing issues or you hit rate limits, Caddy falls back to ZeroSSL automatically. Both CAs are free, both issue 90-day DV certificates (with ACME profiles now supporting 6-day certs), and both are trusted by all major browsers.
Let’s Encrypt rate limits: 50 certificates per registered domain per week. For a homelab with many subdomains, wildcard certificates (which count as one certificate regardless of how many subdomains you use) are strongly preferred.
Certificate Storage
Certificates are stored under /var/lib/caddy/.local/share/caddy/certificates/ when running as the caddy system user. The layout is:
certificates/
acme-v02.api.letsencrypt.org-directory/
home.example.com/
home.example.com.crt
home.example.com.key
home.example.com.json # ACME order metadata
acme.zerossl.com-v2-DV90/
...
Caddy renews certificates automatically when they reach 30 days remaining. You do not need a cron job. You do not need to reload Caddy after renewal — it detects new certificates in storage and starts using them without any restart.
Internal CA for Development
For local development or for services that never need a CA-trusted certificate:
|
|
tls internal tells Caddy to issue a certificate from its own built-in CA. The cert is not publicly trusted, but you can install Caddy’s local CA into your system trust store:
|
|
This installs the CA certificate into your OS certificate store (and into Firefox’s NSS store on Linux), after which browsers on that machine will trust Caddy’s locally-issued certificates without warnings.
DNS Challenge for Internal Homelab Services
This is where Caddy becomes genuinely powerful for homelabs. The scenario: you have a NAS, a Proxmox cluster, and a media server running on your LAN. You want real, CA-trusted TLS certificates for these services — not self-signed certs, not browser warnings — but these hosts are not accessible from the public internet. The HTTP-01 and TLS-ALPN-01 challenges cannot reach them. DNS-01 is the only option.
The DNS-01 challenge does not require your services to be reachable at all. It only requires that Caddy can call your DNS provider’s API to create a TXT record. The CA validates by querying public DNS for that record. Your internal services are never contacted.
How the DNS-01 Flow Works
Caddy DNS Provider API Let's Encrypt
| | |
|-- ACME order request ---------------------------------> |
| | |
| CA responds: add TXT record |
| <------------------------------------------------------- |
| | |
|-- Set TXT record ---------> | |
| _acme-challenge. | |
| home.example.com | |
| | |
|-- Notify CA: ready ----------------------------------> |
| | |
| CA queries DNS for TXT record |
| | <------------------------ |
| | |
| CA validates TXT matches |
| | |
| Certificate issued |
| <------------------------------------------------------- |
| | |
|-- Delete TXT record ------> | |
DNS Provider Modules
Caddy does not include DNS provider modules in the standard build. The caddy-dns GitHub organization maintains modules for most major DNS providers. These must be compiled in with xcaddy or obtained via the download page.
| Provider | Module path |
|---|---|
| Cloudflare | github.com/caddy-dns/cloudflare |
| Route53 (AWS) | github.com/caddy-dns/route53 |
| Porkbun | github.com/caddy-dns/porkbun |
| Namecheap | github.com/caddy-dns/namecheap |
| Gandi | github.com/caddy-dns/gandi |
| Hetzner | github.com/caddy-dns/hetzner |
| deSEC | github.com/caddy-dns/desec |
| DYNDNS/generic | github.com/caddy-dns/ddnss |
Remember: if you are running Caddy 2.10 or later, install provider modules version 1.6 or later to ensure libdns 1.0 compatibility.
Configuring Cloudflare DNS-01
First, create a Cloudflare API token with minimal permissions. In the Cloudflare dashboard: My Profile > API Tokens > Create Token. Use the “Edit zone DNS” template and scope it to only the specific zone you need. The token needs Zone:DNS:Edit — do not create a global API key.
|
|
|
|
The *.home.example.com wildcard certificate covers every subdomain with a single certificate. One issuance, one renewal, one cert entry in Let’s Encrypt’s rate limit accounting.
Split-Horizon DNS
For internal services, you typically want split-horizon DNS: internal DNS resolves *.home.example.com to your LAN IP (e.g., 192.168.1.5 where Caddy runs), while external DNS either does not resolve the record or points nowhere. This means:
- Services are only reachable from inside your LAN
- The Let’s Encrypt certificate is still publicly trusted
- No VPN required for internal access
- The certificate is obtained via DNS-01 (no public HTTP access needed)
Configure this with your local DNS resolver (Pi-hole, AdGuard Home, Unbound, or your router’s DNS). Add an A record for *.home.example.com pointing to your Caddy host’s LAN IP. Clients on your LAN resolve the name internally; clients outside your LAN get no resolution at all.
Reverse Proxy Configuration
Basic Proxying
|
|
Multiple Upstreams and Load Balancing
|
|
lb_policy round_robin: Default. Good for stateless applications where any backend can handle any request.
lb_policy least_conn: Best for long-lived connections (WebSockets, streaming) where connection count is a meaningful measure of load.
lb_policy ip_hash: For stateful applications that do not use shared session storage — the same client always hits the same backend. Be aware this can cause uneven load if a small number of IPs generate most traffic.
lb_policy first: Active-passive failover. All traffic goes to the first healthy upstream; other upstreams only receive traffic if the first fails.
Health Checks
Caddy supports both passive and active health checking.
Passive health checks (enabled by default): Caddy monitors responses from upstreams. When an upstream returns consecutive errors or times out, it marks it as unhealthy and stops sending traffic. It periodically sends a probe to check if the upstream has recovered.
Active health checks: Caddy proactively sends a request to a configured health endpoint on a timer, independent of actual traffic:
|
|
Header Manipulation
|
|
Trusted Proxies
If Caddy sits behind a CDN or upstream load balancer (Cloudflare, AWS ALB), you need to tell it which IPs to trust for X-Forwarded-For headers. Otherwise Caddy will log the load balancer’s IP as the client IP rather than the real client:
|
|
Timeouts
|
|
WebSocket Proxying
Caddy handles WebSocket upgrades automatically. You do not need to set Upgrade, Connection, or proxy_pass headers manually as you would in Nginx. A regular reverse_proxy directive handles WebSocket connections transparently:
|
|
This is a genuine quality-of-life improvement over Nginx, where you must explicitly configure the upgrade:
|
|
gRPC Proxying
For gRPC backends using HTTP/2 cleartext (h2c), prefix the upstream address:
|
|
Static File Serving
Caddy is a competent static file server, though this is not where it differentiates itself from Nginx.
|
|
Serving a Hugo or SPA Site
|
|
The try_files {path} /index.html pattern is essential for single-page applications where the client-side router handles URL paths that do not correspond to real files. Without it, a browser refresh on a deep link returns 404.
Rate Limiting and Security Headers
Rate Limiting
This is where you need to be clear-eyed about Caddy’s limitations. The standard Caddy distribution does not include a rate limiting module. Rate limiting requires the caddy-ratelimit plugin by Matt Holt (github.com/mholt/caddy-ratelimit), which you must compile in with xcaddy.
This is a real difference from Nginx, where limit_req_zone is built in and extensively documented. It is also a difference from Traefik, where rate limiting is a first-class middleware. For homelab use it is rarely a blocking issue, but it is worth knowing before you commit to Caddy for a public-facing service where you need rate limiting at the edge.
|
|
|
|
The caddy-ratelimit module uses a sliding window ring buffer per key. Unlike Nginx’s leaky bucket implementation, it does not require you to pre-allocate a shared memory zone — expired rate limiters are garbage-collected automatically. It also supports distributed rate limiting across a Caddy cluster by writing state to a shared storage backend.
Security Headers
Security headers that should be standard on every service:
|
|
HSTS
Caddy adds Strict-Transport-Security automatically when serving HTTPS, with a default max-age of two years. To customize:
|
|
Be careful with preload. Once a domain is in the HSTS preload list, browsers refuse to connect over HTTP even for the first visit — before they have seen your HSTS header. This is permanent and takes months to undo.
IP Allowlisting
|
|
Basic Authentication
|
|
|
|
Forward Auth (Authentik, Authelia)
For centralized authentication, delegate to an external identity provider:
|
|
When the auth upstream returns 2xx, the request proceeds. Any other status code (typically a redirect to the login page) is returned directly to the client. The copy_headers directive copies authentication metadata from the auth response into the proxied request, so your backend can see which user made the request.
The JSON Config API
The Caddyfile is a convenience layer. Caddy’s actual native configuration format is JSON, and the Caddyfile is compiled to JSON before Caddy runs it. Understanding the JSON format unlocks Caddy’s full flexibility and its admin API.
Viewing the JSON Equivalent
|
|
The output of caddy adapt is verbose — a simple three-line Caddyfile produces 100+ lines of JSON once all defaults are filled in. This verbosity is why the Caddyfile exists for human use, while JSON is better for programmatic generation.
Admin API Operations
The admin API runs on localhost:2019 by default. It accepts GET, POST, PUT, PATCH, and DELETE requests to manage specific parts of the config tree.
|
|
When to Use JSON Over Caddyfile
The Caddyfile covers 95% of use cases. Reach for JSON when:
- You are generating Caddy configuration programmatically (from a service registry, a YAML manifest, or a script)
- You need complex conditional logic that Caddyfile syntax cannot express
- You want to update individual parts of the config without touching others
- You are building automation that adds/removes routes as services start and stop
A practical example: a script that reads a services.yaml file and generates Caddy JSON to register each service. This avoids the Caddyfile entirely and treats Caddy like a programmable API gateway:
|
|
This is the model Caddy was designed for. The admin API makes zero-downtime config updates trivially scriptable — no signal handling, no file watches, no nginx -s reload.
Caddy vs Nginx vs Traefik
Three different tools built with three different assumptions about the world.
Nginx was built in 2004 to solve the C10K problem — serving tens of thousands of concurrent connections on a single server. It is written in C, uses an event-driven non-blocking architecture, and has been performance-tuned for two decades. Its config model is imperative and explicit: you describe exactly what Nginx should do, and it does that. There is no magic. TLS configuration is manual — you point it at certificate files, and when those files change you reload. This explicitness is both its strength and its maintenance burden.
Traefik was built for cloud-native environments. Its key insight is that in Docker and Kubernetes, services come and go dynamically, and manually maintaining a reverse proxy config for ephemeral containers does not scale. Traefik watches the Docker daemon or Kubernetes API and configures itself from labels and annotations. Deploy a container with traefik.http.routers.myapp.rule=Host('myapp.example.com') and Traefik routes to it automatically. This works well — until it does not. The static/dynamic config split confuses new users, debugging why a route is not loading requires diving into the Traefik dashboard and its provider configuration, and Traefik’s certificate management, while automatic, is less mature than Caddy’s.
Caddy occupies the middle ground. It is dynamic enough for homelabs and small production deployments via its admin API, but does not require a container orchestrator. Its TLS automation is the most sophisticated of the three — OCSP stapling, multiple CA rotation, automatic wildcard cert management via DNS-01, ECH, post-quantum key exchange — none of which require any configuration. The Caddyfile syntax is genuinely readable. The trade-off is a smaller ecosystem, some features requiring plugin builds, and a performance ceiling that, while excellent, falls short of Nginx for pure static file serving.
Comparison Table
| Feature | Caddy | Nginx | Traefik |
|---|---|---|---|
| Automatic TLS | Yes, built-in | No (requires Certbot/acme.sh) | Yes, but more configuration |
| Config format | Caddyfile (human-readable) + JSON | nginx.conf (verbose, declarative) | TOML/YAML (static) + labels (dynamic) |
| Docker integration | Manual or via API | Manual | Native (label-based auto-discovery) |
| Kubernetes integration | Manual or via JSON API | Via ingress controller | Native (IngressRoute CRD or Ingress) |
| Static file performance | Good (~38k req/s) | Best (~45k req/s) | Lower (~32k req/s) |
| Memory footprint | 30-100MB | 15-50MB | 50-200MB |
| HTTP/3 (QUIC) | Built-in | Requires build flag / recent version | Built-in in v3 |
| WebSocket proxying | Automatic | Manual header config required | Automatic |
| gRPC proxying | Supported (h2c://) | Supported | Supported |
| Rate limiting | Plugin required (caddy-ratelimit) | Built-in (limit_req_zone) | Built-in middleware |
| Plugin ecosystem | Growing, requires xcaddy builds | Mature, many modules | Growing |
| Learning curve | Low | Medium | Medium-High |
| Production maturity | Good | Excellent | Good (v3+) |
| Single binary | Yes | No (shared libs) | Yes |
| Admin API | Yes (REST) | Limited (nginx -s) | Yes (dashboard + API) |
The Honest Recommendation
Choose Caddy if you are running a homelab, a small-to-medium self-hosted stack, or a new project where developer experience matters. Caddy’s automatic HTTPS is not a gimmick — it genuinely eliminates an entire category of operational toil. The Caddyfile is readable by anyone who can read English. For internal services with DNS-01 wildcard certs, Caddy is the simplest solution that exists. Caddy handles the vast majority of homelab use cases — reverse proxying, static files, WebSockets, basic auth, forward auth — without plugins and without configuration ceremony. At the traffic levels of a homelab or small team, the performance difference versus Nginx is irrelevant.
Choose Nginx if you need the absolute ceiling of static file performance, you need specific Nginx modules (ModSecurity, Lua scripting via OpenResty, specific auth modules), you are operating at scale where the performance difference matters, or you are working in an environment where everyone already knows Nginx. Nginx’s battle-tested production history is real. If you are putting a reverse proxy in front of a high-traffic service where you need every millisecond and every module, Nginx is the correct choice. Accept that you will manage TLS separately.
Choose Traefik if you are running Docker Compose or Kubernetes and want automatic route discovery from container labels or annotations. Traefik’s label-based model shines in environments where services are deployed and torn down frequently — you do not want to edit a Caddyfile every time a container starts. If your infrastructure is heavily containerized and you want routes to appear and disappear automatically with containers, Traefik’s model is genuinely better suited to that workflow. Just budget time for understanding the static/dynamic config split, and use Traefik v3 which has substantially improved the configuration model over v2.
Complete Homelab Example
The following is a production-ready configuration for a typical homelab running five services behind Caddy, using a wildcard certificate via Cloudflare DNS-01.
Directory Structure
/home/user/caddy/
docker-compose.yml
Caddyfile
.env
.env File
|
|
docker-compose.yml
|
|
If you need to build your own image with plugins:
|
|
Full Caddyfile
|
|
Request Flow Diagram
Internet / LAN
|
| :80 (HTTP) :443 (HTTPS/TLS) :443/udp (HTTP/3)
|
+----+--------------------------------------------+
| Caddy (*.home.example.com) |
| |
| TLS termination (wildcard cert from LE/ZeroSSL) |
| HTTP/3 + HTTP/2 + HTTP/1.1 |
| |
| SNI/Host routing: |
| proxmox.* --> HTTPS 192.168.1.10:8006 |
| grafana.* --> HTTP grafana:3000 |
| photos.* --> HTTP immich_server:2283 |
| git.* --> HTTP forgejo:3000 |
| ha.* --> HTTP homeassistant:8123 |
| |
+--------------------------------------------------+
| | | | |
| | | | |
[Proxmox] [Grafana] [Immich] [Forgejo] [HA]
192.168.1.10 :3000 :2283 :3000 :8123
Deployment
|
|
On first start, Caddy will register an ACME account with Let’s Encrypt and ZeroSSL, then initiate a DNS-01 challenge for *.home.example.com. Watch for log lines confirming the TXT record was set and the certificate was issued — typically within 60-90 seconds for Cloudflare (DNS propagation is near-instant on Cloudflare’s anycast network).
Operational Notes and Common Pitfalls
Port 80 must be open for HTTP-01 challenges. If you are using DNS-01, port 80 can be closed. But if you have a mixture of public services (HTTP-01) and internal services (DNS-01), you need Caddy to listen on both. Caddy handles this automatically — it uses DNS-01 for the domains you configure it for and HTTP-01 for the rest.
The admin API binds to localhost only by default. Do not expose port 2019 to the network. If you need remote access to the admin API (for automation scripts running on other hosts), use an SSH tunnel or bind to a VPN interface, not a public IP.
Certificate storage is stateful. If you are running Caddy in Docker without a persistent volume for /data, Caddy will re-request certificates on every container restart. You will hit Let’s Encrypt rate limits quickly. Always mount a persistent volume for /data in Docker deployments.
Log file rotation. Caddy’s built-in log rolling is adequate for homelabs. For production, consider shipping logs to a remote aggregator (Loki, Elasticsearch) rather than relying on local rotation.
The Caddyfile is validated before reload. If your Caddyfile has a syntax error, caddy reload fails and the running config remains unchanged. This is a safety feature — a bad config never takes effect.
tls_insecure_skip_verify is fine for internal services with self-signed certs. You are not bypassing TLS — you are still encrypting the connection. You are just skipping certificate validation for the backend-to-Caddy hop, which is acceptable when the backend is on a trusted internal network. It is not acceptable for connections to external services.
Caddy’s automatic HTTPS is the kind of feature that sounds small until you have spent an afternoon debugging a Certbot renewal that failed because your DNS TTL was too high and the challenge expired. The operational simplicity compounds: one binary, one config file, zero cron jobs, zero certificate management, and a Caddyfile that a new team member can read and understand in ten minutes.
It is not the right tool for every situation. High-traffic static file serving, complex module requirements, and container-native dynamic routing all have better-matched tools. But for a homelab where you want HTTPS everywhere with minimal ongoing maintenance, Caddy is the best answer available.
Comments