Caddy as a Docker-Compose Reverse Proxy

It seems that every 10 years, a new web server comes along that becomes popular. When I started with hosting my own infrastructure, Apache, first launched in 1994, was the cool kid on the block. 10 years later in 2004, nginx saw the first light and has since become very successful. These days, I use nginx in my self-hosted infrastructure to act as a reverse proxy for my containerized services. It works well and is relatively easy to set up with this project from Evert Ramos, but it seems to be a bit of a niche thing. So I was wondering if there is an alternative to it, as it’s always good to have a plan B.

I am well aware of Traefik, but that’s a bit of an overkill for my needs. I need something simple to set up and maintain because I only need it to do two main things:

  • Serve as a reverse proxy for many Docker Compose based projects running on a single host.
  • The reverse proxy needs to automatically retrieve and update Letsencrypt certificates.

Long story short, say hello to Caddy, which was first released in 2015. At first, I thought Caddy was ‘just’ a Dockerized reverse proxy, but that was a misconception. In its ‘native’ form, Caddy has nothing to do with Docker, it runs as a standalone webserver that is easy to configure and has Letsencrypt capabilities built in. However, with this Docker Compose project by Lucas Lorentz, Caddy becomes a super easy to configure and to deploy reverse proxy for Docker Compose services. So let’s have a look:

The Initial Setup and Test – 5 Minutes

As described on Github, the only two things that are required to run Caddy as a reverse proxy for Docker projects are to create a Docker network for Caddy and then start a Caddy container with a short docker-compose.yml file. Nothing else! No .env file, no initial configuration script, absolutely nothing else.

To run a first test with an example Docker project, the only thing that is required is to point a domain name to the server running Caddy and testing it with a ‘whoami’ container. Overall, it takes 5 minutes to get this up and running, including the automatic retrieval of a Letsencrypt certificate for the domain name. In comparison to my current reverse proxy setup, configuration is significantly simpler, and the reverse proxy runs in a single container rather than being split into three containers in a docker-compose project.

So after 5 minutes I had verified the two main features I require: A reverse proxy for Docker Compose projects and the automatic request and installation of Letsencrypt certificates. That being said, let’s have a look at a couple of additional things that I require every now and then:

Basic HTTP authentication

When initializing a new WordPress instance, it is a good idea to use basic http authentication to limit access to the new instance until it is fully initialized. This is because evil actors watch the certificate generation log and immediately come visit to see if they can subvert a yet unconfigured WordPress instance. With my current reverse proxy solution, I have to add this to the proxy configuration and restart the reverse proxy. With this dockerized Caddy reverse proxy setup, the approach is different: Instead of modifying the reverse proxy configuration file, adding a username and password is done in the docker-compose.yml file of the new WordPress instance. The advantage: Username and password stay in the project it is intended for and the reverse proxy does not have to be restarted. To create a new username and password for a project, go to the Caddy docker-compose directory and use the following command:

docker exec -it 000-proxy-caddy-1 caddy hash-password --plaintext 'THESUPERSECRETPASSWORD'

The command outputs a password that can then be inserted in the docker-compose file of the project. Here’s an example:

networks:
  - caddy
labels:
  caddy: test.mydomain.com
  caddy.reverse_proxy: "{{upstreams 80}}"
  caddy.log.output: stdout
  caddy.basic_auth.bob: "$2a$14$owfswA7iz....."

A single line in the config file and the site is protected with basic http authentication for user ‘bob’.

Logging

By default, Caddy does not log http requests. If required, this can be activated per project. In the configuration snippet above that’s done by the ‘caddy.log.output: stdout‘ line.

Large Uploads

Allowing large uploads, e.g. large images or videos to the WordPress site is another thing that has to work. In my current reverse proxy solution, this does not work out of the box and configuration is a bit tricky. So I was of course wondering how my Caddy based reverse proxy would fare. A bit of ‘AI-magic’ created a flask based http server that would accept uploads of any size and puts the received files into a directory. After starting this server with its own domain name, I then used curl to upload a 2GB random file to the server via the Caddy reverse proxy:

curl -F "file=@/home/martin/2GB.bin" --progress-bar http://ytest.mydomain.com/

And indeed, the file made it all the way to the server’s data directory. Perfect! To see if there is a timeout, I uploaded the 2 GB file over my ‘slow’ 40 Mbit/s DSL uplink so it would take a while. The upload took 9 minutes and there was no timeout, so longer uploads are not a problem, either.

For a practical use case I then configured WordPress to accept larger file uploads and uploaded a 100 MB video. This also worked perfectly and I’m thus pretty sure that Caddy works quite well out of the box with large uploads as well.

Non-standard TCP Ports

The next feature I require every now and then is to run the reverse proxy on a non standard TCP port for secure http i.e. on port 12345 instead of port 443. Would Caddy require port 443 to get the TLS certificate and thus fail in such a configuration? Fortunately, it does not, it seems to use the standard method of using port 80 to get the Letsencrypt certificate, which I do have available in my setup.

First Real World Test

The final step I have done so far was to run this blog via the Caddy reverse proxy for a few hours to see if everything is stable. Also, it was a nice test to see if I could just take my latest backup, un-tar it on the Caddy host and run it there. And indeed, both things worked, only a small change in the docker-compose file was required to use Caddy instead of my current reverse proxy.

Final Words

So far so good, this looks very promising. I’m a bit surprised that the Docker Compose setup is not part of the Caddy project itself but instead handled by a 3rd party. I guess the reason behind this is that Caddy is much more than just a Docker reverse proxy. That being said, I think I will experiment a bit more with this and perhaps transfer some of my projects to this reverse proxy setup.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.