<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: MaxTezza</title>
    <description>The latest articles on DEV Community by MaxTezza (@maxtezza).</description>
    <link>https://dev.to/maxtezza</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3604897%2F3ee627e6-7fa4-4834-ae46-29be3de65018.png</url>
      <title>DEV Community: MaxTezza</title>
      <link>https://dev.to/maxtezza</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maxtezza"/>
    <language>en</language>
    <item>
      <title>**_**_**_ **_Using Caddy as a Reverse Proxy with Automatic HTTP_**S_**_**_**</title>
      <dc:creator>MaxTezza</dc:creator>
      <pubDate>Tue, 11 Nov 2025 04:54:00 +0000</pubDate>
      <link>https://dev.to/maxtezza/-using-caddy-as-a-reverse-proxy-with-automatic-https-433b</link>
      <guid>https://dev.to/maxtezza/-using-caddy-as-a-reverse-proxy-with-automatic-https-433b</guid>
      <description>&lt;p&gt;description: Simplify your home lab or VPS setup with Caddy's zero-config automatic HTTPS and dead-simple reverse proxy configuration&lt;br&gt;
tags: homelab, devops, webdev, tutorial&lt;/p&gt;

&lt;p&gt;If you're running multiple services on your home lab or VPS, manually managing HTTPS certificates and reverse proxy configurations can become tedious fast. Caddy is a web server that automatically obtains and renews Let's Encrypt certificates with zero configuration, making it perfect for exposing your services securely.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Caddy Over nginx or Apache
&lt;/h2&gt;

&lt;p&gt;I switched to Caddy after spending years with nginx, and the difference is striking. With nginx, you need Certbot, cron jobs for renewal, and careful configuration management. With Caddy, you literally just specify the domain name and it handles everything. The configuration syntax is also dramatically simpler—what takes 50 lines in nginx takes 5 in Caddy.&lt;/p&gt;
&lt;h2&gt;
  
  
  Installing Caddy
&lt;/h2&gt;

&lt;p&gt;On Ubuntu/Debian:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; debian-keyring debian-archive-keyring apt-transport-https
curl &lt;span class="nt"&gt;-1sLf&lt;/span&gt; &lt;span class="s1"&gt;'https://dl.cloudsmith.io/public/caddy/stable/gpg.key'&lt;/span&gt; | &lt;span class="nb"&gt;sudo &lt;/span&gt;gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl &lt;span class="nt"&gt;-1sLf&lt;/span&gt; &lt;span class="s1"&gt;'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/caddy-stable.list
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;caddy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caddy will automatically start as a systemd service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic Reverse Proxy Configuration
&lt;/h2&gt;

&lt;p&gt;Create or edit &lt;code&gt;/etc/caddy/Caddyfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Simple reverse proxy to a local service
myapp.example.com {
    reverse_proxy localhost:3000
}

# Proxy with custom headers
api.example.com {
    reverse_proxy localhost:8080 {
        header_up Host {host}
        header_up X-Real-IP {remote}
        header_up X-Forwarded-For {remote}
        header_up X-Forwarded-Proto {scheme}
    }
}

