Back to blog

nginx Brotli and Zstd on Debian/Ubuntu: apt install, Not 30 Minutes of Compiling

10 min read
On this page

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.

Written by

Blendbyte

Blendbyte Team

We run what we write about. Production experience only, no theory.

Need help with your setup?

We build and run infrastructure for clients every day. If you need help with your server, cloud, or software setup, talk directly to the engineers who do the work.

Let's talk