HBMHBM Rocket
ALL POSTS
INFRASTRUCTURE · BLOG

Self hosted imgproxy with Cloudflare in front

2026-04-0811 MIN READ

BunnyCDN Optimizer is fine. Cloudflare Polish is fine. Both cost money per zone, per month, with bandwidth meters that creep up as your fleet grows. There is a cheaper, equally effective option that runs on a $5 VPS: self hosted imgproxy behind Cloudflare Free.

Why imgproxy

imgproxy is a single open source binary written in Go. It receives requests like /resize:fit:800:0/format:webp/plain/https://origin.com/photo.jpg, fetches the source, resizes and converts it via libvips, and responds with the optimized output. It supports AVIF, WebP, JPEG, PNG, GIF in and out. It signs URLs to prevent abuse. It runs in 30 MB of RAM.

Why Cloudflare in front

Resizing is expensive (libvips is fast but still spends CPU). Cloudflare caches the responses indefinitely at the edge. The first request to a unique width or format runs imgproxy. Every subsequent request hits the cache. In production we see 99 percent edge cache hit rates, which means the imgproxy origin works once per image variant per cache eviction.

Architecture

Browser
  ↓ HTTPS
Cloudflare (proxied DNS, "Cache Everything" rule on /resize/* path)
  ↓
Caddy (reverse proxy, Let's Encrypt cert, public hostname)
  ↓
imgproxy (Docker, port 8081, signed URLs)
  ↓ HTTP
WordPress origin (/wp-content/uploads/...)

The Docker setup

docker run -d --name imgproxy --restart=unless-stopped \
  -p 127.0.0.1:8081:8080 \
  -e IMGPROXY_KEY=$(openssl rand -hex 32) \
  -e IMGPROXY_SALT=$(openssl rand -hex 32) \
  -e IMGPROXY_USE_ETAG=true \
  -e IMGPROXY_AUTO_WEBP=true \
  -e IMGPROXY_AUTO_AVIF=true \
  -e IMGPROXY_QUALITY=82 \
  -e IMGPROXY_MAX_SRC_RESOLUTION=50 \
  darthsim/imgproxy:v3.27

Two minutes from docker run to a working server. The key and salt are how URLs are signed; without them, anyone could resize anything through your server.

Caddy in front

img.example.com {
    encode zstd gzip
    reverse_proxy 127.0.0.1:8081
    header Cache-Control "public, max-age=31536000, immutable"
}

Two essential headers. encode (zstd preferred, gzip fallback) cuts the payload by another 10 to 20 percent over imgproxy's already optimized output. Cache-Control: immutable tells Cloudflare and the browser the response will never change for this URL. With signed URLs, this is a true statement (the URL encodes the parameters; same URL always means same output).

Cloudflare cache rule

In your Cloudflare zone, create a Cache Rule:

  • If hostname is img.example.com
  • Then cache eligibility: eligible for cache
  • Edge TTL: 1 month, override origin
  • Browser TTL: respect existing headers (1 year via the immutable header above)

That is it. The first request per unique URL runs imgproxy, which runs once and caches at the edge. Every subsequent request hits the edge.

Generating signed URLs from PHP

imgproxy uses HMAC SHA256 over the path. Sign per request from your WordPress plugin or template:

function imgproxy_url($source, $width, $format = 'webp') {
  $key  = hex2bin(IMGPROXY_KEY);
  $salt = hex2bin(IMGPROXY_SALT);
  $encoded = rtrim(strtr(base64_encode($source), '+/', '-_'), '=');
  $path = "/rs:fit:$width:0/{$encoded}." . $format;
  $sig  = rtrim(strtr(base64_encode(hash_hmac('sha256', $salt . $path, $key, true)), '+/', '-_'), '=');
  return 'https://img.example.com/' . $sig . $path;
}

// In a template:
$url = imgproxy_url('https://example.com/wp-content/uploads/photo.jpg', 800);
echo '<img src="' . esc_url($url) . '" loading="lazy" alt="" />';

What it costs

Per month, for any number of zones and any reasonable bandwidth:

  • VPS: $5 to $10 (already running for other things in our case, so $0 incremental)
  • Cloudflare: $0 (Free plan, unmetered cache hits)
  • Domain: about $1 (subdomain of an existing domain, no charge)

For comparison, BunnyCDN Optimizer is roughly $9.50 per month per zone plus $0.005 per GB. Cloudflare Polish is $25 per month minimum.

At very high traffic (10 million requests per day) the imgproxy origin starts becoming the bottleneck on cache miss. At that scale, run two imgproxy containers behind a load balancer or move to Cloudflare Images. Below that, this setup will not break a sweat.

What HBM Rocket adds

The integration runs imgproxy on the same VPS as the control plane. The plugin auto rewrites WordPress image URLs through imgproxy, generates signed URLs server side, and tunes the quality and format settings per site based on the audit history. You configure nothing.

Self hosted imgproxy with Cloudflare in front | HBM Rocket