How To Setup Caddy As A Reverse Proxy

Feb 4, 2025 · 15 mins read
How To Setup Caddy As A Reverse Proxy

In the video below, we show you how to install and configure caddy as a reverse proxy


Containers are an extremely useful and efficient way of deploying applications

But you could end up with a quite a lot of them, each managed through their own web interface

In some cases the web session will have no encryption, otherwise a self-signed certificate will be provided

And this is where Caddy can help

It has several purposes but you can use it as a reverse proxy for your containers

With an encrypted session to Caddy itself, your web browser only needs to trust one certificate

So in this video we’ll go over how to install Caddy as a container and how to configure it as a reverse proxy for other containers

Useful links:
https://caddyserver.com/docs/running#docker%20compose
https://caddyserver.com/docs/quick-starts/reverse-proxy
https://caddyserver.com/docs/caddyfile
https://caddyserver.com/docs/caddyfile/directives/reverse_proxy
https://caddyserver.com/docs/caddyfile/directives/tls
https://caddyserver.com/docs/automatic-https#local-https

Assumptions:
Now because this video is about setting up Caddy as a reverse proxy, I’m going to assume you already have Docker installed, or you at least know how to install it

If not then I do have another video available that shows you how to install Docker in a VM running in Proxmox VE for instance

Overview:
Now even if you’re only interested in how to use Caddy, I do feel it’s very important to provide this overview of how reverse proxies work because they pose a security risk

targets

In a simple world, when you’re connecting to a web server, your computer will connect directly to that server

Assuming there’s encryption being used, you’ll have what is known as end-to-end encryption

In other words, only your computer and the server sees the data being exchanged

Everything in between just sees encrypted information

But not all containers support encryption, and those that do will require certificate management, because it’s not advisable to use self-signed certificates

Why?

Because it puts you at greater risk of a man-in-the-middle attack

With a reverse proxy, you’ll connect to the reverse proxy and that will create its own connection to the web server and it can provide connectivity to many servers

So at a basic level, you’ll have a secure connection to the reverse proxy and so only one certificate to trust

After those connections are established, it proxies the data between the web browser and the web server

Now this is the same thing that happens with a man-in-the-middle attack, except you control the device in the middle, or at least you should

So you have to put a lot of trust in the developer of that reverse proxy not to leak or steal your data

Your web browser will encrypt the data it sends to the web server but the reverse proxy will then decrypt it

That’s because you aren’t directly connected to the web server

And depending on how it connects to the web server it may or may not re-encrypt that data before sending it on

So do bear in mind that the reverse proxy will see usernames, passwords, etc. as they pass between your computer and the server you intended to connect to

Another thing to bear in mind is a reverse proxy can give you a false sense of security

Let’s say you have a physical network switch and it doesn’t support encryption for management through a web interface

If you use Caddy to access that, while your connection to Caddy will be secure, the security risk remains the same, i.e. the traffic to and from that switch is still unencrypted, but that’s now between Caddy and the switch rather than your computer and the switch

So while your web browser is telling you the connection is secure, anybody that can monitor the traffic between Caddy and the switch will be able to see all the data being exchanged, including usernames and passwords

In that case you’d want to be more strategic as to where you deploy a reverse proxy as you’ll want it as close to the end device as possible and ideally to give it direct access or find some means to block other devices being able to see the unencrypted data

So do plan accordingly

But assuming you trust the developer, I feel that a reverse proxy is very useful for containers

Caddy for instance can be deployed as a container, so as long as the other containers you connect to are on the same host, a lack of encryption between Caddy and another container may not concern you too much because that traffic won’t leave the host

Caddy Container:
The first thing to do is to create a container for Caddy

I like to use Docker Compose, so we’ll edit an existing compose file

nano docker-compose.yml
volumes:
  caddy_data: {}
  caddy_config: {}

services:
  caddy:
    image: caddy:latest
    container_name: caddy
    restart: unless-stopped
    ports:
      - "443:443"
    volumes:
      - ./caddy/Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config

Now save and exit

As with most containers, Caddy needs some information to persist a reboot for instance, so we’re instructing Docker Compose to create some volumes

As usual, we want the latest version of the image for this container

