Post

Switching to Caddy

Backstory

This entire chronicle happened since I mentioned that I was installing nginx to my Proxmox server. A friend told me to switch to Caddy instead, and I was like, “why?”. I didn’t really get the reason to switch to Caddy, but I did decide to give it a shot since with its Caddyfile feature it seemed perfect for development environments.

well, this made me look further into what Caddy can do, and I found out that it can do a lot more than what I thought.

What is Caddy?

Caddy is an OSS web server written in Go. At least, that’s what it is at its core.

Caddy is very extensible, and it can easily be configured to replace many moving parts of a web server. Even out of the box, it replaces whatever HTTPS certificate solution you were previously using, by providing certificates from Let’s Encrypt or ZeroSSL by default. It can even provide self-signed certificates for local development with its own root authority (which you obviously need to add to your client’s trust store).

How do we extend Caddy?

Before getting too deep, let’s talk about the basics.

How to install modules

You can install modules by going to the Caddy downloads page and downloading a binary with the modules you wish to use. While this initially made me worry about automatic updates, I found out that this is not an issue as Caddy updates itself with the according binary for the modules you have installed.

Automatic HTTPS

As mentioned, Caddy provides a default ACME certificate authority, but you can easily change that to something such as Cloudflare.

So, how do we do that?

As you might have noticed in the previous section, Caddy provides modules for many DNS providers (look for dns.provider.*). Simply enable the one you wish to use and download the binary.

After that, you can start using it by adding the following to your Caddyfile:

1
2
3
4
5
6
7
8
{
    # If you wish to explicitly use a self-signed
    # certificate uncomment the line below and comment
    # the acme_dns line
    #
    # local_certs
    acme_dns cloudflare {env.CF_API_TOKEN}
}

This will set it as the default DNS provider for all domains.

If you wish to configure this per domain instead, you can do this instead:

1
2
3
4
5
example.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
}

Note: I am using environment variables to set the API token for Cloudflare. You will see how I have done that later in this post.

Dynamic DNS

Now if your reaction was like mine, you are probably equally shocked and excited about the fact Caddy can provide dynamic DNS records. Out of the box, Caddy does not come with the ability to do this, but again, that’s what modules are for.

You will need the dynamic_dns module. Additionally, remember the DNS provider module you chose earlier for automatic HTTPS? That same module also gives us the DNS provider information here.

After that, you can use a config such as this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
    dynamic_dns {
        provider cloudflare {env.CF_API_TOKEN}
        domains {
            example.com ddns # This will update 'ddns.example.com'
            domain.com @ ddns # This will update 'domain.com' and 'ddns.domain.com'
        }
        ip_source upnp # You may specify multiple sources. They will be tried in order
        ip_source simple_http https://icanhazip.com
        ip_source simple_http https://api64.ipify.org
        ip_source interface eth0
        check_interval 5m
        versions ipv4 ipv6 # Update both A and AAAA records
    }
}

Ok, cool but how do I switch?

Alright, I get it. Enough of what it can do. How do you go from nginx to Caddy?

I will show you a couple examples from nginx and the way I have achieved them in Caddy.

First of all, Caddy has no such thing as sites-available or sites-enabled. Instead, it has a Caddyfile. Though, I assume you do not wish to have all of your domains in one file. That’s why we will be using the import directive.

1
2
3
4
5
{
    ... # Contents of Caddyfile
}

import Example

Simple as that. This will import directives defined in the Example Caddyfile.

Now, how do we create the sites themselves? I will show you a practical example, that being my Gitea config file.

Let’s start with my nginx config file:

1
2
3
4
5
6
7
8
9
10
11
12
server {
    server_name git.aesth.dev;
    client_max_body_size 10M;
    location / {
        proxy_pass http://10.10.10.101:3000;
    }

    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    ssl_certificate         /etc/ssl/certs/key.pem;
    ssl_certificate_key     /etc/ssl/private/key.pem;
}

Simple enough, right? Well, as you are about to see, this can become even easier using Caddy.

1
2
3
4
5
6
7
8
git.aesth.dev {
        encode gzip
        reverse_proxy 10.10.10.101:3000

        request_body {
                max_size 10MB
        }
}

That’s it. This will also handle HTTPS automagically using the options we specified earlier in the global config. The way this is set up for me is that git.aesth.dev is simply a CNAME record to ddns.aesth.dev which I have also shown you how to configure earlier.

Ok, now how about more “complex” configurations?

File Server

Take this nginx configuration for my old website:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
        root /var/www/aesth;
        index index.html;
        server_name aesth.dev;

        location / {
                try_files $uri $uri/ =404;
        }

        location /reboot {
                alias /var/www/reboot;
        }

        listen 443 ssl http2;
        listen [::]:443 ssl http2;
        ssl_certificate /etc/ssl/certs/aesth.pem;
        ssl_certificate_key /etc/ssl/private/aesth.pem;
}

This is a bit more complicated to achieve in Caddy, as it requires handling of trailing slashes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
aesth.dev {
    encode gzip
    redir /reboot /reboot/
    root * /var/www/aesth
    try_files {path} /index.html
    file_server
}

aesth.dev/reboot/ {
    encode gzip
    root * /var/www/reboot
    try_files {path} /index.html
    file_server
}

This will handle aesth.dev/reboot/ and redirect aesth.dev/reboot to it.

The redir directive is quite powerful, so you can even do things such as redir /reboot /reboot/?{query} if you wish to preserve the URL query string.

There is a lot to cover here, but the vast majority of configurations can be translated to Caddy without too much trouble. You can see more info about this on the Caddy community wiki

Running as a service

Now, you will probably want to run Caddy as a service. This section will also show you how I configured my environment variables.

Simply add the following file to /etc/systemd/system/:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# caddy.service
#
# For using Caddy with a config file.
#
# Make sure the ExecStart and ExecReload commands are correct
# for your installation.
#
# See https://caddyserver.com/docs/install for instructions.
#
# WARNING: This service does not use the --resume flag, so if you
# use the API to make changes, they will be overwritten by the
# Caddyfile next time the service is restarted. If you intend to
# use Caddy's API to configure it, add the --resume flag to the
# `caddy run` command or use the caddy-api.service file instead.

[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target

[Service]
EnvironmentFile=/etc/caddy/env
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

Then simply add the following to /etc/caddy/env:

1
CF_API_TOKEN=YOUR_API_TOKEN

The resulting directory structure for /etc/caddy/ looks like this in my case:

/etc/caddy
├── Caddyfile
├── env
├── Gitea
└── ...

And my Caddy binary is located at /usr/bin/caddy.

Conclusion

I hope this post has also made you consider switching to Caddy, or at least made you think about it. In the end Caddy has replaced handling HTTPS and my old cron job which handled running cloudflare-ddns with a simple nicely contained solution.

This post is licensed under CC BY 4.0 by the author.