Caddy is a popular web server written in Go, that has started to take over the old standard apache and nginx webservers. Maybe the most prominent feature that clearly sets Caddy apart is its automatic handling of Let’s Encrypt certificates, which makes hosting any sort of SSL-protected service behind a domain name you own essentially a zero effort task.

Caddy has nice features all around, and offers extensive customizability via external modules, which can freely extend its capabilities. The flip side is that adding a module to Caddy requires building a new binary, with the new dependency loaded at compile time.

As you may know, Gentoo’s package manager, Portage, is made to build all of its packages from source, with customizable compile-time options. Those are called USE flags, and are used to toggle on or off certain features. This would have been a perfect use case for Caddy on Gentoo, if the number of external modules available was less than a dozen. It is therefore both a blessing and a curse that hundreds of third-party extensions are available for Caddy.

The standard solution to this issue, which is ultimately faced by all package managers, is to move outside of packaged distribution and instead manually insert the binary (which is conveniently statically linked) into your system. Updates then have to be handled manually. Caddy makes building those binaries easy, either locally through xcaddy, or via a remote building server directly as a subcommand of the caddy CLI.

I personnally dislike this solution, partly because the binary then escapes the standard system update process, partly because you either end up with non-bespoke binaries or have to support a whole build system, but mostly because the gentoo ebuild is so close to providing what I’m looking for.

Portage allows users to provide a custom patch to apply before compiling the source code (so long as the ebuild allows it, which is almost always the case — it’s implicit). We could patch the src/caddy/main.go file to append our custom modules, this is exactly what packagers do, and this is exactly what the gentoo ebuild does for its two supported external modules. This approach is insufficient in itself: while the dependencies are added to the project, they were not downloaded by portage during the building process, and since networking is disallowed inside ebuilds, go cannot grab the new dependencies.

Fortunately, Portage also offers advanced hooking capabilities, which means every part of the building process can be adjusted and overwritten. In particular, we can turn off network sandboxing for one specific package by using package-specific environment declaration files. It looks like this.


www-servers/caddy disable-network-sandbox.conf

FEATURES="-network-sandbox"

With this, we’ve made it so that go can actually fetch the right dependencies. There is an additional hurdle to overcome though: since dependencies are preloaded by portage into a vendor directory, the go CLI adopts a no-download policy. This means we’ll have to actually hook into the ebuild to make go forcibly download the extra dependencies. Since we’re hooking directly into the ebuild, we might as well inject our custom dependency there, instead of using a patch, though both approaches will end up being equivalent (with the exception, perhaps, that a patch file is harder to maintain with updates, since as you’ll see, we’re going to exploit the mechanism already present in the ebuild to inject modules).


post_src_unpack() {
	echo ":: Custom hook: Adding custom modules"
  # Below is an explicit list of modules that I want in my Caddy setup.
  # We're hijacking the $MY_MODULES variable, which is a list of all the modules
  # that will get injected in src/caddy/main.go
	MY_MODULES="${MY_MODULES} github.com/caddy-dns/cloudflare"
	MY_MODULES="${MY_MODULES} github.com/mholt/caddy-webdav"
	echo ":: Custom hook: New list of modules"
	for moo in ${MY_MODULES}; do
		echo "   - ${moo}"
	done
}

post_src_prepare() {
	echo ":: Custom hook: Using go get on custom modules"
	for moo in ${MY_MODULES}; do
		ego get $moo # Fetch the dependency
	done
	echo ":: Custom hook: Calling go mod vendor"
	ego mod vendor # Rebuild the vendor directory
	echo ":: Custom hook: Done!"
}
# vim: set ft=bash:

Just rebuild caddy with emerge -1 www-servers/caddy and you’re good to go! You can check that the modules are actually loaded with

$ caddy list-modules
[...]

dns.providers.cloudflare
http.handlers.webdav

  Non-standard modules: 2