Providing a name for the container is also useful as it makes it easier to reference this and identify it from the command line

And we want it to be always running, unless we manually stop it

As with most secure web services, the standard port is 443, so we’ll have the host expose this

Now there is one slight catch here. You can’t have two applications using the same IP address and port

Because a reverse proxy can simplify our lives, if you do have other containers listening on port 443, it’s better to try and change them or access them through Caddy

There is an alternative option, which involves putting Caddy or the other container on a different IP address to the host, but it would be more complicated

Another thing to bear in mind, is if you’re running Podman for instance, port 443 is a privileged port that normally requires root access. So there would be additional work to run this in rootless mode

The configuration file needs to survive a reboot, so we’ll map that to a file that will be stored on the host

We then map its data and config folders to the volumes we’re creating

Reverse Proxy:
Now in this video we want Caddy to act as a reverse proxy, specifically for containers on the same host

First though we’ll create our Caddyfile to tell Caddy about a web server we want it to proxy

We’ll create the data folder for this container

mkdir caddy

We then need a configuration file, referred to as the Caddyfile, and what you put in that depends on the use case

nano caddy/Caddyfile
whoami.homelab.lan {
	tls internal
	reverse_proxy whoami:80
}

Now save and exit

NOTE: The formatting is sensitive and it requires tabs not spaces

In this example, we want to connect to a web server that we’ll reference in the browser as whoami.homelab.lan, in other words it’s Fully Qualified Domain name (FQDN)

NOTE: Your computer will need to be able to resolve this to an IP address either through DNS or its hosts file

We want the session to Caddy to be secure, so we’ll use TLS and we want Caddy to provide the certificate using its own internal Certificate Authority (CA)

The container we’ll create will be called whoami and it will be listening on port 80

TIP: Container platforms like Docker provide their own DHCP and DNS service so usually you can reference a container by its name

TIP: After you make a change to the Caddyfile you can either restart the container or instruct it to reload the config instead

docker compose exec -w /etc/caddy caddy caddy reload

Next, we’ll edit our compose file and we’ll create our web server container at the end of the services section

nano docker-compose.yml
services:
...
  whoami:
    image: traefik/whoami
    container_name: whoami
    restart: unless-stopped

Now save and exit

We’ll use an image that is actually from the developers of another popular reverse proxy, Traefik Proxy

We haven’t exposed any ports for this container because we want users to access it via Caddy

So the application in the container is still listening on port 80, but the host isn’t making this available to the rest of the network

We’ll then start the whoami container

docker compose up -d whoami

Then we’ll check the container is working

docker ps -l

Next, we’ll start Caddy

docker compose up -d caddy

And then we’ll check that container is working

docker ps -l

Now our web browser needs to trust TLS certificates that are signed by Caddy’s Intermediate CA

That has a certificate signed by its own Root CA, so we need a copy of the root certificate to import into our web browser

To obtain this we’ll copy it from the container to the host

docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt caddy/root.crt

The challenge then is how to transfer this file to your client computer

For me, I just use the clipboard to copy and paste the contents into a new file I create on the other computer

Browsers typically don’t use the root store anymore, so you’ll likely need to import this file into your browser as a trusted CA

How you do that depends on the browser but typically there will be a Privacy and Security option and in there maybe a Security or Certificates option. Further down the rabbit hole you’ll eventually find an option to import an Authority and at that point you can import this file

If we now browse to https://whoami.homelab.lan we should see a secure connection because the web browser trusts the certificate

So what we have here is a secure session to Caddy, which in turn has an insecure and unencrypted session to the web server

Now this makes administration much simpler, because Caddy will automatically create a new certificate for each web server we ask it to proxy, and it will keep these up to date

In addition, our web browser will trust these certificates because it trusts the Root CA

And this will save us a lot of time

Backend Encryption:
Now there is one slight problem that I see with Caddy and this it do with the backend connection

While we’re improving security for containers on the same host that don’t support encryption, what about containers that do support TLS?

Well there’s a problem because Caddy won’t trust a self-signed certificate and the connection will fail without intervention

One option might be to disable security on the backend container and use HTTP instead, but that doesn’t seem ideal

