You want brotli on nginx. Maybe zstd too. You Google it. Every result walks you through cloning a repo, installing build-essential and a pile of -dev packages, downloading the exact nginx source that matches your installed version, running ./configure with flags you'll forget by tomorrow, and praying that make modules doesn't error out.
Then nginx updates. Your module won't load. Start over.
There's a better way. It takes 60 seconds per module.
Why brotli matters
Brotli is Google's general-purpose compression algorithm. On text-based content (HTML, CSS, JavaScript, JSON, SVG), it consistently produces responses 15-25% smaller than gzip at equivalent CPU cost. Chrome and Firefox added support in 2016, Safari followed in late 2017, and today every major browser handles it. It's not bleeding edge. It's table stakes.
Smaller responses mean lower bandwidth costs, faster page loads, and better Core Web Vitals scores. If you're still serving gzip-only in 2026, you're leaving performance on the table for no reason. And with zstd now supported by Chrome and Firefox (Safari is the holdout), there's a second modern algorithm worth considering (we'll cover zstd in detail after the brotli setup).
The only problem: nginx doesn't ship with either. You have to add the modules yourself.
The hard way (every tutorial on the internet)
Here's what the existing guides tell you to do. We're putting this here so you can appreciate what you're about to skip.
# Install build dependencies
sudo apt install build-essential libpcre2-dev zlib1g-dev libssl-dev cmake git
# Clone the brotli module source
git clone --recurse-submodules https://github.com/google/ngx_brotli.git
cd ngx_brotli && mkdir out && cd out
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \
-DCMAKE_C_FLAGS="-Ofast -flto -funroll-loops -ffunction-sections -fdata-sections -Wl,--gc-sections" ..
cmake --build . --config Release --target brotlienc
# Download the exact nginx source matching your installed version
cd /tmp
NGINX_VERSION=$(nginx -v 2>&1 | grep -oP '\d+\.\d+\.\d+')
wget https://nginx.org/download/nginx-$NGINX_VERSION.tar.gz
tar xzf nginx-$NGINX_VERSION.tar.gz
cd nginx-$NGINX_VERSION
# Get the configure flags your nginx was built with
nginx -V 2>&1 | grep 'configure arguments' | sed 's/configure arguments: //'
# Copy all those flags, then add --add-dynamic-module=...
./configure <all-those-flags> --add-dynamic-module=/path/to/ngx_brotli
make modules
# Copy the .so files to the right place
sudo cp objs/ngx_http_brotli_filter_module.so /usr/lib/nginx/modules/
sudo cp objs/ngx_http_brotli_static_module.so /usr/lib/nginx/modules/
If you got the nginx version wrong, the module won't load. If you missed a configure flag, the binary is incompatible. If you forgot --recurse-submodules when cloning, the build fails with cryptic errors. And every single nginx update means you do this again, because the dynamic module ABI changes between versions.
We've watched people spend 30 minutes on this. We've watched people spend an afternoon.
The easy way
sudo apt install nginx-module-brotli
That's it. One command. The module loads automatically. No build dependencies on your production server. No source code. No cmake. No version matching. When nginx updates, the module rebuilds automatically in our CI pipeline, and apt upgrade keeps everything in sync.
If you haven't added the nginx-modules repository yet, here's the one-time setup:
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://apt.blendbyte.net/nginx/blendbyte-archive-keyring.gpg \
| sudo tee /etc/apt/keyrings/blendbyte.gpg >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/blendbyte.gpg] https://apt.blendbyte.net/nginx $(lsb_release -cs) main" \
| sudo tee /etc/apt/sources.list.d/blendbyte.list
sudo apt update
sudo apt install nginx-module-brotli
This requires nginx from nginx.org, not the version in Debian or Ubuntu's default repos. If you're running the distro-bundled nginx, you'll need to switch to nginx.org's packages first. The installation guide covers this.
Configuring brotli in nginx
Once the module is installed, add the brotli directives to your http {} block:
http {
# Enable brotli compression
brotli on;
brotli_comp_level 6;
brotli_types
text/html
text/css
text/plain
text/xml
application/javascript
application/json
application/xml
application/rss+xml
application/atom+xml
image/svg+xml
font/truetype
font/opentype
application/vnd.ms-fontobject;
# Keep gzip as a fallback for clients that don't support brotli
gzip on;
gzip_vary on;
gzip_types text/css application/javascript application/json image/svg+xml;
}
Test the config and reload:
sudo nginx -t && sudo systemctl reload nginx
A few notes on those settings:
brotli_comp_level 6 is the sweet spot for dynamic content. Levels 1-4 are faster but compress less. Levels 10-11 produce the smallest output but burn significantly more CPU. If you're compressing on the fly (not pre-compressed static files), stay at 4-6. Save level 11 for pre-compression with nginx-module-brotli-static.
brotli_types should list every text-based MIME type your server returns. Don't include binary formats like images or videos. They're already compressed, and brotli will waste CPU trying to squeeze water from a stone.
Keep gzip enabled alongside brotli. nginx will serve brotli to clients that send Accept-Encoding: br and fall back to gzip for everything else. The two don't conflict.
Verifying it works
Check the response headers with curl:
curl -s -I -H "Accept-Encoding: br" https://yoursite.com | grep -i content-encoding
You should see:
content-encoding: br
If you see gzip instead, either the module isn't loaded, the brotli_types list doesn't include the content type of that response, or the response is too small to be worth compressing (nginx skips compression for very small responses).
You can also check in your browser's dev tools. Open the Network tab, click any text resource, and look at the Content-Encoding response header. Chrome, Firefox, and Safari all show this.
Pre-compressed static files with brotli-static
Dynamic brotli compression works great, but there's an even better option for static files: pre-compress them at build time and serve the compressed version directly. Zero CPU cost at request time.
Install the static module:
sudo apt install nginx-module-brotli-static
Add to your nginx config:
brotli_static on;
Now nginx will check for a .br version of any requested file before compressing on the fly. If style.css.br exists alongside style.css, nginx serves the pre-compressed version with no CPU overhead.
Pre-compress your static assets during your build process:
# Pre-compress at maximum quality (level 11, fine for offline compression)
find /var/www/html -type f \( -name "*.css" -o -name "*.js" -o -name "*.html" -o -name "*.svg" -o -name "*.json" \) \
-exec brotli --best --keep {} \;
This gives you level 11 compression (the absolute best ratio) without any runtime CPU penalty. Best of both worlds.
Zstd: the other compression algorithm you should know about
Zstandard (zstd) is Meta's compression algorithm, and it's worth understanding even if brotli is your default. The headline number: zstd compresses roughly 40% faster than brotli at comparable ratios, and matches gzip compression ratios at significantly higher speed. Decompression speeds are similar across all three algorithms. Where zstd wins is server-side CPU: for responses that get compressed on the fly (API responses, server-rendered HTML, dynamic JSON), your server spends less time compressing each response.
Browser support
Chrome and Firefox support it. Safari doesn't. That's the honest picture.
- Chrome 123 (March 2024):
Accept-Encoding: zstd - Firefox 126 (May 2024):
Accept-Encoding: zstd - Safari: Not supported as of Safari 18. No timeline from Apple.
That means roughly 70-75% of browser traffic can receive zstd today (varies by audience). Safari users get brotli or gzip instead, which is fine since nginx negotiates automatically based on Accept-Encoding. You're not breaking anything for Safari users by enabling zstd. You're just not serving it to them.
Installing zstd on nginx
Same as brotli. One command, same repository:
sudo apt install nginx-module-zstd
Configuring zstd in nginx
Add to your http {} block, alongside your existing brotli and gzip config:
http {
# Zstd compression
zstd on;
zstd_comp_level 3;
zstd_types
text/html
text/css
text/plain
text/xml
application/javascript
application/json
application/xml
application/rss+xml
application/atom+xml
image/svg+xml
font/truetype
font/opentype
application/vnd.ms-fontobject;
# Brotli compression
brotli on;
brotli_comp_level 6;
brotli_types
text/html
text/css
text/plain
text/xml
application/javascript
application/json
application/xml
application/rss+xml
application/atom+xml
image/svg+xml
font/truetype
font/opentype
application/vnd.ms-fontobject;
# Gzip fallback
gzip on;
gzip_vary on;
gzip_types text/html text/css text/plain text/xml application/javascript
application/json application/xml image/svg+xml;
}
zstd_comp_level 3 is the sweet spot for dynamic content. Zstd's level scale is different from brotli's: level 3 already gives you compression ratios close to gzip-9, but at much higher speed. Levels above 6 start trading noticeable CPU for diminishing returns on dynamic content.
Verifying zstd
curl -s -I -H "Accept-Encoding: zstd" https://yoursite.com | grep -i content-encoding
You should see:
content-encoding: zstd
Which algorithm for what?
Here's the practical decision tree:
Pre-compressed static assets (CSS, JS, SVG): Use brotli at level 11 via brotli_static. Best compression ratio, and the CPU cost is paid once at build time, not on every request. You can also pre-compress with zstd via nginx-module-zstd-static (zstd --ultra -22 style.css -o style.css.zst), but brotli still wins on ratio for text content at maximum levels.
Dynamic content (HTML, JSON, API responses): Zstd at level 3 compresses significantly faster than brotli at comparable ratios, which means less CPU per request on your server. Brotli at level 4-6 produces slightly smaller output but takes more server-side CPU to do it. For high-traffic servers where CPU is the bottleneck, zstd pulls ahead. Note that Safari users won't receive zstd (they'll get brotli or gzip instead), so you'll want both enabled.
Universal fallback: Gzip. Always keep it on. Some clients, bots, and CDN edge nodes still only speak gzip.
The pragmatic setup: Run all three. nginx will negotiate the best option based on what the client sends in Accept-Encoding. Chrome and Firefox get zstd or brotli, Safari gets brotli, older clients get gzip. You don't have to choose, and you're not penalizing anyone.
Migrating from Sury's brotli module
If you had libnginx-mod-http-brotli-filter from the Sury nginx repository, our package is a drop-in replacement. Apt handles the swap automatically:
sudo apt install nginx-module-brotli
The Replaces and Conflicts declarations take care of the transition. See the full migration guide for details.
All the modules, one repository
The compression modules (brotli filter, brotli static, zstd, zstd static) are four of 12 prebuilt modules we maintain for Debian and Ubuntu. The rest cover security (modsecurity), geolocation (geoip2), headers (headers-more), and more. All free, all open source, all updated within 24 hours of every nginx stable release.
Your full compression stack is one line:
sudo apt install nginx-module-brotli nginx-module-brotli-static nginx-module-zstd nginx-module-zstd-static
If you're managing nginx servers and tired of compiling modules by hand, check out the full list.
Need help optimizing your nginx setup?
Compression is one piece of the performance puzzle. We tune nginx for production workloads across dozens of client environments, from solo Laravel apps to high-traffic SaaS platforms. If you want help with compression, caching, or nginx performance in general, talk to us.