Switching from Nginx to Caddy

 •  Filed under caddy, nginx, web server, letsencrypt

It would be no exaggeration to say that Caddy, an up-and-coming HTTP/2 server written in Go that's been gaining a lot of traction, is outright witchcraft. Out of the box, it just works. With HTTPS. With very little configuration.

In a previous post, I wrote that this blog is running on Ghost + PM2 + nginx. I have to admit, I have become somewhat complacent and in the past have relied heavily on Ansible roles to set a web server up for me. To set up this blog, I decided to roll up my sleeves and do it the old-fashioned way: manual package installs and conf files. And surprise, surprise: getting nginx to work perfectly with Ghost took more than the 5 minutes I expected.

To demonstrate just how neat Caddy is, let's compare my nginx conf file with the Caddyfile:

server {  
    listen 80;
    server_name blog.kixpanganiban.com;
    return 301 https://$server_name$request_uri;

server {  
    listen      443 ssl;
    server_name blog.kixpanganiban.com;
    ssl_certificate /etc/letsencrypt/live/blog.kixpanganiban.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/blog.kixpanganiban.com/privkey.pem;

    charset     utf-8;

    # max upload size
    client_max_body_size 75M;

    location / {
       proxy_set_header   X-Real-IP $remote_addr;
       proxy_set_header   Host      $http_host;
       proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_set_header        X-Forwarded-Proto https;
       proxy_pass   http://localhost:2368/;

The nginx conf file. 25 lines (+1 blank space before EOF), explicit directive to redirect http:// to https://, server directives to attach the LetsEncrypt cert files to the SSL server, location directives to properly forward the client information and make HTTPS redirect work without making Ghost stuck in a redirect loop.

And here's the Caddyfile:

proxy / localhost:2368 {  

The Caddyfile is four freaking lines. And it does everything that the nginx conf file it preceded does: HTTP to HTTPS redirect, SSL, and proxying. The transparent directive on line 4 basically takes care of all the proxy_ directives in the nginx conf.

The whole process of installing Caddy, writing the Caddyfile, and swapping nginx out for it took me a little under 5 minutes. In a nutshell, here are the steps I took:

  1. Download the Caddy archive from https://caddyserver.com/download
  2. Move the caddy executable somewhere accessible in my $PATH
  3. Write the Caddyfile in my Ghost root
  4. Run $ sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/caddy to allow Caddy to bind to 80 and 443 without sudo
  5. Run $ nohup caddy &

After step 5, Caddy prompted me to enter my email for the LetsEncrypt cert. In the background, it took care of signing me up for LE, verifying my domain ownership, and downloading and attaching my cert files. Then I visited http://blog.kixpanganiban.com and it just worked (and it redirected me to a fully functional HTTPS to boot.

Sure, seasoned devops guys won't sweat configuring nginx like I did. And to be fair, installing your own SSL cert with LetsEncrypt/Certbot is a pretty straightforward process. But, the fact that Caddy does all this for minimal effort is frankly astonishing, and when Caddy hits the distro package managers (yum, apt, pacman, brew, etc.) it's gonna get even easier.

Addendum: The good folks over at Hacker News have been kind enough to point out that a verbose config is almost always better than a seemingly magical configuration. And I absolutely agree. However, I argue that anyone looking at the Caddyfile who understands Caddy just as much as they do nginx would agree that the file is clear and self-explanatory:

Line 1: blog.kixpanganiban.com

This tells Caddy to automatically serve the site on both HTTP (port 80) and HTTPS (port 443) because I didn't explicitly define a port to bind to, ie blog.kixpanganiban.com:80.

Line 2: proxy / localhost:2386 {

Equivalent to nginx's proxy pass. This tells Caddy to proxy requests for the root path / to the Ghost backend listening at localhost:2386.

Line 3: transparent

A Caddy preset directive which is basically a shorthand for the following:

header_upstream Host {host}  
header_upstream X-Real-IP {remote}  
header_upstream X-Forwarded-For {remote}  
header_upstream X-Forwarded-Proto {scheme}  

...which is exactly the same things you would define explicitly in a typical nginx config.

And Line 4, of course, is the closing bracket.

@tyinq pointed out that it might not have been the best idea to pit Caddy against nginx, and after getting a very short downtime with Node/Ghost being unable the traffic spike (and no caching middleware in my Caddy binary, d'oh!) I can see where the other sentiments are coming from.

I've since put the whole site behind CloudFlare for good measure. :)