# Multiple services on different paths
example.com {
    reverse_proxy /api/* localhost:8080
    reverse_proxy /web/* localhost:3000
    reverse_proxy /* localhost:8081
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No SSL configuration needed. Caddy automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Obtains Let's Encrypt certificates&lt;/li&gt;
&lt;li&gt;Redirects HTTP to HTTPS&lt;/li&gt;
&lt;li&gt;Renews certificates before expiry&lt;/li&gt;
&lt;li&gt;Serves HTTPS on port 443&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real-World Example: Home Lab Setup
&lt;/h2&gt;

&lt;p&gt;Here's my actual Caddyfile for my home lab services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Jellyfin media server
jellyfin.mydomain.com {
    reverse_proxy localhost:8096
}

# Home Assistant
home.mydomain.com {
    reverse_proxy localhost:8123
}

# Grafana monitoring
grafana.mydomain.com {
    reverse_proxy localhost:3001
}

# Portainer container management
portainer.mydomain.com {
    reverse_proxy localhost:9000 {
        transport http {
            tls_insecure_skip_verify
        }
    }
}

# Catch-all for root domain
mydomain.com {
    respond "Home Lab Services" 200
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After editing the Caddyfile, reload Caddy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload caddy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch the logs to see certificates being obtained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; caddy &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see Caddy automatically reaching out to Let's Encrypt and obtaining certificates for each domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced Configuration: Docker Container Backend
&lt;/h2&gt;

&lt;p&gt;If your services run in Docker, you can proxy to them by container name when Caddy is on the same Docker network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;caddy:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./Caddyfile:/etc/caddy/Caddyfile&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_data:/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_config:/config&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;webproxy&lt;/span&gt;

  &lt;span class="na"&gt;myapp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp:latest&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;webproxy&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;webproxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your Caddyfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;myapp.example.com {
    reverse_proxy myapp:3000
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Handling Websockets
&lt;/h2&gt;

&lt;p&gt;Many modern apps use WebSockets. Caddy handles them automatically with no extra configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chat.example.com {
    reverse_proxy localhost:3000
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Unlike nginx where you need specific upgrade headers, Caddy detects and handles WebSocket connections automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Load Balancing Multiple Backends
&lt;/h2&gt;

&lt;p&gt;If you're running multiple instances of a service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api.example.com {
    reverse_proxy localhost:8001 localhost:8002 localhost:8003 {
        lb_policy round_robin
        health_uri /health
        health_interval 10s
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caddy will distribute requests across backends and automatically remove unhealthy ones from rotation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Headers
&lt;/h2&gt;

&lt;p&gt;Add security headers easily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com {
    header {
        Strict-Transport-Security "max-age=31536000;"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "no-referrer-when-downgrade"
    }
    reverse_proxy localhost:3000
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Authentication Layer
&lt;/h2&gt;

&lt;p&gt;Add basic auth in front of any service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;admin.example.com {
    basicauth {
        admin $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4wHaNzfF29C
    }
    reverse_proxy localhost:9000
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate the hash with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;caddy hash-password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common Gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Port 80/443 Already in Use&lt;/strong&gt;: If you have Apache or nginx running, stop them first. Only one process can listen on these ports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DNS Not Pointing to Server&lt;/strong&gt;: Caddy needs to complete the ACME challenge. Your domain must resolve to your server's public IP before Caddy can obtain certificates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firewall Blocking&lt;/strong&gt;: Ensure ports 80 and 443 are open in your firewall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;IPv6 Issues&lt;/strong&gt;: If you see IPv6 errors but only use IPv4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    auto_https disable_redirects
}

myapp.example.com {
    bind 0.0.0.0
    reverse_proxy localhost:3000
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;p&gt;Caddy is written in Go and is very efficient, but for high-traffic scenarios, tune these settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    servers {
        timeouts {
            read_body 10s
            read_header 5s
            write 30s
            idle 120s
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What This Replaced
&lt;/h2&gt;

&lt;p&gt;Before Caddy, my nginx setup required:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initial nginx configuration (30+ lines per service)&lt;/li&gt;
&lt;li&gt;Certbot installation and configuration&lt;/li&gt;
&lt;li&gt;Systemd timer or cron job for renewal&lt;/li&gt;
&lt;li&gt;Manual renewal testing&lt;/li&gt;
&lt;li&gt;Separate configuration for HTTP-&amp;gt;HTTPS redirects&lt;/li&gt;
&lt;li&gt;Regular maintenance to ensure renewals work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With Caddy, I have a 3-line config per service and zero maintenance. I've been running it for two years and have never thought about certificates once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Start small. Pick one service you're currently accessing via HTTP or with a self-signed certificate. Add a 3-line Caddy config for it, point the DNS, and watch it work. Once you see how trivial it is, you'll migrate everything else quickly.&lt;/p&gt;

&lt;p&gt;The combination of automatic HTTPS and simple configuration makes Caddy the obvious choice for home labs and small-scale deployments. It's one of those tools that makes you wonder how you ever did it the old way.&lt;/p&gt;

&lt;p&gt;What's your go-to reverse proxy? Have you tried Caddy? Drop a comment below!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
