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
sudoprivileges. - 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
brinAccept-Encodingover HTTPS, althoughcurlcan 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 onaddsVary: Accept-Encoding, so caches and CDNs store compressed and uncompressed copies separately.gzip_proxied anyalso compresses responses to requests that came through a proxy or CDN.gzip_comp_level 5is a good balance. Levels above 6 cost noticeably more CPU for a very small gain.gzip_min_length 256skips tiny responses, where compression overhead is larger than the saving.gzip_typeslists the MIME types to compress.text/htmlis 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-Encodingheader at all. The response type is not in your type list, or the response is smaller than the minimum length. Check the real type withcurl -sI http://localhost/file | grep -i content-typeand add it togzip_types/brotli_typesorAddOutputFilterByType. - 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 sendContent-Encoding. Compress in one place only. - Browsers never get Brotli, but
curldoes. Browsers only requestbrover HTTPS. Test againsthttps://your_domain. - A CDN in front of the server. The CDN may recompress or strip encodings. Keep
Vary: Accept-Encodingenabled and check the CDN's own compression settings. - High CPU after enabling compression. Lower
gzip_comp_level/brotli_comp_level(orBrotliCompressionQuality) 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.
