HTTP compression shrinks text responses (HTML, CSS, JavaScript, JSON, SVG) before they leave the server, which cuts transfer size and page load time with almost no effort. Gzip is supported by every client, and Brotli, which every modern browser supports over HTTPS, usually produces files 15-25% smaller than gzip at a similar CPU cost. In this tutorial you will measure a baseline, enable gzip and Brotli in either Nginx or Apache on Ubuntu 24.04, optionally serve precompressed files, and verify the result with curl.

Prerequisites

To follow this tutorial, you will need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
  • A non-root user with sudo privileges.
  • Nginx or Apache already installed and serving files from /var/www/html. Follow only the section for the web server you use.
  • For Brotli in real browsers: HTTPS on your site. Browsers only advertise br in Accept-Encoding over HTTPS, although curl can test it over plain HTTP.

Step 1 - Creating a test file and measuring the baseline

To see the effect of compression you need a realistic text file. Download the uncompressed Bootstrap stylesheet into the web root:

sudo curl -sSo /var/www/html/test.css https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.css

Now request it without compression and print the number of bytes transferred. The identity encoding tells the server not to compress:

curl -s -o /dev/null -H 'Accept-Encoding: identity' -w '%{size_download} bytes\n' http://localhost/test.css
281046 bytes

Keep this number: it is your baseline. The exact size can differ slightly between versions of the file.

Step 2 - Configuring gzip in Nginx

Skip to Step 5 if you use Apache.

Ubuntu's Nginx package already sets gzip on; in /etc/nginx/nginx.conf, but only for text/html, and the tuning directives are commented out. Declaring gzip on; again in another file would make Nginx fail with a "directive is duplicate" error, so edit the existing block instead:

sudo nano /etc/nginx/nginx.conf

Find the Gzip Settings section inside the http block and replace it with:

	##
	# Gzip Settings
	##

	gzip on;
	gzip_vary on;
	gzip_proxied any;
	gzip_comp_level 5;
	gzip_min_length 256;
	gzip_types
		text/plain
		text/css
		text/xml
		text/javascript
		application/javascript
		application/json
		application/xml
		application/rss+xml
		application/atom+xml
		application/manifest+json
		image/svg+xml
		font/ttf
		font/otf;

What each directive does:

  • gzip_vary on adds Vary: Accept-Encoding, so caches and CDNs store compressed and uncompressed copies separately.
  • gzip_proxied any also compresses responses to requests that came through a proxy or CDN.
  • gzip_comp_level 5 is a good balance. Levels above 6 cost noticeably more CPU for a very small gain.
  • gzip_min_length 256 skips tiny responses, where compression overhead is larger than the saving.
  • gzip_types lists the MIME types to compress. text/html is always compressed and must not be listed, otherwise Nginx logs a duplicate MIME type warning. Do not add images such as JPEG, PNG or WebP, or WOFF2 fonts: they are already compressed.

Test the configuration and reload Nginx:

sudo nginx -t && sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Check the headers of a gzip request. The command downloads the body to /dev/null and prints only the response headers:

curl -s -o /dev/null -D - -H 'Accept-Encoding: gzip' http://localhost/test.css | grep -iE 'content-encoding|vary'
Vary: Accept-Encoding
Content-Encoding: gzip

Step 3 - Adding Brotli to Nginx

Ubuntu 24.04 ships the Brotli module for Nginx as two packages in the universe repository: the filter module (compresses on the fly) and the static module (serves precompressed .br files). Install both, plus the brotli command line tool you will use in Step 4:

sudo apt update
sudo apt install libnginx-mod-http-brotli-filter libnginx-mod-http-brotli-static brotli

The packages drop load_module lines into /etc/nginx/modules-enabled/, so you do not need to load them by hand. Confirm they are there:

