Images / NGINX

How to Serve WebP Images Automatically with NGINX: Complete 2026 Guide

by ,


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth 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:

  1. A Chrome user requests image.jpg
  2. Your CDN caches the WebP version
  3. An old Safari user requests image.jpg
  4. 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:

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

  1. User requests /images/photo.jpg
  2. If browser supports WebP, $webp_suffix = .webp
  3. try_files checks for /images/photo.jpg.webp first
  4. If found, serves WebP; otherwise serves original JPEG
  5. Vary: Accept header 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:

  1. Checks if the browser accepts WebP (via Accept header)
  2. Converts the JPEG to WebP using cwebp and saves it as image.jpg.webp
  3. Returns the WebP image with proper Content-Type
  4. Falls back to JPEG if WebP not supported
  5. On subsequent requests, serves the cached .webp file 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

  1. Open Chrome DevTools (F12)
  2. Go to Network tab
  3. Filter by “Img”
  4. Check Response Headers for Content-Type: image/webp

Troubleshooting Common Issues

WebP Not Being Served

Check these common causes:

  1. Map directive location β€” must be in http context, not server
  2. WebP file naming β€” ensure files use .jpg.webp suffix, not just .webp
  3. File permissions β€” nginx must have read access
  4. Accept header β€” verify browser sends image/webp in Accept header

CDN Serving Wrong Format

Ensure:

  1. Vary: Accept header is included
  2. CDN respects Vary headers
  3. 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.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience β€’ Maintainer of GetPageSpeed RPM repository β€’ Contributor to open-source NGINX modules

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.