Caddy as reverse proxy and webserver on one machine

1. Output of caddy version:

v2.6.2 h1:wKoFIxpmOJLGl3QXoo6PNbYvGW4xLEgo32GPBEjWL8o=

2. How I run Caddy:

I want to run multiple caddy webservers on my machine and put a caddy reverse proxy in front of them. So I can point different DNS names to my machine and host multiple websites. However, caddy is showing only blank websites.

a. System environment:

Ubuntu 22.04.1 LTS, no Docker

b. Command:

Paste command here.

c. Service/unit/compose file:

Paste full file contents here.
Make sure backticks stay on their own lines,
and the post looks nice in the preview pane. -->

d. My complete Caddy config:

die-lordis.de {
   reverse_proxy http://localhost:2020
}

http://localhost:2020 {
   root * /var/www/html
   file_server
}

3. The problem I’m having:

When I only have on webserver configured with the config below, the sites loads fine.

die-lordis.de
root * /var/www/html
file_server

When I try to put a reverse proxy in front of per the config in d., I only get a blank site.

4. Error messages and/or full log output:

No error message in the log

Paste logs/commands/output here.
USE THE PREVIEW PANE TO MAKE SURE IT LOOKS NICELY FORMATTED.

5. What I already tried:

I tested all sorts of combination with and without http, with 127.0.0.1 instead of localhost, but no luck.

6. Links to relevant resources:

Hi,

can you elaborate on what you mean by


Also, your page (https://die-lordis.de) is serving html just fine for me :innocent:
Did you manage to fix it in the meantime, or did I misunderstand your problem? :slight_smile:

Hi,

The site is running at the moment with the below config:

die-lordis.de
root * /var/www/html
file_server

When I put a reverse proxy in front of it like below, it does not work anymore but only shows a blank page.

die-lordis.de {
   reverse_proxy http://localhost:2020
}

http://localhost:2020 {
   root * /var/www/html
   file_server
}

Regards,
Dominik

Ah, I see.

Okay, two things:
First, Caddy’s default response - if nothing else matches - is a blank http/200.
There has been a new addition to the wiki lately about that, see Why Caddy emits empty 200 OK responses by default if you want to know why.
The scope of possible sites to match is per port, essentially.

Your die-lordis.de is on :443, because of Automatic HTTPS — Caddy Documentation.
http://localhost:2020 on the other hand, is on port :2020, like you specified.
Now, if you connect via http://localhost:2020, then your page will show.
That’s because Caddy matches your site blocks based on the Host header.

Essentially, your client (in your case, most likely your web browser) sends the Host: localhost:2020 to Caddy and Caddy is like “yee, I understand! You want me to serve that http://localhost:2020 you specified in the Caddyfile to you!”.

But if you would go ahead and send a custom Host header, like Host: example.com, then Caddy would be like “eeeehh, I know you are trying to connect to me and all, cool, I’ll permit that, but I don’t know that page! Here, see my default - the blank http/200 - instead”.

Take the following example Caddyfile:

http://localhost:2020 {
	respond "example response"
}

and curl:

❯ curl http://localhost:2020 -iv
*   Trying 127.0.0.1:2020...
* Connected to localhost (127.0.0.1) port 2020 (#0)
> GET / HTTP/1.1
> Host: localhost:2020
> User-Agent: curl/7.86.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Content-Type: text/plain; charset=utf-8
Content-Type: text/plain; charset=utf-8
< Server: Caddy
Server: Caddy
< Date: Mon, 21 Nov 2022 08:06:32 GMT
Date: Mon, 21 Nov 2022 08:06:32 GMT
< Content-Length: 16
Content-Length: 16

< 
* Connection #0 to host localhost left intact
example response%                                                                                                                                                                                                          

You can read the > lines as curl sending something to Caddy, and the < ones as curl receiving something.
Note the > Host: localhost:2020.

The same config, this time with a custom Host: header:

❯ curl http://localhost:2020 -iv --header "Host: example.com"
*   Trying 127.0.0.1:2020...
* Connected to localhost (127.0.0.1) port 2020 (#0)
> GET / HTTP/1.1
> Host: example.com
> User-Agent: curl/7.86.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Server: Caddy
Server: Caddy
< Date: Mon, 21 Nov 2022 08:09:39 GMT
Date: Mon, 21 Nov 2022 08:09:39 GMT
< Content-Length: 0
Content-Length: 0

< 
* Connection #0 to host localhost left intact

No “example response” as response and a Content-Length of 0 bytes.
That’s the default response, if nothing matches, as explained in the wiki linked above.

Second, the reverse_proxy (Caddyfile directive) — Caddy Documentation passes multiple headers to the upstream (in your case http://localhost:2020) by default.

Quote:

Defaults:link:

By default, Caddy passes thru incoming headers—including Host—to the backend without modifications, with three exceptions:

There is that Host header again!

So essentially, what’s happening is:

  1. Client sends Host: die-lordis.de to :443
  2. Caddy is like “yee, alright, great, I’ll pass that to http://localhost:2020 as instructed”
  3. Caddy requests http://localhost:2020 with the original (unchanged) Host: die-lordis.de
  4. Caddy has no die-lordis.de configured on port :2020 and default to the blank http/200

There are multiple ways to fix that:

Option 1: Override the Host header within the reverse_proxy directive:

reverse_proxy http://localhost:2020 {
	header_up Host {upstream_hostport}
}

or

reverse_proxy http://localhost:2020 {
	header_up Host localhost:2020
}

Both achieve literally the same. Though the first one leverages Caddyfile Concepts — Caddy Documentation, so you have a single source of truth :slight_smile:

Option 2: Use a catch-all for port :2020

You could also just make port :2020 not care about the Host header. A catch-all essentially.
For that, you would just drop the localhost in your config like so:

http://:2020 {
   root * /var/www/html
   file_server
}

Option 3: Specify your domain within the same block

The Caddyfile allows you to specify multiple domains/hostnames/IPs in a single block.
You can either separate them by a comma (,) or a whitespace ( ).

die-lordis.de http://localhost:2020 {
   root * /var/www/html
   file_server
}

Valid site addresses are mentioned at Caddyfile Concepts — Caddy Documentation in case you want to see some more :sweat_smile:


One more thing:
If you had done something like

http://example.localhost {
	reverse_proxy http://localhost
}

http://localhost {
	respond "example response"
}

and then tried to connect to http://example.localhost, you would have ended up in an infinite loop, because Caddy keeping the Host header unchanged would never reach http://localhost but instead http://example.localhost, which would then in turn connect to http://example.localhost over and over again.


Hope that explains why you are experiencing the issue you are having and some workarounds :innocent:

3 Likes

Thanks for the detailed explanation. I went with option 2.

Thanks
Dominik