Precompressed Assets with Caddy
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.