Using wildcard domains with multiple file_server sites

1. The problem I’m having:

I’m hosting a couple dozen static sites at *.classes.example.com (like course1.classes.example.com, course2.classes.example.com, and so on).

(For the sake of illustration and reproducibility, I’ll use classes.localhost instead here).

My Caddyfile looks like this:

course1.classes.localhost {
	root * /var/www/html/classes/course1
	file_server
}

course2.classes.localhost {
	root * /var/www/html/classes/course2
	file_server
}

# A bunch more...

That works fine, and I could probably use a snippet or something to automate it more, but this is okay.

If I run this locally, and look in my data directory, I can see that it generates individual certificates for each of the sites:

certificates
└── local
    ├── course1.classes.localhost
    │   ├── course1.classes.localhost.crt
    │   ├── course1.classes.localhost.json
    │   └── course1.classes.localhost.key
    └── course2.classes.localhost
        ├── course2.classes.localhost.crt
        ├── course2.classes.localhost.json
        └── course2.classes.localhost.key

My concern is that in production, I’ll run into Let’s Encrypt rate limits, given how many there are. I’d like to create just one wildcard domain and have all the sites use that.

To test this locally, I added a wildcard domain:

*.classes.localhost {
	tls internal

	# For production
	# tls {
	# 	dns digitalocean {env.DO_API_TOKEN}
	# }
}

course1.classes.localhost {
	root * /var/www/html/classes/course1
	file_server
}

course2.classes.localhost {
	root * /var/www/html/classes/course2
	file_server
}

# A bunch more...

I was hoping that course1.classes.localhost and course2.classes.localhost would not use site-specific certificates and instead use the wildcard one, but that’s not the case. Caddy generates a wildcard certificate, but it also generates site-specific ones too:

certificates
└── local
    ├── course1.classes.localhost
    │   ├── course1.classes.localhost.crt
    │   ├── course1.classes.localhost.json
    │   └── course1.classes.localhost.key
    ├── course2.classes.localhost
    │   ├── course2.classes.localhost.crt
    │   ├── course2.classes.localhost.json
    │   └── course2.classes.localhost.key
    └── wildcard_.classes.localhost
        ├── wildcard_.classes.localhost.crt
        ├── wildcard_.classes.localhost.json
        └── wildcard_.classes.localhost.key

2. Error messages and/or full log output:

There are no error messages or log issues—Caddy just creates a bunch of certificates instead of just one wildcard one. This is what’s in the data folder:

certificates
└── local
    ├── course1.classes.localhost
    │   ├── course1.classes.localhost.crt
    │   ├── course1.classes.localhost.json
    │   └── course1.classes.localhost.key
    ├── course2.classes.localhost
    │   ├── course2.classes.localhost.crt
    │   ├── course2.classes.localhost.json
    │   └── course2.classes.localhost.key
    └── wildcard_.classes.localhost
        ├── wildcard_.classes.localhost.crt
        ├── wildcard_.classes.localhost.json
        └── wildcard_.classes.localhost.key

3. Caddy version:

I’m using Caddy v2.7.6 .

4. How I installed and ran Caddy:

a. System environment:

I’m using Caddy through Docker Desktop 4.26.1 and Docker Compose v2.23.3 on macOS Sonoma 14.2.1

c. Service/unit/compose file:

version: "3.8"

services:
  caddy:
    image: caddy:2-alpine
    ports:
      - 80:80
      - 443:443

    volumes:
      - "./sites/:/var/www/html/:ro"
      - "./caddyfile-dev/Caddyfile:/etc/caddy/Caddyfile"

      # Caddy needs to mount /data and /config for persisted data
      - "./caddy_data/:/data/"
      - "./caddy_config/:/config/"

d. My complete Caddy config:

Here’s the full Caddyfile:

# Wildcard domain
*.classes.localhost {
	tls internal
}

course1.classes.localhost {
	root * /var/www/html/classes/course1
	file_server
}

course2.classes.localhost {
	root * /var/www/html/classes/course2
	file_server
}

5. Links to relevant resources:

In the Common Patterns section of the documentation, it discusses wildcard domains, and I think I’d need to include each of the site-specific blocks inside the wildcard block with some combination of handle and host, but I’m not entirely sure how that would look, since the documentation shows basic respond commands and not server names and roots and file server details

So it seems like something like this does it:

*.classes.localhost {
	tls internal
	
	@course1 host course1.classes.localhost 
	handle @course1 {
		root * /var/www/html/classes/course1
		file_server
	}

	@course2 host course2.classes.localhost 
	handle @course2 {
		root * /var/www/html/classes/course2
		file_server
	}
}

This only generates one certificate:

certificates/local/wildcard_.classes.localhost
├── wildcard_.classes.localhost.crt
├── wildcard_.classes.localhost.json
└── wildcard_.classes.localhost.key

Is this the correct approach?

You can have tens of thousands before it becomes a problem.

Yep, but if you want a wildcard cert you will need to use the DNS challenge when using a real (public) domain name.

1 Like

Yeah, I’ve got that set up and working with the DigitalOcean DNS module for production:

*.classes.andrewheiss.com {
	# For production
	# tls {
	# 	dns digitalocean {env.DO_API_TOKEN}
	# }
}

This is genuinely surprising to me! I’m converting from an old Apache server that I set up in 2015ish when Let’s Encrypt was brand new and had really low rate limits, and on that server I set up DNS challenges with certbot to handle wildcard domains to reduce the strain on Let’s Encrypt. So my mental model is apparently super outdated!

I have maybe 45–50 total static sites and reverse-proxied services, and I add a couple new ones every year (for semester-specific academic course websites). Let’s Encrypt will be fine with issuing and renewing that many certificates nowadays?

Yep! Caddy will throttle itself to not hit the rate limits, going as fast as 10 attempts per 10 seconds. See Automatic HTTPS — Caddy Documentation

1 Like

Well that’s magical! I’ll just disregard this wildcard approach and let Caddy do its thing!