Migrating From Nginx to Caddy

Published: November 05, 2024
Caddy offers a simplified and easier to manage configuration than nginx. I wanted to give it a try.

Caddy is the relatively new kid on the block in web servers offering a simple configuration for common setups that would normally take multiple lines in nginx for the same configuration. I wanted to convert my personal VPS from nginx to caddy for all of my sites.

The Current Setup

  • VPS with web sites running under a non-privileged user.
  • Nginx listening on ports 80 and 433. Each site is defined in /etc/nginx/snippets/ and symlinked from /etc/nginx/sites-available/ to /etc/nginx/sites-enabed/.
  • Certbot installed to handle SSL certificates through using webroot verification.
  • All sites and apps live under /app/.
  • Rails webapps - which includes this blog under two top-level domains and five under subdomains. The puma webservers for the webapps put sockets in /app/socks/ which nginx reverse proxies to.
  • One static site under a different domain.
  • An instance of The Lounge RSS client under a subdomain using a reverse_proxy.
  • Two static sites under a subdomain that are behind basic auth and are not using SSL.

Install Caddy

Using the official installation docs, I copy/pasted the commands to install the stable release.

$ sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
$ curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
$ curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
$ sudo apt update
$ sudo apt install caddy

All this time, nginx is still running and taking up ports 80 and 443 which caddy also wants to use. So the systemd service included in the install will be in a failed state. This is fine for now.

$ systemctl status caddy
× caddy.service - Caddy
     Loaded: loaded (/usr/lib/systemd/system/caddy.service; enabled; preset: enabled)
     Active: failed (Result: exit-code) since Fri 2024-09-27 22:13:22 UTC; 19s ago
       Docs: https://caddyserver.com/docs/
   Main PID: 4022 (code=exited, status=1/FAILURE)
     Status: "loading new config: http app module: start: listening on :80: listen tcp :80: bind: address already in u>
        CPU: 66ms

For now, I want to configure my apps then shutdown nginx and start caddy to let it take over.

Configuring Caddy

Caddy's configuration file (singular) lives at /etc/caddy/Caddyfile. The default file contains some example code which I removed as it's not needed.

Each site or webapp lives in a server block. The first part of the block (outside of the { } is the domain(s). It's optional to include the http/https. Caddy will assume you want to support SSL by default. Inside the block is where you define where your site lives, other settings, and matchers. Unlike nginx which could require tens of lines to configure everything about a site including SSL, custom headers, etc, Caddy makes a lot of assumptions and configuration for you.

Configuring Rails Apps

All six Rails apps are configured the same. The domains/subdomains used, location of the public directory, and the location of the socket is what changes.

Here's the complete block for configuring this blog:

t27duck.com, www.t27duck.com, t27duck.xyz, www.t27duck.xyz {
  root * /apps/t27duck/current/public
  encode zstd gzip
  file_server

  @static {
    file
    path *.ico *.css *.js *.gif *.webp *.avif *.jpg *.jpeg *.png *.svg *.woff *.woff2 *.map *.swf *.flv
  }
  header @static Cache-Control "public, max-age=5184000"

  @notStatic {
    not file
  }
  reverse_proxy @notStatic unix//apps/socks/puma.t27duck.sock
}
  • The root directive points to the public root of the website. In this case, we're all requests are relative to the location of the public directory of the Rails app.
  • The encode directive implements compression for what's returned from the request.
  • The file_server directive allows for file system access to everything in the root of the site. This allows things such as CSS and JavaScript files which physically live in the public directory to be served.
  • @static defines a matcher. For any static file requests (the "file" part) in which the path ends in any of the listed file extensions, a cache-control header is included in the response.
  • @noStatic defines another matcher. In this case I'm defining "@notStatic" as anything that is not a physical file on disc.
  • Lastly, reverse_proxy directs all requests that match "@notStatic" (meaning a request that does not match a file living in the public directory) to the socket for Rails to process.

The order of the declarations in the block does not matter. Caddy has a set precedence for each directive. For example, reverse_proxy takes precidence over file_server. This is why we have to instruct revese_proxy to match @not_file to allow physical files to be served via file_server and skip the proxy.

This block is repeated for each Rails app with applicable file paths and domains changed.

Configure The Lounge Installation

Per The Lounge's documentation, all that is needed for Caddy is to specify the subdomain I want and a single reverse_proxy directive pointing to the running installation.

irc.t27duck.xyz {
  reverse_proxy http://127.0.0.1:9000
}

Configure static sites

For the basic static sites, a root and file_server directory is all that is required.

otherdomain.com, www.otherdomain.com {
  root * /apps/staticsite1
  file_server
}

Configuring http-only + basic auth Static Sites

Lastly, we need to configure sites that I do not want SSL over and "lock" (because this is really not secure) them behind a basic auth prompt.

By default, caddy assumes you want to enforce SSL for your sites. For cases where you just want http access, you can include the leading "http" for your domain list.

Basic auth is handled with the basic_auth directive. Its block takes the username and the hashed password. A hashed password can be generated via the caddy hash-password command.

$ caddy hash-password -p mypassword

The output of the above command can be used in the basic_auth directive.

The rest of the server block is identical to the other static sites.

http://subdomain.t27duck.com {
  basic_auth {
    theusername $2a$14$6abcdefg....
  }
  root * /apps/staticsite2
  file_server
}