Another is to disable certificate verification on Caddy, which is similar to ignoring the warning in a web browser when it tells you it doesn’t trust a certificate

So for example, lets say we have a server that supports HTTPS on port 443. In that case, we could have an entry in the Caddyfile a bit like this

apache.homelab.lan {
        tls internal
        reverse_proxy https://apache {
                transport http {
                        tls_insecure_skip_verify
                }
        }
}

It’s similar to what we did before, but this time we want to use HTTPS for the connection from Caddy to the web server, but we won’t validate the certificate

Although this works, it isn’t ideal either because there is a risk of a man-in-the middle attack

Now as unlikely as that may seem on an internal network, it is still feasible, particularly if we use Caddy as a reverse proxy for applications on other hosts or for stand alone devices

As part of best practice, if a web server supports HTTPS, we should give it a certificate that Caddy trusts

Now it is possible for Caddy to act as an ACME server, so it could provision the certificates itself, but that’s out of scope for this video, and as ever it depends

Not all devices support ACME and even those that do, may be limited to only working with Public solutions like Let’s Encrypt

It may also need additional software installing, but that may not be possible on standalone devices like network switches for instance

For me it’s something that needs further thought

So for now, I’ll only be using Caddy for containers on the same host

The traffic between the client and Caddy will be encrypted, and the traffic between Caddy and the other container will be on the host

Realistically, if a container or the host is compromised, it’s game over anyway

Networking:
Now there is one last thing to do for completeness and that is to create a separate network

While the whoami container doesn’t have any ports exposed to the external network, we can improve security a bit more

Docker creates virtual networks and by default, all containers will be attached to a default bridge created for the user

As a result, all containers will have direct access to each other

The goal here is to access containers like this via Caddy, so we’ll create a separate network

In which case, we’ll edit the compose file

nano docker-compose.yml
  caddy:
    ...
    networks:
      proxy:

  whoami:
    ...
    networks:
      proxy:

networks:
  proxy:

Now save and exit

First we update the two containers with a network entry which will connect them to a network we’ll call proxy, but you can call this whatever you like

So when the caddy container talks to the one we’ve called whomai, it will now do that over this new network

Then we’ll tell Docker Compose to create this new network

Now you can customise a network more, but for this video we’ll keep things simple

For the changes to take effect we’ll have to stop the containers

docker stop whoami && docker stop caddy

Then a new network needs to be created and the containers rebuilt

docker compose up -d

The we’ll check things are working

docker ps

TIP: You can check what containers are attached to a bridge/network by running a command like this

docker network inspect dockermgr_proxy

NOTE: The network name includes the user name you’ve logged in as and isn’t just what you called it

Now you can and probably should create other separate networks for your containers and you can join Caddy to those as well because a container isn’t limited to one network

For instance, I have a lot of containers based around Prometheus

They all need access to Prometheus or vice versa, so it makes sense to put them in their own network

But any other containers on the host would be in a different network

Similarly, if you have a layered application, you would have a web container that connects to a web network and an application network, an application container connected to the application and database networks, and a database container connected to the database network

So users connect to the web server, that in turn accesses the application on another network, which in turn accesses the database on another network

All of those network layers exist to make it harder to get unauthorised access to the data

Summary:
As long you’re dealing with containers running on the same host as Caddy then I think this can save you a lot of time and effort

When containers are on separate hosts though, things get a bit tricky

So you could install Caddy on multiple container platforms, and for security reasons I do run separate Docker instances in different network segments

This provides you with a secure session to each host and the backend traffic remains on the host

From what I’ve seen, you can have other instances of Caddy obtain their CA certificate from a master one. This way your web browser only needs to trust one root certificate

But for me, the jury’s still out when it comes to encryption between Caddy and separate devices

If a standalone device doesn’t support encryption, you’ll still have unencrypted traffic going over the network

It’s better than no encryption at all, but you could be lulled into a false sense of security if the unencrypted traffic goes over a path that can be monitored by a 3rd party

On the other hand, if the end device does support encryption their could be a challenge getting Caddy to provide it with a certificate that Caddy trusts

In which case, you might still have to manually administer certificates on your devices and so it won’t really save you time

Sharing is caring!