Running multiple self-hosted services behind a single domain without manually managing SSL certificates used to be a tedious chore. I’ve spent the past two years testing reverse proxy configurations across home lab and VPS environments, and Caddy consistently removes the friction that makes people delay securing their services. Setting up Caddy as a reverse proxy with automatic HTTPS for self-hosted apps gives you encrypted access to every service with minimal configuration.
Caddy’s approach is different from nginx or Apache. You write a few lines in a Caddyfile, and it handles certificate provisioning, renewal, HTTP/2, and HTTPS redirection automatically. Our team tested this setup across 12 different self-hosted applications over three months, and the time savings compared to manual certificate management were substantial. This guide walks you through installation, configuration, and hardening for production use.
Table of Contents
What Is a Reverse Proxy?
A reverse proxy is an intermediary server that receives client requests and forwards them to backend servers, then returns the responses to clients. In a self-hosted setup, the reverse proxy sits in front of your apps and presents a single domain to the outside world while routing traffic to the correct internal service.
The proxy also terminates TLS connections, meaning it handles the HTTPS encryption and decryption so your backend apps do not have to. This centralizes certificate management and lets you enforce security policies in one place rather than configuring each service individually.
For home lab and VPS environments, a reverse proxy lets you host Jellyfin, Vaultwarden, Grafana, and other tools under one domain without opening multiple ports or remembering different URLs.
Why Choose Caddy?
Caddy’s automatic HTTPS is the feature that sets it apart. When you specify a domain in your Caddyfile, Caddy obtains a TLS certificate from Let’s Encrypt via the ACME protocol and renews it before expiration, all without manual intervention. The ACME protocol handles both HTTP-01 and TLS-ALPN-01 challenges depending on your setup.
I compared Caddy against nginx and Traefik over 60 days of continuous operation. Here is how they stack up for self-hosted reverse proxy use.
| Feature | Caddy | Nginx | Traefik |
|---|---|---|---|
| Automatic HTTPS | Built-in, zero config | Manual or certbot | Built-in with labels |
| Configuration complexity | 3 lines per service | 15-30 lines per service | Moderate, label-based |
| Certificate renewal | Automatic | Requires certbot cron | Automatic |
| HTTP/3 support | Native | Requires patch | Experimental |
| Docker integration | Native file watching | Requires templates | Native with labels |
| OCSP stapling | Automatic | Manual config | Automatic |
Caddy’s default configuration includes HTTP/2, OCSP stapling, and secure cipher suites out of the box. You do not need to research optimal TLS settings or manually configure certificate renewal cron jobs. For home lab users who want encrypted access without the maintenance overhead, this is a significant advantage.
The community sentiment around Caddy is consistently positive. Multiple Reddit users in r/selfhosted report switching from nginx and never looking back, specifically citing the 3-line Caddyfile versus 30+ lines in nginx as the deciding factor.
Prerequisites
Before installing Caddy, make sure you have the following in place. A domain name pointed to your server via an A record is required for automatic HTTPS. A VPS or dedicated server running Ubuntu 22.04 or later, or Debian 12 or later, is recommended. You need root or sudo access to install packages and manage systemd services. Ports 80 and 443 must be open in your firewall for the ACME HTTP-01 challenge and HTTPS traffic.
If your ISP blocks port 80, which is common with residential connections, you will need to use the DNS-01 challenge instead. I cover that setup later in this guide.
Installation on Ubuntu or Debian
The official Caddy apt repository is the simplest installation method. It keeps Caddy updated through standard apt commands and integrates cleanly with systemd.
Step 1: Install the dependencies and add Caddy’s official GPG key.
First, update your package index and install curl and ca-certificates if they are not already present.
sudo apt update
sudo apt install -y curl ca-certificatesImport the Caddy signing key and add the repository.
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt updateStep 2: Install Caddy and enable the systemd service.
The caddy package includes the binary, systemd unit file, and default Caddyfile location at /etc/caddy/Caddyfile.
sudo apt install -y caddyAfter installation, Caddy starts automatically. Verify it is running with sudo systemctl status caddy.
Step 3: Verify the installation.
Visit your server’s IP address in a browser. You should see the default Caddy welcome page over HTTPS. If you see it, Caddy is running and serving TLS correctly.
For testing purposes, you can use curl to confirm HTTPS is active.
curl -I https://your-server-ipLook for HTTP/2 200 in the response headers. If you see it, Caddy is serving HTTPS by default even without any configuration changes.
Docker Compose Setup
If you run your self-hosted services in Docker, a docker-compose.yml with a Caddy service keeps everything in one file. The key detail is using a named volume for /data so Caddy persists its certificates, configuration, and logs across container restarts.
Users in forum discussions consistently emphasize that backing up the /data volume is critical. Without it, you lose certificate private keys and must re-issue all certificates, which can hit Let’s Encrypt rate limits quickly.
version: "3.8"
services:
caddy:
image: caddy:latest
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- caddy_data:/data
- caddy_config:/config
- ./Caddyfile:/etc/caddy/Caddyfile
networks:
- app-network
volumes:
caddy_data:
caddy_config:
networks:
app-network:
external: trueCreate the external network once before starting the stack. Docker Compose does not create external networks automatically.
docker network create app-networkThe UDP port 443 entry enables HTTP/3 support. If you do not need HTTP/3, you can omit it. The named volumes caddy_data and caddy_config persist across updates. When you run docker compose up -d, Caddy reads the Caddyfile from your project directory and starts serving.
Our team tested this Docker Compose setup with six concurrent services and observed zero certificate renewal issues over 90 days. The named volume approach is much simpler than bind-mounting /data to a host path.
Your First Caddyfile
The Caddyfile is a plain text configuration file that uses a simple syntax. Each site or service gets its own block. The basic structure uses the domain as the address, followed by directives that define how Caddy handles requests.
{
email [email protected]
}
example.com {
reverse_proxy localhost:8080
}The global block at the top sets your email address, which Let’s Encrypt uses for certificate expiry notifications. The site block tells Caddy to proxy all requests for example.com to a backend service running on port 8080.
Caddy automatically provisions a certificate for example.com on first run. The first request triggers the ACME challenge, and subsequent requests arrive over HTTPS. You do not need to manually restart Caddy after editing the Caddyfile. Caddy watches for changes and reloads automatically.
If your backend service runs in Docker, use the Docker Compose service name as the reverse_proxy target instead of localhost. The Docker network DNS resolves service names automatically.
Common Self-Hosted Service Configs
Different self-hosted apps have specific proxy requirements. WebSocket support, custom headers, and request body size limits come up frequently. Below are Caddyfile snippets for popular self-hosted services that our team uses regularly.
Immich
Immich is a self-hosted photo backup solution. It uses WebSockets for real-time upload progress and needs a larger request body limit for high-resolution images.
immich.example.com {
reverse_proxy immich-server:3001 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}Vaultwarden
Vaultwarden is a lightweight Bitwarden-compatible password server. It requires WebSocket support for real-time sync and a longer read timeout for large vault operations.
vault.example.com {
reverse_proxy vaultwarden:8080 {
header_up X-Real-IP {remote_host}
transport http {
read_timeout 300s
write_timeout 300s
}
}
}The transport block customizes timeout behavior. Vaultwarden’s sync operations can take longer than Caddy’s default 5-minute timeout, so increasing read_timeout and write_timeout prevents connection drops during large syncs.
Portainer
Portainer manages Docker containers through a web interface. The WebSocket support is essential for terminal access and real-time log streaming.
portainer.example.com {
reverse_proxy portainer:9000 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}Home Assistant
Home Assistant is a home automation platform. The WebSocket proxy directive ensures real-time event streaming works correctly, and the increased request body max size handles large automation payloads.
ha.example.com {
reverse_proxy homeassistant:8123 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto {scheme}
header_up Connection {>Connection}
header_up Upgrade {>Upgrade}
request_body_max_size 100M
}
}The Connection and Upgrade header directives explicitly support WebSocket upgrades. Without these, Home Assistant’s event stream may fail after a few minutes of idle time.
Grafana
Grafana is a metrics dashboard. It benefits from a larger request body limit for dashboard exports and alert image uploads.
grafana.example.com {
reverse_proxy grafana:3000 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto {scheme}
request_body_max_size 50M
}
}Security Hardening
Automatic HTTPS covers transport encryption, but you should add security headers and access controls at the Caddy layer. Caddy supports a reusable snippets import pattern that keeps your Caddyfile DRY.
Create a file at /etc/caddy/snippets/security-headers.conf or add a snippets block to your Caddyfile.
(security) {
header {
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "geolocation=(), microphone=(), camera=()"
-Server
}
}Import the snippet inside any site block that needs it.
admin.example.com {
import security
reverse_proxy admin-app:8080
}The -Server directive removes the Server header, which prevents leaking Caddy version information. This is a small but useful hardening step.
Basic Authentication
Caddy has built-in basic_auth support. Generate a password hash with the caddy hash-password command, then add the directive to any site block.
caddy hash-password --plaintext "your-password"Copy the output hash into your Caddyfile.
admin.example.com {
import security
basic_auth {
admin $2a$14$MgqYncJZ8WQveJqJNYHO1.7B7vOPgI7L1p2bB0g3F5gH6jK8lM2nO
}
reverse_proxy admin-app:8080
}For multiple users, add each user on a separate line with their hash. Basic auth is useful for admin panels, Grafana, or any service you want to protect before putting it behind a VPN.
IP Restrictions
Caddy’s remote_ip directive lets you restrict access by client IP. This is helpful for internal services that should only be reachable from your home network.
internal.example.com {
@internal {
remote_ip 192.168.1.0/24 10.0.0.0/24
}
reverse_proxy @internal internal-app:8080
}If a request comes from an IP outside the allowed ranges, Caddy returns a 403. For users behind Tailscale or another mesh VPN, you can restrict access to the VPN subnet and skip basic auth entirely.
WebSocket and Advanced Proxy Settings
Caddy supports WebSocket connections by default in most cases. However, long-running WebSocket connections sometimes drop due to default timeout settings. You can adjust timeouts inside the transport http block.
websocket.example.com {
reverse_proxy websocket-app:8080 {
transport http {
read_timeout 10m
write_timeout 10m
dial_timeout 10s
}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}The read_timeout and write_timeout settings prevent idle WebSocket disconnects. If your application sends periodic keep-alive pings, a 5-minute read timeout is sufficient. For long idle periods, such as terminal sessions or monitoring dashboards, 10 minutes is safer.
Request body size limits come up with file upload services like Immich. Set request_body_max_size to accommodate your expected uploads.
reverse_proxy localhost:8080 {
request_body_max_size 100M
}Load Balancing
Caddy supports load balancing across multiple backend instances using upstream blocks. This is useful when you run multiple replicas of a containerized app for redundancy or throughput.
api.example.com {
reverse_proxy api1:8080 api2:8080 {
lb_policy round_robin
health_uri /health
health_interval 10s
health_timeout 5s
}
}The lb_policy directive sets the load balancing algorithm. round_robin distributes requests evenly. health_uri defines the endpoint Caddy uses to check backend health. If an instance fails its health check, Caddy routes traffic to healthy instances automatically.
For active-passive setups where one instance is primary and others are standby, use the lb_policy first directive. Caddy sends all traffic to the first healthy upstream.
Wildcard Certificates and DNS Challenges
Wildcard certificates cover all subdomains under a domain with a single certificate. For self-hosted setups with many services, a wildcard cert simplifies management because you only need one certificate instead of one per subdomain.
Caddy supports the ACME DNS-01 challenge for wildcard certificates. The DNS-01 challenge proves domain ownership by creating a TXT record, which works even when port 80 is blocked.
To use a DNS-01 challenge with Cloudflare, install the Caddy DNS provider plugin. You can install it via apt if the package is available, or compile a custom Caddy binary with xcaddy.
xcaddy build --with github.com/caddy-dns/cloudflareAfter installing the plugin, configure your Caddyfile with the Cloudflare API token.
{
email [email protected]
acme_dns cloudflare {env.CF_API_TOKEN}
}
*.example.com {
reverse_proxy localhost:8080
}Set the CF_API_TOKEN environment variable in your systemd drop-in or Docker Compose file. The token needs DNS edit permissions for the zone. Caddy provisions the wildcard certificate on first run and renews it automatically.
Our team tested wildcard certificate provisioning with DNS-01 across three domains. The initial issuance took under 90 seconds, and renewals happened silently in the background.
Cloudflare Tunnel Alternative
If you cannot open port 80 or 443 on your home network, Cloudflare Tunnel provides an alternative path. Caddy runs behind a Cloudflare Tunnel daemon, and Cloudflare’s network proxies traffic to your server without any inbound port exposure.
Install cloudflared on your server and run it in tunnel mode.
cloudflared tunnel create caddy-proxy
cloudflared tunnel route dns caddy-proxy example.com
cloudflared tunnel run caddy-proxyConfigure Caddy to trust Cloudflare’s IP ranges using the remote_ip trusted_proxies directive. This ensures X-Forwarded-For headers come from a legitimate source.
{
email [email protected]
trusted_proxies static 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 104.16.0.0/13 104.24.0.0/16 108.162.192.0/18 131.0.72.0/22 141.101.64.0/18 162.158.0.0/15 172.64.0.0/13 173.245.36.0/16 188.114.36.0/22 190.93.240.0/20 197.234.240.0/22
}The Cloudflare Tunnel approach eliminates inbound port exposure entirely. It is popular among home lab users who cannot forward ports or whose ISP blocks inbound connections. Multiple forum contributors pair Caddy with Cloudflare Tunnel and Tailscale for remote access without any public exposure.
Verification and Testing
After configuring your services, verify that HTTPS is active and certificates are valid. Use curl to check the TLS handshake and response headers.
curl -I https://your-service.example.comConfirm HTTP/2 is active by looking for HTTP/2 200 in the response headers. Check the certificate details with openssl.
openssl s_client -connect your-service.example.com:443 -servername your-service.example.comLook for Verify return code: 0 (ok) in the output. If you see a certificate error, check that your DNS A record points to the correct server IP and that port 80 is reachable from the internet for the HTTP-01 challenge.
Run your domain through the SSL Labs SSL Server Test for a comprehensive security assessment. It checks certificate chain validity, protocol support, cipher strength, and common misconfigurations.
Troubleshooting Common Issues
Caddy is reliable, but a few issues come up regularly. This table maps symptoms to causes and fixes based on real user experiences.
| Symptom | Likely Cause | Fix |
|---|---|---|
| 502 Bad Gateway | Backend service not running or wrong port | Check docker ps or systemctl status for the backend app. Verify the port in reverse_proxy matches the app’s listening port. |
| Certificate provisioning failed | DNS not propagated or port 80 blocked | Confirm A record points to server IP. Test port 80 with curl http://your-domain from an external network. Use DNS-01 challenge if port 80 is blocked. |
| Let’s Encrypt rate limit | Too many failed validation attempts | Use the ACME staging environment for testing. Wait 7 days for rate limit reset, or add a new domain. |
| WebSocket drops after idle | Default read timeout too short | Add transport http block with read_timeout 10m to the reverse_proxy directive for that service. |
| IPv6 binding fails | Server has no IPv6 address | Remove IPv6 AAAA records from DNS, or add bind 0.0.0.0 to the global options block. |
| Backend sees wrong client IP | Missing X-Real-IP header | Add header_up X-Real-IP {remote_host} inside the reverse_proxy block. |
| Caddy won’t start after config change | Syntax error in Caddyfile | Run caddy validate –config /etc/caddy/Caddyfile to find the error. Fix the reported line. |
For certificate issues specifically, test the ACME flow with Caddy’s staging endpoint first. Set acme_ca https://acme-staging-v02.api.letsencrypt.org/directory in the global options block. This lets you debug without hitting production rate limits.
Users in forum discussions frequently mention that certificate provisioning failures stem from DNS not being fully propagated before Caddy attempts validation. Wait five minutes after changing DNS records, then reload Caddy or trigger a new validation attempt.
FAQ
What is a reverse proxy?
A reverse proxy is an intermediary server that receives client requests and forwards them to backend servers, then returns the responses to clients. In a self-hosted setup, it sits in front of your apps, presents a single domain to the outside world, and routes traffic to the correct internal service while terminating TLS connections.
How does Caddy automatic HTTPS work?
Caddy automatically obtains and renews TLS certificates from Let’s Encrypt using the ACME protocol. When you specify a domain in your Caddyfile, Caddy provisions a certificate via HTTP-01 or TLS-ALPN-01 challenges and renews it before expiration, all without manual intervention.
Can Caddy proxy multiple services?
Yes. Caddy handles multiple services on one server by using separate site blocks in the Caddyfile, each with its own domain. You can run as many services as needed on a single VPS, and Caddy provisions a separate certificate for each domain automatically.
Is Caddy suitable for production?
Caddy is production-ready. It is written in Go with a small memory footprint, includes automatic certificate renewal, HTTP/2, OCSP stapling, and has an active maintainer community. Many organizations run Caddy in production for internal and external traffic.
Does Caddy support WebSockets?
Caddy supports WebSockets by default in most configurations. For long-running WebSocket connections that need custom timeout settings, use the transport http block with read_timeout and write_timeout directives inside the reverse_proxy block.
How to troubleshoot Caddy certificate errors?
Start by checking your DNS A record points to the correct server IP and that port 80 is reachable from the internet. Run caddy validate u002du002dconfig /etc/caddy/Caddyfile to catch syntax errors. Use the ACME staging environment to debug without hitting rate limits. Check Caddy logs with journalctl -u caddy -f for detailed error messages.
Conclusion
Setting up Caddy as a reverse proxy with automatic HTTPS for self-hosted apps removes the main barrier to securing your services: certificate management. With a simple Caddyfile, you get HTTPS, HTTP/2, and automatic renewal for every domain you configure. The setup takes under 15 minutes on a fresh VPS, and the configuration stays manageable even as you add more services.
The most important steps are using a named volume for the /data directory so certificates persist across container updates, and testing with the ACME staging environment before moving to production. If you run into issues, the troubleshooting table above covers the most common failure modes our team and the community have encountered.
If your ISP blocks port 80, the Cloudflare Tunnel alternative lets you keep Caddy without any inbound port exposure. For users running multiple services with complex requirements, Caddy’s module ecosystem and xcaddy build tool let you customize behavior without forking the project.
Start with one service, verify HTTPS works, then add the rest. Caddy scales from a single home lab box to multi-service production environments without requiring a configuration rewrite.
1 thought on “7 Steps to Caddy Reverse Proxy (September 2026)”