Configuring SSL

Caddy transparently handles SSL certificate configuration and renewals. It requires a proper A/AAA DNS setting for each domain. There's no need for certbot or configuring challenge-answer checks via a .well-known directory.

Bonus: Block php requests

There are times when bots and scanners hit my sites thinking it's possibly a Wordpress blog resulting in false positive exception alerts. The following matcher and declaration can send any request ending in .php back as a 404:

@blocked {
  path *.php
}
respond @blocked 404

Switching over to caddy

With the configuration done, nginx can be shut down (freeing up ports 80 and 443) and caddy can start up.

$ sudo systemctl stop nginx
$ sudo systemctl start caddy

Checking the status of caddy shows the service running as expected.

$ systemctl status caddy
● caddy.service - Caddy
     Loaded: loaded (/usr/lib/systemd/system/caddy.service; enabled; preset: enabled)
     Active: active (running) since Fri 2024-09-27 22:50:33 UTC; 11s ago
       Docs: https://caddyserver.com/docs/
   Main PID: 5461 (caddy)
      Tasks: 9 (limit: 9444)
     Memory: 14.5M (peak: 15.3M)
        CPU: 291ms
     CGroup: /system.slice/caddy.service
             └─5461 /usr/bin/caddy run --environ --config /etc/caddy/Caddyfile

Cleaning Up and Removing nginx.

To complete the change over, once all sites are verified to be working as expected, existing nginx configs can be backed up to the home directory. After that, all packages and files related to both nginx and certbot can be safely removed. Caddy's SSL management automatically created new certificates to serve requests.

$ tar -czf ~/nginxconf.tar.gz /etc/nginx/snippets /etc/nginx/sites-available /etc/nginx/.htpasswd
$ sudo apt-get purge nginx nginx-common
$ sudo apt-get purge certbot
$ sudo apt-get autoremove

The End

The end result of all this is I'm now running an new modern web server using 70-80% less configuration than before. This makes managing and adding additional sites quicker and easier over time.

Documentation Referenced and Other Helpful Links

Caddy Installation: https://caddyserver.com/docs/install#debian-ubuntu-raspbian
Sending static files to file_server and not reverse_proxy: https://caddy.community/t/v2-serve-rails-application-with-static-assets/9006
Auto HTTPS: https://caddyserver.com/docs/automatic-https
Basic auth: https://caddyserver.com/docs/caddyfile/directives/basic_auth
Blocking access based on request extension: https://caddy.community/t/how-to-block-file-and-directory-access-in-caddy-v2/8010
Rewrite rules: https://caddy.community/t/caddy-v2-help-with-rewrite-rule-using-value-contained-in-the-query-string/8524
Caddy and system tmp: https://caddy.community/t/reverse-proxy-wiht-unix-socket-not-working/23223

Tags: caddy, rails, vps