grep -h brotli /etc/nginx/modules-enabled/*.conf
load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;

Create a configuration file for Brotli. Files in /etc/nginx/conf.d/ are included inside the http block:

sudo nano /etc/nginx/conf.d/brotli.conf
brotli on;
brotli_comp_level 5;
brotli_min_length 256;
brotli_types
    text/plain
    text/css
    text/xml
    text/javascript
    application/javascript
    application/json
    application/xml
    application/rss+xml
    application/atom+xml
    application/manifest+json
    image/svg+xml
    font/ttf
    font/otf;

Brotli level 11 is meant for offline precompression, not for every request. Level 4 to 6 on the fly gives better compression than gzip at a comparable CPU cost. As with gzip, text/html is compressed by default and is not listed.

Reload Nginx and request the file with Brotli:

sudo nginx -t && sudo systemctl reload nginx
curl -s -o /dev/null -D - -H 'Accept-Encoding: gzip, deflate, br' http://localhost/test.css | grep -i content-encoding
Content-Encoding: br

When a client accepts both, Nginx prefers Brotli. Clients that only accept gzip still get gzip.

Step 4 - Serving precompressed files with Nginx (optional)

Compressing the same static file on every request wastes CPU. For assets that only change on deploy (CSS, JavaScript, SVG, fonts), you can compress them once at maximum level and let Nginx serve the ready-made .gz and .br files. The gzip_static module is built into Ubuntu's Nginx, and brotli_static comes from the package you installed in Step 3.

Generate the compressed copies next to the originals. gzip -k and brotli both keep the original file:

sudo find /var/www/html -type f \( -name '*.css' -o -name '*.js' -o -name '*.svg' -o -name '*.json' \) \
  -exec gzip -k -9 -f {} \; -exec brotli -q 11 -f {} \;
ls -l /var/www/html/test.css*
-rw-r--r-- 1 root root 281046 Sep 25 10:12 /var/www/html/test.css
-rw-r--r-- 1 root root  26840 Sep 25 10:15 /var/www/html/test.css.br
-rw-r--r-- 1 root root  35902 Sep 25 10:15 /var/www/html/test.css.gz

Enable static serving. Add these two lines to the end of /etc/nginx/conf.d/brotli.conf:

sudo nano /etc/nginx/conf.d/brotli.conf
gzip_static on;
brotli_static on;

Reload Nginx:

sudo nginx -t && sudo systemctl reload nginx

Nginx now looks for test.css.br or test.css.gz before compressing on the fly, and falls back to dynamic compression when no precompressed file exists. Remember to regenerate the .gz and .br files every time you deploy new assets, otherwise visitors will receive stale content. Run the find command from your deploy script or build pipeline.

Continue with Step 7 to measure the result.

Step 5 - Configuring gzip in Apache

Apache on Ubuntu enables mod_deflate by default, and /etc/apache2/mods-available/deflate.conf already compresses HTML, plain text, XML, CSS and JavaScript. Confirm the module is loaded:

sudo apache2ctl -M | grep -E 'deflate|brotli|filter'
 deflate_module (shared)
 filter_module (shared)

To also cover JSON, SVG and fonts and set a sensible compression level, replace the contents of the stock file:

sudo nano /etc/apache2/mods-available/deflate.conf
<IfModule mod_filter.c>
    AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript
    AddOutputFilterByType DEFLATE application/javascript application/json application/xml
    AddOutputFilterByType DEFLATE application/rss+xml application/atom+xml application/manifest+json
    AddOutputFilterByType DEFLATE image/svg+xml font/ttf font/otf
    DeflateCompressionLevel 5
</IfModule>

mod_deflate adds the Vary: Accept-Encoding header by itself and skips responses that already carry a Content-Encoding, so you do not need the old browser-sniffing rules found in many copied configurations.

Test and reload:

sudo apache2ctl configtest && sudo systemctl reload apache2
Syntax OK
curl -s -o /dev/null -D - -H 'Accept-Encoding: gzip' http://localhost/test.css | grep -iE 'content-encoding|vary'
Vary: Accept-Encoding
Content-Encoding: gzip

Step 6 - Adding Brotli to Apache

mod_brotli is part of the apache2 package on Ubuntu, so there is nothing to compile. You only need to enable it and tell Apache which types to compress.

Create the module's configuration file. Apache includes the files in mods-enabled/ in alphabetical order, so brotli.conf is read before deflate.conf. That order matters: whichever filter runs first compresses the response, and the other one skips it. Putting Brotli first means clients that accept both get Brotli.

sudo nano /etc/apache2/mods-available/brotli.conf
<IfModule mod_brotli.c>
    AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css text/javascript
    AddOutputFilterByType BROTLI_COMPRESS application/javascript application/json application/xml
    AddOutputFilterByType BROTLI_COMPRESS application/rss+xml application/atom+xml application/manifest+json
    AddOutputFilterByType BROTLI_COMPRESS image/svg+xml font/ttf font/otf
    BrotliCompressionQuality 5
</IfModule>

Enable the module and restart Apache so the new module is loaded:

sudo a2enmod brotli
sudo apache2ctl configtest && sudo systemctl restart apache2

Request the file advertising both encodings, like a browser does:

curl -s -o /dev/null -D - -H 'Accept-Encoding: gzip, deflate, br' http://localhost/test.css | grep -i content-encoding
Content-Encoding: br

If you get gzip here, the Brotli filter is running after DEFLATE. Check that the Brotli directives live in mods-available/brotli.conf and not in a file under conf-enabled/, which is loaded after the modules.

Step 7 - Measuring the result

Repeat the measurement from Step 1 with each encoding. This works the same for Nginx and Apache:

for enc in identity gzip br; do
  printf '%-9s' "$enc"
  curl -s -o /dev/null -H "Accept-Encoding: $enc" -w '%{size_download} bytes\n' http://localhost/test.css
done
identity 281046 bytes
gzip     36511 bytes
br       29470 bytes

In this example gzip cuts the stylesheet by about 87% and Brotli saves a further 19% over gzip. With precompressed files from Step 4 the Brotli number drops a little more because level 11 was used.

Finally, test from a browser against your HTTPS site: open the developer tools, go to the Network tab, reload, and check the content-encoding response header of your CSS and JavaScript files. You can also check from any machine with:

curl -s -o /dev/null -D - -H 'Accept-Encoding: gzip, deflate, br' https://your_domain/ | grep -i content-encoding

Troubleshooting

  • No Content-Encoding header at all. The response type is not in your type list, or the response is smaller than the minimum length. Check the real type with curl -sI http://localhost/file | grep -i content-type and add it to gzip_types / brotli_types or AddOutputFilterByType.
  • Responses from a backend (PHP-FPM, Node.js, a proxy) are not compressed in Nginx. Make sure gzip_proxied any; is set, and that the backend does not already send Content-Encoding. Compress in one place only.
  • Browsers never get Brotli, but curl does. Browsers only request br over HTTPS. Test against https://your_domain.
  • A CDN in front of the server. The CDN may recompress or strip encodings. Keep Vary: Accept-Encoding enabled and check the CDN's own compression settings.
  • High CPU after enabling compression. Lower gzip_comp_level / brotli_comp_level (or BrotliCompressionQuality) to 4, and precompress static assets as in Step 4.

Conclusion

Your web server now compresses text responses with Brotli for modern browsers and falls back to gzip for everything else, and you have measured the size reduction with curl. Next, consider adding long Cache-Control headers for static assets so compressed files are also cached by browsers, enabling HTTP/2 or HTTP/3 on your HTTPS virtual host, and benchmarking the server under load with tools such as ab or wrk to confirm the CPU cost of compression is acceptable.