Having to manage SSL certificates is a chore no developer wants to deal with. It can be a hassle to remember to renew them, to set them up, to upload the right files to the right folder.

In this tutorial I want to show you how to serve your web application over HTTPS without creating certificates manually. Instead we’re going to use caddy - open source web server with automatic HTTPS written in Go.

I recently stumbled upon caddy when I was automating the process of loading a VM with the source code of a web app, then exposing the app on a public address over HTTPS.

Ideally, I envisioned the steps to be more or less 1) run the vm, 2) tweak a few DNS settings, 3) copy the source and 4) run the app. Thanks to caddy I managed to preserve that sequence without dealing with certificates myself.

How does caddy work?

Serving a website with caddy can be summarized in 3 steps:

  1. Create the Caddyfile
  2. Configure your web server in Caddyfile (for example saying ‘make iamawesome.com serve those files’)
  3. Run caddy start

That’s it, https://iamawesome.com will be accessible shortly after ✅ From here on caddy will also handle renewing the ceretificate!

How is the certificate created exactly?

Obtaining a publicly-trusted SSL certificate requries a validation process which boils down to up to three challenges that caddy will perform:

If the selected challenges are sorted out correctly caddy will proceed with creating the certificate.

Under the hood Caddy uses the well-known Let’s encrypt Certificate Authority to actually issue the certificate for your website.

Prerequisites

  • some command line experience
  • brew install caddy
  • ruby 2.6.3, node 12.4.0 + yarn

Run your static website with caddy

Running a web server to serve a static website on localhost is as easy as:

caddy file-server --domain localhost

given that you’re in the root directory of your site (where your index.html resides) and the default port 80 is available.

⚠️ caddy commands may require sudo permissions to spin the web server.

Alternatively, to indicate root folder you can:

caddy file-server --root /path/to/your/website

You can also specify another port to bind on:

caddy file-server --listen :8085


Let’s create a ‘hello world’ static website and serve it on https://app.localhost:8085:

mkdir mywebsite && cd mywebsite
echo '
Hello world!
' > index.html caddy file-server --domain app.localhost --listen :8085

Congrats! Open https://app.localhost:8085 and see your website served over HTTPS 😉

Configuring the web server in command line in this case is an alternative to creating a Caddyfile. To run it on the same address using a Caddyfile, the file should contain:

app.localhost:8085
root * /path/to/your/mywebsite
file_server

then run caddy in the folder of your Caddyfile:

caddy start

Run your dynamic web app with caddy

In the spirit of the previous Docker tutorials (Part 1 & Part 2) let’s use a Todos app that consists of a Vue.js single page app and a Rails app in api mode. Go ahead and clone the Todos from here.

Install libraries for both parts & set the databse:

cd todos-api && bundle install && rails db:setup
cd ../todos-app && yarn install

Build Vue.js app:

yarn build

Run rails api (in another terminal):

cd ../todos-api && rails s

Now if we wanted to serve only the Rails api over https we can simply make use of caddy’s reverse proxy mode:

caddy reverse-proxy --from api.localhost --to 127.0.0.1:3000

given that we’ve run Rails server on port 3000

Let’s serve the front-end part on app.localhost and the back-end on api.localhost. Here’s the content of the Caddyfile:

api.localhost {
  reverse_proxy * localhost:3000
}

app.localhost {
  root * /full/path/to/todos-app/dist
  
  file_server
  try_files {path} ./index.html
}

Here with try_files directive we set ./index.html as a default for all missing files. That might also be suitable for an error page like 404.html or similar. Make sure to repalce /full/path/to/todos-app/dist with the full path to the todos-app/dist folder on your machine.

Again run caddy start, then open https://app.localhost and start adding todos securely 🔒

Now if Rails api was namespaced with /api/ we could serve the whole Todos app (Vue.js & Rails apps) on the same domain like myapp.localhost. Then the Caddyfile will look a bit different:

myapp.localhost {
  reverse_proxy /api/* localhost:3000
  root * /full/path/to/todos-app/dist
  file_server
}


⚠️ In all of the examples we used localhost as the main domain address. In those cases caddy will use its internal certificate authority to create the certificates (self-signed). However, when using caddy in production instead of myapp.localhost you’ll use your real domain like awesometodos.com, then caddy will use its default CA - Let’s encrypt

Certificate issue limits

Let’s Encrypt is the default CA of caddy as we have already mentioned, hence caddy depends on Let’s Encrypt rate limits:

  • 50 certificates per week (1 certificate for 1 registered domain)
  • 100 subdomain names per certificate (in case you want to combine many subdomains in 1 certificate)
  • 5 renewals per certificate per week
  • 5 validation failures per hostname per hour

Those are the limits the you’re likely to run into. There are a couple more that you’re unlikely to touch, however you can find all the data on the official Let’s Encrypt rate limits page if you want to be on the safe side.

I personally got stung by the last one - 5 validation failures per hour. As I described above part of the setup that I was adding caddy to involved configuring some DNS settings. While tinkering with those settings, then waiting for them to persist and running caddy at the same time I started getting ‘too many failed authorizations recently’. So I had to wait for an hour to spin the web server again and test my setup. Luckily my caddy configuration was relatively simple and I didn’t see that error again.

I highly recommend using Let’s encrypt staging environment while developing locally and testing your caddy setup, especially if it’s a complex one. The staging environment has much bigger rate limits and so it’s easier to debug connectivity problems and others.

I hope that article was helpful and caddy will come in handy to you at some point 🤞