Serving precompressed assets with Caddy has historically required some configuration gymnastics.

A good example can be found in a previous post of mine.

That all changes in Caddy 2.4.0

Native Precompressed Asset Serving§

As seen in the above-linked 2.4.0 release notes, Caddy added support for serving "precompressed sidecar files".

Unfortunately, they fail to point to a more useful section of their documentation that actually describes how to configure Caddy to serve said assets.

The real documentation lies under their file_server directive.

If you want the TL;DR version, here you go:

file_server {
    precompressed br zstd gzip
}

The above will handle an incoming request for an asset by analyzing the Accept-Encoding header provided by the client (browser) and searching for files ending in .br, .zst, and .gz, respectively.

And yes, the order that you specify in the precompressed field does matter, so pick wisely. If you aren't sure, just use what I have above. Once Zstandard has widespread browser adoption (it doesn't yet), and established dictionaries for compressing HTML/CSS/JS, then it will make more sense to prioritize zstd before br (Brotli compression). But we're unfortunately a few years out from then.

Note that this feature works wonderfully with file_server's index attribute. This means that if you use "clean urls" (without an ending index.html for example, like https://austindw.com/precompressed-assets-caddy/), precompressing your index.html files and serving them with this option will work correctly. This required extra configuration to get right using the old way.

Before and After§

Just to provide some context on why this is such a nice improvement let's look at the old configuration compared to the new configuration.

Before§

example.com {
    root * /path/to/dir

    file_server

    ### Precompression support
	@brotli {
	header Accept-Encoding *br*
	  file {
	    try_files {path}.br {path}/index.html.br {path}.html.br
	  }
	}
	handle @brotli {
	  header {
	    Content-Encoding br
	    Content-Type text/html
	  }
	  rewrite {http.matchers.file.relative}
	}

	@gzip {
	  header Accept-Encoding *gzip*
	  file {
	    try_files {path}.gz {path}/index.html.gz {path}.html.gz
	  }
	}
	handle @gzip {
	  header {
	    Content-Encoding gzip
	    Content-Type text/html
	  }
	  rewrite {http.matchers.file.relative}
	}

	@html {
	  file
	  path *.html */
	}
	header @html {
	  Content-Type text/html
	  defer
	}

	@css {
	  file
	  path *.css
	}
	header @css {
	  Content-Type text/css
	  defer
	}

	@js {
	  file
	  path *.js
	}
	header @js {
	  Content-Type text/javascript
	  defer
	}

	@svg {
	  file
	  path *.svg
	}
	header @svg {
	  Content-Type image/svg+xml
	  defer
	}

	@xml {
	  file
	  path *.xml
	}
	header @xml {
	  Content-Type application/xml
	  defer
	}

	@json {
	  file
	  path *.json
	}
	header @json {
	  Content-Type application/json
	  defer
	}
}

After§

example.com {
    root * /path/to/dir

    file_server {
        precompressed br zstd gzip
    }
}

Pretty dope improvement, no?

That's all for today - go forth and precompress.