yum upgrades for production use, this is the repository for you.
Active subscription is required.
Want to serve WebP with NGINX automatically? This comprehensive guide shows you exactly how to configure NGINX to serve WebP images to browsers that support them while gracefully falling back to JPEG/PNG for older browsers. Whether you’re looking for simple content negotiation or advanced on-the-fly conversion, we’ve got you covered.
WebP is a modern image format developed by Google that provides superior compression compared to traditional formats. By implementing automatic WebP serving, you can reduce image file sizes by 25-35% without losing visual quality.
Why Serve WebP with NGINX?
Images typically account for 50% or more of a webpage’s total size. When you serve WebP with NGINX, you get substantial benefits:
- 25-35% smaller file sizes compared to JPEG at equivalent quality
- 26% smaller than PNG with lossless compression
- Support for transparency (like PNG) and animation (like GIF)
- Native browser support in Chrome, Firefox, Edge, Safari, and Opera
The challenge is that some older browsers don’t support WebP, meaning you need to serve the original JPEG/PNG as a fallback. NGINX makes this seamless with content negotiation.
How to Serve WebP: Understanding Content Negotiation
Modern browsers advertise their WebP support through the Accept HTTP request header:
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
When NGINX receives a request for an image, it checks this header and can serve WebP when supported, or fall back to JPEG/PNG otherwise. This technique is called content negotiation.
The Critical Vary Header
When you serve WebP with NGINX using content negotiation, you must include the Vary: Accept response header. This header tells CDNs and browser caches that the response varies based on the Accept request header.
Without Vary: Accept:
- A Chrome user requests
image.jpg - Your CDN caches the WebP version
- An old Safari user requests
image.jpg - The CDN serves the cached WebP β broken!
With Vary: Accept, caches store separate versions for different Accept header values, ensuring correct content delivery.
In this post, we are going to set Vary header using the standard add_header directive that is built-in for NGINX. If you want to go beyond that and for flexible setting, use more_set_headers whenever possible.
WebP File Naming Conventions
There are two common approaches for WebP file naming:
1. Suffix-Based Naming (Recommended)
image.jpg β image.jpg.webp
photo.png β photo.png.webp
This approach avoids filename collisions. Consider this scenario with extension replacement:
logo.png β logo.webp
logo.jpg β logo.webp β Collision!
2. Extension Replacement
image.jpg β image.webp
photo.png β photo.webp
While simpler, this method risks overwriting files when you have identically named images with different extensions.
Method 1: Serve WebP with NGINX Using Pre-Generated Images
If you pre-generate WebP versions during deployment, this is the most efficient approach. NGINX simply checks if a .webp version exists.
NGINX Configuration to Serve WebP
Add this to your http block in /etc/nginx/nginx.conf:
http {
# Map WebP support based on Accept header
map $http_accept $webp_suffix {
default "";
"~*webp" ".webp";
}
# ... rest of http config
}
Then in your server block:
server {
listen 80;
server_name example.com;
root /var/www/html;
# Serve WebP when available
location ~* \.(png|jpe?g)$ {
add_header Vary Accept;
try_files $uri$webp_suffix $uri =404;
}
}
How This Configuration Works
- User requests
/images/photo.jpg - If browser supports WebP,
$webp_suffix=.webp try_fileschecks for/images/photo.jpg.webpfirst- If found, serves WebP; otherwise serves original JPEG
Vary: Acceptheader ensures proper caching
Generating WebP Images During Deployment
For best performance, convert images to WebP during your deployment process. On Rocky Linux 9, AlmaLinux 9, or RHEL 9, use ImageMagick:
# Install ImageMagick with WebP support
sudo dnf install ImageMagick
# Convert a single image
convert image.jpg -quality 80 image.jpg.webp
# Batch convert all JPEG files in a directory
find /var/www/html/images -name "*.jpg" -exec sh -c '
convert "$1" -quality 80 "${1}.webp"
' _ {} \;
# Batch convert all PNG files
find /var/www/html/images -name "*.png" -exec sh -c '
convert "$1" "${1}.webp"
' _ {} \;
You can add this to your CI/CD pipeline:
#!/bin/bash
# deploy-webp.sh - Generate WebP versions before deployment
IMAGE_DIR="/var/www/html/images"
find "$IMAGE_DIR" -type f \( -name "*.jpg" -o -name "*.jpeg" -o -name "*.png" \) | while read img; do
webp_file="${img}.webp"
# Only convert if WebP doesn't exist or is older than source
if [ ! -f "$webp_file" ] || [ "$img" -nt "$webp_file" ]; then
echo "Converting: $img"
convert "$img" -quality 82 "$webp_file"
fi
done
Method 2: Serve WebP with NGINX WebP Module (On-the-Fly)
For dynamic environments where pre-generating images isn’t practical, the NGINX WebP module converts JPEG images to WebP on demand.
Installation on Rocky Linux / AlmaLinux / RHEL 9
# Install GetPageSpeed repository
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
# Install NGINX with WebP module
sudo dnf install nginx nginx-module-webp
Configuration
Load the module at the top of /etc/nginx/nginx.conf:
load_module modules/ngx_http_webp_module.so;
Then configure your image location:
server {
listen 80;
server_name example.com;
root /var/www/html;
location ~* "\.jpg$" {
add_header Vary Accept;
webp;
}
}
How the WebP Module Works
The module intercepts JPEG requests and:
- Checks if the browser accepts WebP (via
Acceptheader) - Converts the JPEG to WebP using
cwebpand saves it asimage.jpg.webp - Returns the WebP image with proper
Content-Type - Falls back to JPEG if WebP not supported
- On subsequent requests, serves the cached
.webpfile directly
Important: The WebP module does not automatically add the Vary: Accept header. You must add it manually for proper CDN caching:
location ~* "\.jpg$" {
add_header Vary Accept;
webp;
}
Performance Considerations
On-the-fly conversion uses CPU resources. The WebP module caches converted images as .webp files alongside the originals, so conversion only happens once per image. For high-traffic sites with CDN or reverse proxy setups, consider using Varnish Cache in front of NGINX.
Note: NGINX’s proxy_cache directive only works with proxy_pass to upstream backendsβit cannot cache static file or module output directly.
cwebp Dependency
The WebP module requires the cwebp tool to perform on-the-fly conversion. On Rocky Linux 9 / AlmaLinux 9, install it via EPEL:
sudo dnf install epel-release
sudo dnf install libwebp-tools
Without cwebp installed, the module will only serve pre-existing .webp files and fall back to JPEG when no WebP version exists.
Method 3: Serve WebP Using PageSpeed Module
The PageSpeed module provides comprehensive automatic image optimization, including WebP conversion, without modifying your HTML.
Installation
sudo dnf install nginx-module-pagespeed
Configuration
load_module modules/ngx_pagespeed.so;
http {
server {
listen 80;
server_name example.com;
root /var/www/html;
# Enable PageSpeed
pagespeed on;
pagespeed FileCachePath /var/cache/ngx_pagespeed;
# Enable image optimization with WebP support
pagespeed EnableFilters convert_jpeg_to_webp;
pagespeed EnableFilters convert_to_webp_lossless;
# Serve optimized resources
location ~ "\.pagespeed\.([a-z]\.)?[a-z]{2}\.[^.]{10}\.[^.]+" {
add_header "" "";
}
location ~ "^/pagespeed_static/" { }
location ~ "^/ngx_pagespeed_beacon$" { }
}
}
PageSpeed Benefits
- Automatic WebP conversion based on browser support
- Intelligent format selection β uses lossless WebP for sharp-edged images
- Image resizing to actual display dimensions
- Lazy loading integration
Comparison: Which Method to Serve WebP with NGINX?
| Feature | Pre-generated WebP | WebP Module | PageSpeed |
|---|---|---|---|
| CPU Usage | None at runtime | Moderate | Moderate |
| Flexibility | Low | Medium | High |
| Setup Complexity | Simple | Simple | Moderate |
| Additional Optimization | No | No | Yes |
| Cache Management | Manual | Automatic | Automatic |
Complete Example Configuration
Here’s a complete, example configuration to serve WebP with NGINX:
load_module modules/ngx_http_webp_module.so;
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;
events {
worker_connections 1024;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" webp=$webp_suffix';
access_log /var/log/nginx/access.log main;
# Performance
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
# Gzip - exclude images (already compressed)
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_vary on;
# WebP content negotiation
map $http_accept $webp_suffix {
default "";
"~*webp" ".webp";
}
server {
listen 80;
listen 443 ssl http2;
server_name example.com;
root /var/www/html;
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
# Static assets with WebP support
location ~* \.(png|jpe?g)$ {
add_header Vary Accept;
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri$webp_suffix $uri =404;
}
# On-the-fly WebP for uploads directory
location /uploads/ {
alias /var/www/uploads/;
location ~* "\.jpg$" {
add_header Vary Accept;
add_header Cache-Control "public, max-age=86400";
webp;
}
}
# Other static assets
location ~* \.(css|js|woff2?|ttf|eot|svg|ico)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
# Default location
location / {
try_files $uri $uri/ /index.php?$query_string;
}
}
}
In this example, user-dependent uploads directory relies on WebP images being generated on the fly, whereas everything else is supposed to be generated manually (your website deployment).
Testing Your WebP Implementation
Using curl to Verify
Test WebP delivery:
# Request with WebP support
curl -s -I -H "Accept: image/webp,*/*" https://example.com/images/photo.jpg | grep -E "Content-Type|Vary"
# Expected output:
# Content-Type: image/webp
# Vary: Accept
Test fallback:
# Request without WebP support
curl -s -I -H "Accept: image/jpeg,*/*" https://example.com/images/photo.jpg | grep -E "Content-Type|Vary"
# Expected output:
# Content-Type: image/jpeg
# Vary: Accept
Browser Testing
- Open Chrome DevTools (F12)
- Go to Network tab
- Filter by “Img”
- Check Response Headers for
Content-Type: image/webp
Troubleshooting Common Issues
WebP Not Being Served
Check these common causes:
- Map directive location β must be in
httpcontext, notserver - WebP file naming β ensure files use
.jpg.webpsuffix, not just.webp - File permissions β nginx must have read access
- Accept header β verify browser sends
image/webpin Accept header
CDN Serving Wrong Format
Ensure:
Vary: Acceptheader is included- CDN respects Vary headers
- Cache is purged after implementation
For more NGINX troubleshooting tips, check our server setup guides.
Performance Results
Real-world testing shows significant improvements when you serve WebP with NGINX:
| Image Type | JPEG Size | WebP Size | Reduction |
|---|---|---|---|
| Photography | 245 KB | 178 KB | 27% |
| Graphics | 89 KB | 54 KB | 39% |
| Screenshots | 156 KB | 98 KB | 37% |
| Average | – | – | 34% |
Conclusion
Learning to serve WebP with NGINX dramatically improves web performance while maintaining compatibility with all browsers. Choose your implementation method based on your needs:
- Pre-generated WebP: Best for static sites and maximum performance
- NGINX WebP Module: Great for dynamic content with minimal setup
- PageSpeed Module: Ideal for comprehensive automatic optimization
Regardless of the method chosen, always include the Vary: Accept header and use suffix-based naming to avoid file conflicts. With proper implementation, you can reduce image bandwidth by 25-35% with no visible quality loss.
