Moving from NGINX to Caddy v2

Motivation

EDIT: I had a misconfiguration that bit me in the ass recently (along with blindly updating Go to 1.17). Code below is updated.

I recently decided to try Caddy v2 for my personal home server, and had such a good and easy time with it that I decided to migrate my website server to Caddy from NGINX. NGINX was doing just fine, fast, and stable. But with the recent addition of a Cloudflare proxy and stuff, I didn’t want to have to deal with certbot and manually dealing with a DNS challenge. And, the prospect of having sane preconfigured behaviors was quite nice, since now with two daughters, I have less and less time to tinker with things like this.

That, and I (with the help of my brothers) had setup NGINX probably five years ago now, and though I still feel like I’m hacking stuff together, back then, it really was quite hacky, so who knows how much bad config was in my NGINX conf file. So if any of you are seasoned webserver sysadmins and are thinking, “what the hell is this config?” I apologize. If you see something egregious or any security vulnerabilities, please shoot me a message/leave a comment and educate me!

So… since I’m on a trip for a concert, I have some time to get this done. I’m happy how easy Caddy is to configure, and you don’t need to write as many lines to config things because it assumes (correctly in my case) defaults for most people running webservers and reverse proxies. Docs are very clear, though getting your intuition from an Apache or NGINX based config to the Caddyfile can be a bit tough.

I’m most impressed with the automatic tls and configuring the wordpress site. Instead of all the certbot stuff and having to run a certbot in systemd or something similar, you can use caddy. I did have a bit of trouble finding how to easily add the DNS challenge plugin, and finally found in the CLI caddy add-package [plugin-package-github-url] so you can add the cloudflare DNS plugin that way. For .php sites, instead of doing all that try_files and fast_cgi stuff, you can just call php_fastcgi directive, and the defaults all work for hosting a wordpress site (and, most .php sites from what I understand). If you need to configure anything, that is all possible.

I’m also in the process of updating a lot of stuff, so these configs will change (haha why is my node code served from /root…?). I also have some redirects that are made in the node express server… Again, why? Who knows. But I’ll clean that up in the future.

Anyways, compare for yourself. They’re not 100% equivalent since I keep messing around with it, and stopped using the hasura container for now, but at least to the end user, everything seems to work as before the change. But, ~160 lines to ~80 lines, around a 50% reduction! YMMV.

NGINX .conf

server {
        root /root/sycpiano;
        index index.php index.html index.htm index.nginx-debian.html;
        server_name seanchenpiano.com www.seanchenpiano.com;

        location ^~ /.well-known/acme-challenge/ {
                root /srv/www/letsencrypt;
                default_type "text/plain";
        }

        location = /.well-known/acme-challenge/ {
                autoindex off;
                return 404;
        }

        location ~ ^/static/scripts/(.*)$ {
                root /srv/www;
                try_files /assets/scripts/$1 =404;
        }

        location ~ ^/static/(.*\.js)$ {
                try_files /web/build/$1 =404;
        }

        location ~ ^/static/(.*)$ {
                root /srv/www;
                try_files /assets/$1 =404;
        }

        location / {
                proxy_pass http://localhost:8080;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection 'upgrade';
                proxy_set_header Host $host;
                proxy_cache_bypass $http_upgrade;
                http2_push_preload on;
        }

        location /pianonotes {
                root /root;
                try_files $uri $uri/ /pianonotes/index.php?$args;
        }

        location /hasura/ {
                proxy_pass http://localhost:8081/;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection 'upgrade';
        }

        location ~ \.php {
                root /root;
                include fastcgi.conf;
                fastcgi_intercept_errors on;
                fastcgi_pass unix:/run/php/php7.2-fpm.sock;
                fastcgi_split_path_info ^(.+\.php)(/.+)$;
                fastcgi_index index.php;
                fastcgi_param PATH_INFO $fastcgi_path_info;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        }

        location ~ /\.ht {
                deny all;
        }
        listen [::]:443 ssl http2 default_server; # managed by Certbot
        listen 443 ssl http2 default_server;
        ssl_certificate /etc/letsencrypt/live/seanchenpiano.com/fullchain.pem; # managed by Certbot
        ssl_certificate_key /etc/letsencrypt/live/seanchenpiano.com/privkey.pem; # managed by Certbot
        include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

        if ($scheme != "https") {
                return 301 https://$host$request_uri;
        }
}
server {

        if ($host = www.seanchenpiano.com) {
                return 301 https://$host$request_uri;
        } # managed by Certbot


        if ($host = seanchenpiano.com) {
                return 301 https://$host$request_uri;
        } # managed by Certbot

        listen 80 default_server;
        listen [::]:80 default_server;

        server_name seanchenpiano.com www.seanchenpiano.com;
        return 404; # managed by Certbot
}

server {
        server_name labs.seanchenpiano.com;

        listen 443 ssl http2; # managed by Certbot
        listen [::]:443 ssl http2;
        ssl_certificate /etc/letsencrypt/live/seanchenpiano.com/fullchain.pem; # managed by Certbot
        ssl_certificate_key /etc/letsencrypt/live/seanchenpiano.com/privkey.pem; # managed by Certbot
        include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

        #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

        root /root/labs;
        index index.html;

        location ^~ /.well-known/acme-challenge/ {
                root /srv/www/letsencrypt;
                default_type "text/plain";
        }

        location = /.well-known/acme-challenge/ {
                autoindex off;
                return 404;
        }

        location / {
                root /root/labs/public/;
                error_log /var/log/nginx/labs.log debug;
                rewrite_log on;
                try_files $uri /index.html =404;
                autoindex off;
        }
}

server {
        listen 80;
        listen [::]:80;

        if ($host = labs.seanchenpiano.com) {
                return 301 https://$host$request_uri;
        } # managed by Certbot

        server_name labs.seanchenpiano.com;
        return 404; # managed by Certbot
}

Caddy v2 Caddyfile

(dns_cloudflare) {
        tls {
                issuer acme {
                        disable_tlsalpn_challenge
                        dns cloudflare [REDACTED]
                }
                issuer zerossl {
                        disable_tlsalpn_challenge
                        dns cloudflare [REDACTED]
                }
        }
}

seanchenpiano.com {
        # Set this path to your site's directory.
        encode zstd gzip
        import dns_cloudflare
        header Strict-Transport-Security "max-age=31536000; includeSubDomains"
        handle /static/scripts/* {
                root * /srv/www
                file_server
        }

        handle_path /static/* {
                handle_path /scripts/* {
                        root * /srv/www/assets/scripts
                        file_server
                }
                handle {
                        @js path *.js
                        root @js /root/sycpiano/web/build
                        root * /srv/www/assets
                        file_server
                }
        }

        handle_path /pianonotes {
                redir https://seanchenpiano.com/pianonotes/
        }

        handle_path /pianonotes/* {
                root * /root/pianonotes
                php_fastcgi unix//run/php/php7.2-fpm.sock {
                        root /root/pianonotes
                }
                @ht {
                        path *.htaccess
                        path *.htpasswd
                }
                error @ht "Unauthorized" 403
                file_server
        }

        handle {
                reverse_proxy localhost:8080
        }

        log {
                output file /var/log/caddy/seanchenpiano.log
        }
}

www.seanchenpiano.com {
        import dns_cloudflare
        redir https://seanchenpiano.com{uri} permanent
}

labs.seanchenpiano.com {
        encode zstd gzip
        import dns_cloudflare
        root * /root/labs/public
        log {
                output file /var/log/caddy/labs.log
        }
        try_files {uri} index.html
        file_server
}