Skip to content

Advanced Usage of Caddy

Introduction

So far, we have vaultwarden being proxied to port 80 via dns. Excellent! If that was all we needed caddy for, we can pat ourselves on the back and walk away. However we still have a ton of features we still want to implement. Encryption being on the top of the list.

Let’s have a look at some of the cooler things caddy can do to help protect our services.

TL;DR

In this section of the guide, we will demonstrate:

  • Snippets
  • Hot Reloading
  • IP Whitelisting
  • Proxying self signed TLS services
  • Serving with Self Signed TLS

Using Caddy Snippets

At the moment our caddy file is pretty darn short. Which is good! However, as you add more containers, it’ll get more unwieldy to manage. This is where we can leverage snippets for more reusability. Snippets are either reusable caddyfile blocks, or more commonly, a separate caddyfile block that we import as a discrete file. Let’s show how this can help break up our syntax:

  • rename your Caddyfile to vaultwarden.caddy
  • create a new Caddyfile with the following content:
import *.caddy
  • reload the caddy container

Functionally we actually haven’t changed anything. However, now we can use discrete files to separate our sites, keeping our structure much cleaner. As you add more files, Caddy will automatically import their configs on load.

Hot Reloading

At the moment we are restarting caddy by restarting the whole container. Which is fine. At least until you make a syntax error in your caddyfile and break the whole darn container on reload. This is especially fun when you are relying on caddy to reverse proxy your remote connection, which now just permanently dropped.

We can prevent that by running a hot reload instead. a Hot reload will restart the caddy service without restarting the whole container, and warn you if there is a syntax problem.

  • Create a new file at /mnt/containers/hot-reload.sh
  • Add the following content:
#!/bin/sh
docker exec caddy caddy reload --adapter caddyfile --config /etc/caddy/Caddyfile
  • Run the following to make the script executable and run, to see if caddy hot reloads
cd /mnt/containers/caddy
chmod +x hot-reload.sh
./hot-reload.sh

Great! Now if you happen to mess up a configuration, caddy will keep the last known good configuration and let you know there’s an error (at least until restart).

IP Whitelisting

Some, possibly even most, services probably shouldn’t be exposed to the public web. If you haven’t port forwarded, then this is true by default. If you have port forwarded, you’ll want some way to limit external access. You can do this by limiting the subnets that can have access to your service.

  • In the Caddyfile, define two named matchers nested in snippets: localHostOnly and localSubnets:
(localHostOnly) {
    @localHostOnly remote_ip 127.0.0.1
}

(localSubnets) {
    @localSubnets remote_ip private_ranges
}

import *.caddy
  • Modify your vaultwarden.caddy to include the remote_ip matcher set to 127.0.0.1 (essentially blocking all remotes)
http://warden.<your-domain> {
    import localHostOnly
    reverse_proxy @localHostOnly http://<your-IP>:8080
}

  • Reload caddy and confirm you can no longer reach the site

  • change the vaultwarden config to something more sane like just local subnets and reload caddy again. Confirm that you can now access the site again.
http://warden.<your-domain> {
    import localSubnets
    reverse_proxy @localSubnets http://<your-IP>:8080
}

Proxying self-signed Encrypted Services

So far we’ve been using caddy to just proxy docker services, and that’s great. But we don’t have to proxy docker services. We don’t even have to proxy services on the same host. We can even proxy services that use self signed certificates. For example, our host is running cockpit on port 9090, using self signed HTTPS. Let’s try proxying that:

Info

this example will only work if you are running cockpit on your host. Proxying encrypted services also has a performance impact (compared to HTTP), albeit negligible for anything short of production scale.

  • Create a dns entry for <yourdockerserver>.<yourdomain>.com.au

  • create a .caddy file in the caddy/container-config directory with the following and hot-reload caddy:
http://<your-host>.<your-domain> {
    import localSubnets
    reverse_proxy @localSubnets https://<your-ip>:9090 {
        transport http {
            tls_insecure_skip_verify
        }
    }
}

Info

This caddy directive tells caddy to restrict access to local subnets and ignore the self signed nature of the https certificate it’s pulling from

  • navigate to your new dns address and see if you can get to cockpit!

Serving with Self Signed TLS

So far, everything we’ve been proxying has been back to unencrypted HTTP. In fact, we’ve actually reduced our security with cockpit since we’ve downgraded from self-signed https to normal http. oops.

Let’s get all of our services encrypted with HTTPS instead.

  • Modify your base Caddyfile to resemble the following:
{
    # do not attempt to install certs on the docker container
    skip_install_trust
}

(localTLS) {
    tls internal {
        on_demand
    }
}

(localHostOnly) {
    @localHostOnly remote_ip 127.0.0.1
}

(localSubnets) {
    @localSubnets remote_ip private_ranges
}

import *.caddy

Info

We are creating a global setting for caddy to skip trying to install itself as a root CA, in the event we want to run caddy as a rootless container. We are also adding a new snippet to self sign a site when used

  • Now go into the two .caddy files and change the sites to https. Also add the localTLS snippet. hot reload when finished.

  • Now browse to either site, and see it’s giving scary warnings! Don’t worry, this is a good thing. This means we’ve encrypted our sites (albeit with self signed certs).

Using Docker Networks

right now we have vaultwarden forwaded on port 8080 and our vaultwarden.caddy is referring to the host port. This isn’t ideal and isn’t necessary if both caddy and a container are running in docker.

  • create a docker network for our reverse proxy.
docker network create reverseproxy-nw
  • Set up caddy to use that network and update with docker-compose up -d
services:
  caddy:
    image: caddy:latest
    container_name: caddy
    restart: unless-stopped
    security_opt:
      - label:disable
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./container-config:/etc/caddy
      - /etc/localtime:/etc/localtime:ro
    networks:
      - reverseproxy-nw

networks:
  reverseproxy-nw:
    external: true

  • Set up vaultwarden (or whatever your web service) to live on that network as well. Comment out the published port:
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: always
    security_opt:
      - label:disable
    # ports:
      # - 8080:80
    volumes:
      - ./container-data/data:/data
      - /etc/localtime:/etc/localtime:ro
    networks:
      - reverseproxy-nw

networks:
  reverseproxy-nw:
    external: true
  • Bring up the service with docker-compose up -d

  • Finally modify the vaultwarden.caddy to refer to vaultwarden by its internal port and container name:
https://warden.<your-domain> {
    import localSubnets
    import localTLS
    reverse_proxy @localSubnets http://vaultwarden
}
  • hot reload caddy, and you should see that vaultwarden is still accessible! despite not having any published ports.

Moving On

So far we’ve really just scratched the surface of what caddy can do. It’s a very powerful tool. However, we still have a big problem. All of our websites have scary warnings!

If we have a registered domain, this is a fixable problem. Let’s figure out how in Adding Acme Certification.