Image Optimization Pipeline for Web Servers
An image optimization pipeline reduces bandwidth and improves page load times by converting images to modern formats (WebP, AVIF), compressing them, and resizing on demand. This guide covers building an automated image optimization pipeline on Linux web servers using Nginx, on-the-fly processing, CDN integration, and batch conversion scripts.
Prerequisites
- Ubuntu 20.04+/Debian 11+ or CentOS 8+/Rocky Linux 8+
- Nginx installed
- Root or sudo access
- At least 1 GB RAM for image processing operations
Installing Image Processing Tools
# Install core image tools
sudo apt update && sudo apt install -y \
imagemagick \
libvips-tools \
webp \
libjpeg-turbo-progs \
pngquant \
optipng \
gifsicle
# Install AVIF support (via libavif)
sudo apt install -y libavif-bin # Ubuntu 22.04+
# Install sqip/sharp via Node.js for advanced processing
npm install -g sharp-cli
# Verify installations
cwebp --version
convert --version | head -1
vips --version
For CentOS/Rocky:
sudo dnf install -y ImageMagick libwebp-tools pngquant gifsicle
WebP and AVIF Conversion Scripts
Create a script to convert images to WebP:
cat > /usr/local/bin/convert-to-webp.sh << 'SCRIPT'
#!/bin/bash
# Convert JPEG/PNG images to WebP format
# Usage: convert-to-webp.sh [directory] [quality]
DIR="${1:-.}"
QUALITY="${2:-80}"
CONVERTED=0
SKIPPED=0
find "$DIR" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" \) | \
while read -r FILE; do
WEBP="${FILE%.*}.webp"
if [ ! -f "$WEBP" ] || [ "$FILE" -nt "$WEBP" ]; then
if cwebp -q "$QUALITY" "$FILE" -o "$WEBP" 2>/dev/null; then
ORIG_SIZE=$(stat -c%s "$FILE")
NEW_SIZE=$(stat -c%s "$WEBP")
SAVING=$(( (ORIG_SIZE - NEW_SIZE) * 100 / ORIG_SIZE ))
echo "Converted: $FILE → $WEBP (${SAVING}% smaller)"
((CONVERTED++))
fi
else
((SKIPPED++))
fi
done
echo "Done. Converted: $CONVERTED, Skipped (up-to-date): $SKIPPED"
SCRIPT
chmod +x /usr/local/bin/convert-to-webp.sh
Create an AVIF conversion script:
cat > /usr/local/bin/convert-to-avif.sh << 'SCRIPT'
#!/bin/bash
# Convert images to AVIF format
# Requires: avifenc (libavif-bin) or ImageMagick with AVIF support
DIR="${1:-.}"
QUALITY="${2:-60}" # AVIF quality 0-63, lower = better quality
find "$DIR" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" \) | \
while read -r FILE; do
AVIF="${FILE%.*}.avif"
if [ ! -f "$AVIF" ] || [ "$FILE" -nt "$AVIF" ]; then
# Try avifenc first (better quality), fall back to ImageMagick
if command -v avifenc &>/dev/null; then
avifenc -q "$QUALITY" "$FILE" "$AVIF" 2>/dev/null && \
echo "Converted: $FILE → $AVIF"
else
convert "$FILE" -quality "$QUALITY" "$AVIF" 2>/dev/null && \
echo "Converted: $FILE → $AVIF"
fi
fi
done
SCRIPT
chmod +x /usr/local/bin/convert-to-avif.sh
Nginx On-the-Fly Image Serving
Serve WebP/AVIF to supporting browsers automatically via Nginx:
# Create a map for WebP support detection
# Add to the http {} block in /etc/nginx/nginx.conf
# In http {} block
map $http_accept $webp_suffix {
default "";
"~*webp" ".webp";
}
map $http_accept $avif_suffix {
default "";
"~*avif" ".avif";
}
In your server block:
server {
listen 443 ssl;
server_name www.yourdomain.com;
root /var/www/html;
# Try AVIF first, then WebP, then original
location ~* \.(jpe?g|png)$ {
# Check for AVIF support
set $img_uri $uri;
# Try serving AVIF for browsers that support it
location ~* \.(jpe?g|png)$ {
add_header Vary Accept;
try_files
$uri$avif_suffix
$uri$webp_suffix
$uri =404;
}
}
# Serve pre-generated WebP files
location / {
add_header Vary Accept;
try_files $uri$webp_suffix $uri $uri/ =404;
}
# Set long cache for optimized images
location ~* \.(webp|avif)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
A cleaner approach with a dedicated image server location:
server {
root /var/www/html;
location ~* \.(jpg|jpeg|png)$ {
add_header Vary Accept;
expires 30d;
# Serve WebP to supporting browsers
if ($http_accept ~* "webp") {
set $webp_path $document_root$uri.webp;
if (-f $webp_path) {
rewrite ^ $uri.webp last;
}
}
}
location ~* \.webp$ {
add_header Content-Type image/webp;
expires 30d;
add_header Cache-Control "public";
}
}
On-the-Fly Resizing with Nginx Image Filter
The ngx_http_image_filter_module resizes images on the fly:
# Install the module (usually included in nginx-extras)
sudo apt install -y nginx-extras
nginx -V 2>&1 | grep image_filter # Verify
server {
# Image resizing endpoint
# Usage: /resize/300x200/path/to/image.jpg
location ~ ^/resize/(\d+)x(\d+)/(.+)$ {
alias /var/www/html/$3;
image_filter resize $1 $2;
image_filter_jpeg_quality 85;
image_filter_webp_quality 80;
image_filter_buffer 20M;
add_header Cache-Control "public, max-age=86400";
}
# Thumbnail generation
location ~ ^/thumb/(\d+)/(.+)$ {
alias /var/www/html/$2;
image_filter resize $1 -; # Resize width, auto height
image_filter_jpeg_quality 80;
image_filter_buffer 10M;
}
}
For more advanced resizing, use ngx_small_light or proxy to a dedicated image server like Thumbor or imgproxy:
# Deploy imgproxy for production-grade on-the-fly processing
docker run -d \
--name imgproxy \
-p 8080:8080 \
-e IMGPROXY_KEY=your_hex_key \
-e IMGPROXY_SALT=your_hex_salt \
-e IMGPROXY_QUALITY=80 \
-e IMGPROXY_WEBP_COMPRESSION=80 \
-e IMGPROXY_AVIF_SPEED=8 \
darthsim/imgproxy:latest
Automated Batch Processing
Automate image optimization as part of your deployment pipeline:
cat > /usr/local/bin/optimize-web-images.sh << 'SCRIPT'
#!/bin/bash
# Full image optimization pipeline
DIR="${1:-/var/www/html}"
echo "=== Optimizing JPEG files ==="
find "$DIR" -name "*.jpg" -o -name "*.jpeg" | while read f; do
jpegoptim --max=85 --strip-all "$f"
done
echo "=== Optimizing PNG files ==="
find "$DIR" -name "*.png" | while read f; do
pngquant --force --quality=65-80 --output "$f" "$f"
done
echo "=== Generating WebP variants ==="
/usr/local/bin/convert-to-webp.sh "$DIR" 80
echo "=== Generating AVIF variants ==="
/usr/local/bin/convert-to-avif.sh "$DIR" 60
echo "=== Done ==="
du -sh "$DIR"
SCRIPT
chmod +x /usr/local/bin/optimize-web-images.sh
Set up inotify to process new uploads automatically:
sudo apt install -y inotify-tools
cat > /usr/local/bin/watch-uploads.sh << 'SCRIPT'
#!/bin/bash
WATCH_DIR="/var/www/html/uploads"
inotifywait -m -e close_write -e moved_to "$WATCH_DIR" | \
while read DIR EVENT FILE; do
case "${FILE##*.}" in
jpg|jpeg|png)
echo "Processing: $DIR$FILE"
cwebp -q 80 "$DIR$FILE" -o "${DIR}${FILE%.*}.webp"
jpegoptim --max=85 "$DIR$FILE" 2>/dev/null || true
;;
esac
done
SCRIPT
chmod +x /usr/local/bin/watch-uploads.sh
# Run as a systemd service
sudo tee /etc/systemd/system/image-watcher.service << 'EOF'
[Unit]
Description=Image Optimization Watcher
After=network.target
[Service]
ExecStart=/usr/local/bin/watch-uploads.sh
Restart=always
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable --now image-watcher
Lazy Loading Configuration
Nginx can inject lazy loading attributes into HTML responses:
# Use sub_filter to add lazy loading to images
location ~* \.html$ {
sub_filter '<img ' '<img loading="lazy" ';
sub_filter_once off;
sub_filter_types text/html;
}
Or configure it at the application level. For static sites, process with a script:
# Add loading="lazy" to all img tags in HTML files
find /var/www/html -name "*.html" -exec sed -i \
's/<img\([^>]*\) \(src\)/<img\1 loading="lazy" \2/g' {} \;
CDN Integration
After converting images locally, sync to a CDN origin:
# Sync optimized images to S3-compatible storage (CDN origin)
aws s3 sync /var/www/html/images/ s3://my-cdn-bucket/images/ \
--exclude "*" \
--include "*.webp" \
--include "*.avif" \
--include "*.jpg" \
--cache-control "max-age=31536000,public" \
--content-type "image/webp" \
--metadata-directive REPLACE
# Or use rclone for non-AWS CDNs
rclone sync /var/www/html/images/ mycdnremote:bucket/images/ \
--header-upload "Cache-Control: max-age=31536000"
Troubleshooting
WebP files not being served to Chrome:
# Verify Accept header detection in Nginx
curl -H "Accept: image/webp,*/*" -I https://yourdomain.com/image.jpg
# Should show: Content-Type: image/webp
Image filter module returning 415:
The image_filter_buffer may be too small for large images. Increase it:
image_filter_buffer 50M;
Batch conversion running out of memory:
Process in smaller batches and limit ImageMagick memory:
export MAGICK_MEMORY_LIMIT=256MB
export MAGICK_MAP_LIMIT=512MB
AVIF conversion very slow:
AVIF encoding is CPU-intensive. Use parallel processing:
find /var/www/html -name "*.jpg" | \
parallel -j4 'avifenc -q 60 {} {.}.avif'
Conclusion
A well-designed image optimization pipeline combining WebP/AVIF conversion, on-the-fly resizing, and lazy loading can reduce image payload by 50-80% compared to unoptimized JPEGs. Start with static WebP conversion and the try_files Nginx pattern for immediate gains, then add on-the-fly resizing via imgproxy or Nginx image filter as your needs grow. Automate the pipeline with inotify watchers to handle new uploads without manual intervention.


