Caddy as a webserver behind caddy reverse proxy

1. The problem I’m having:

I’m trying to have a caddy webserver hosting my static site on a separate server from my reverse proxy. The reverse proxy server proxiesa lot of other services, which are working. I can’t seem to get the caddy proxy to server working though.

I haven’t been able to access the static site through http from a different machine. I also haven’t been able to set up the reverse proxy and I don’t get what’s going wrong.

On my testing machine I could access the server using the same http://localhost/ config as I’m using now, but I can’t get it to work. I’ve also tried using :80, http://docs.domain.tld and http://:2020. All of these either broke or didn’t work. I’ve also tried disabling tls globally and in my config itself.

When running the http://docs.domain.tld, I get to many redirects. When running :80, which doesn’t resolve with curl or the browser. When running http://:2020, I also can’t resolve.

3. Caddy version:

2.10.0

4. How I installed and ran Caddy:

a. System environment:

I’m running both caddy instances from docker with the recommended compose files.

b. Command:

docker compose up -d

c. Service/unit/compose file:

services:
  caddy:
    image: caddy:2.10.0
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
      - 443:443/udp
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./caddy/data:/data
      - ./caddy/config:/config
      - ./docs/site:/srv

d. My complete Caddy config:

Reverse proxy:

docs.domain.tld {
    reverse_proxy 192.168.5.164
}

Webserver:

http://localhost {
    root * /srv
    file_server
}

5. Links to relevant resources:

The solutions I’ve tested came from multiple posts, this is the list:

I’ve been messing around a bit more and I’ve managed to make the website accessible from it’s IP. I still can’t reverse proxy it, though.

I’ve updated my Caddyfile for the webserver to this:

http://192.168.5.164 {
    root * /srv
    file_server
}

This makes it accessible through a web browser using the IP address, but the reverse proxy still isn’t working, it just sends a 308 permanent redirect:

* Connected to docs.domain.tld (IP) port 80 (#0)
> GET / HTTP/1.1
> Host: docs.domain.tld
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 308 Permanent Redirect
< Connection: close
< Location: https://docs.domain.tld/
< Server: Caddy
< Date: Tue, 03 Jun 2025 10:10:24 GMT
< Content-Length: 0
< 
* Closing connection 0

That’s not an error. It’s expected behavior. See the Location header. Caddy is telling the client to use the HTTPS endpoint. Add -L to your curl command.

Doing that gives me the following output, which doesn’t tell me much…

* Connected to docs.domain.tld (IP) port 80 (#0)
> GET / HTTP/1.1
> Host: docs.domain.tld
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 308 Permanent Redirect
< Connection: close
< Location: https://docs.domain.tld/
< Server: Caddy
< Date: Tue, 03 Jun 2025 10:50:35 GMT
< Content-Length: 0
< 
* Closing connection 0
* Clear auth, redirects to port from 80 to 443
* Issue another request to this URL: 'https://docs.domain.tld/'
*   Trying IP:443...
* Connected to docs.domain.tld (IP) port 443 (#1)
* ALPN, offering h2
* ALPN, offering http/1.1
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: /etc/ssl/certs
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS header, Finished (20):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.2 (OUT), TLS header, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=docs.domain.tld
*  start date: May 29 18:03:53 2025 GMT
*  expire date: Aug 27 18:03:52 2025 GMT
*  subjectAltName: host "docs.domain.tld" matched cert's "docs.domain.tld"
*  issuer: C=US; O=Let's Encrypt; CN=E5
*  SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* Using Stream ID: 1 (easy handle 0x5671260939f0)
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> GET / HTTP/2
> Host: docs.domain.tld
> user-agent: curl/7.81.0
> accept: */*
> 
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
< HTTP/2 200 
< alt-svc: h3=":443"; ma=2592000
< date: Tue, 03 Jun 2025 10:50:35 GMT
< server: Caddy
< via: 1.1 Caddy
< content-length: 0
< 
* Connection #1 to host docs.domain.tld left intact

That’s a successful response. Your file server is returning empty response. Do you have files in the root directory?

Yes, when I ping the 192.168.5.164 server, this is the response (I omitted most of the html, to shorten it a bit).

*   Trying 192.168.5.164:80...
* Connected to 192.168.5.164 (192.168.5.164) port 80
> GET / HTTP/1.1
> Host: 192.168.5.164
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Content-Length: 18442
< Content-Type: text/html; charset=utf-8
< Etag: "dactr4c9o36be8a"
< Last-Modified: Tue, 03 Jun 2025 10:26:31 GMT
< Server: Caddy
< Vary: Accept-Encoding
< Date: Tue, 03 Jun 2025 12:04:46 GMT
< 

<!doctype html>
<html lang="en" class="no-js">
  <head>
...    
  </head>

    <body dir="ltr" data-md-color-scheme="default" data-md-color-primary="teal" data-md-color-accent="cyan">

...

  </body>
* Connection #0 to host 192.168.5.164 left intact
</html

This is the response from the webserver, which is proxied through the other server. That is using the reverse_proxy 192.168.5.164 directive to proxy the web server that pulls produces above response.

When using curl on the reverse proxy server, it produces the empty succes response.

I’ve been trying to fix this in the past hour and after googling and vibe coding my way through a lot of crap, I got the reverse proxy working by using the header_up directive in the proxy. I’m not sure if this is the best way of doing this, but it is working now. The reverse proxy looks like this now:

docs.domain.tld {
    reverse_proxy 192.168.5.164 {
        header_up Host {upstream_hostport}
    }
}

I’ve changed the internal IP in previous posts, as I was obfuscating it unnecessarily.

1 Like

Oh, I see it now. You told Caddy to only expect the domain name localhost. It won’t answer other domains with the files because it wasn’t told to serve files on those domains.

1 Like