How to Self-Host an Image Proxy with imgproxy
This guide extends Image CDNs and fetchpriority, part of Image & Media Optimization. Managed image CDNs resize, convert and cache images from a URL — convenient, but priced per transformation or per request, which becomes expensive for large catalogues or user-generated content. imgproxy is an open-source, libvips-based server that does the same job: you request /<signature>/rs:fit:800:0/f:avif/plain/s3://bucket/photo.jpg and it fetches the source, transforms it and returns the result. Put a CDN in front of it and each variant is computed once, then served from the edge.
Self-hosting shifts the cost from per-request fees to compute you run yourself, and gives full control over formats, quality and source locations. The responsibilities move too: you must sign URLs to prevent abuse, cap resource usage, cache correctly with Vary: Accept, and monitor the service like any other piece of production infrastructure.
Rapid Diagnosis
- Estimate volume. Count unique variants (sources × widths × formats) and monthly image requests; managed pricing scales with these.
- Check source locations. imgproxy can read from S3, GCS, Azure, local files or HTTP origins.
- Check existing URL patterns. A migration is easiest if templates already generate image URLs from one helper.
- Check CDN capabilities. You need cache keys that include the format decision, or URLs that encode the format explicitly.
Root Cause Analysis: Why Teams Self-Host
1. Cost at scale. Per-transformation pricing dominates for catalogues with millions of images and many variants.
2. Control. Custom quality per content type, specific encoders, and data residency requirements.
3. User-generated content. Batch conversion is impractical for continuous uploads; on-the-fly transformation handles it.
4. Vendor flexibility. Keeping transformation separate from the CDN makes it easier to change CDNs.
Step-by-Step Resolution
1. Run imgproxy with signing and limits
# docker-compose.yml
services:
imgproxy:
image: darthsim/imgproxy:latest
environment:
IMGPROXY_KEY: ${IMGPROXY_KEY} # hex-encoded signing key
IMGPROXY_SALT: ${IMGPROXY_SALT}
IMGPROXY_USE_S3: "true"
IMGPROXY_ENABLE_AVIF_DETECTION: "true" # choose AVIF from Accept
IMGPROXY_ENABLE_WEBP_DETECTION: "true"
IMGPROXY_MAX_SRC_RESOLUTION: "50" # megapixels
IMGPROXY_ALLOWED_SOURCES: "s3://product-images/"
IMGPROXY_AVIF_SPEED: "7"
IMGPROXY_FORMAT_QUALITY: "avif=55,webp=78,jpeg=80"
ports: ["8080:8080"]
# trade-off: AVIF_SPEED 7 encodes faster but produces larger files than slower
# speeds; with a CDN in front, misses are rare, so slower speeds may be affordable.
2. Generate signed URLs in templates
import { createHmac } from 'node:crypto';
const KEY = Buffer.from(process.env.IMGPROXY_KEY, 'hex');
const SALT = Buffer.from(process.env.IMGPROXY_SALT, 'hex');
export function imgUrl(src, width) {
const path = `/rs:fit:${width}:0/plain/${src}`;
const sig = createHmac('sha256', KEY).update(SALT).update(path).digest('base64url');
return `https://img.example.com/${sig}${path}`;
}
// trade-off: signing prevents arbitrary resize requests (a cheap DoS vector), but
// every URL must be generated server-side; clients cannot build new sizes.
3. Cache at the CDN with format awareness
With Accept-based detection, imgproxy returns Vary: Accept. Configure the CDN to normalise Accept into a small key (avif, webp, other), or encode the format in the URL instead (f:avif) and choose it in <picture> markup.
4. Monitor and scale
Track request rate, latency, memory and error rate. Scale horizontally behind a load balancer; set concurrency limits (IMGPROXY_WORKERS) close to the number of cores, since image processing is CPU-bound.
Verification
Request the same signed URL with different Accept headers and confirm the format changes (or that explicit f: URLs return the stated format). Check that unsigned or tampered URLs return 403. Confirm CDN hit ratio for image paths climbs above 95% after warm-up, and that origin requests to imgproxy are mostly first-time variants. Load-test misses to find how many concurrent transformations each instance sustains.
Worked Example: A Classifieds Platform
A classifieds platform received 400,000 photo uploads per day and served about 900 million image requests per month through a managed image CDN. Transformation charges had become the second-largest infrastructure cost. The team deployed imgproxy on four 8-core instances behind their existing CDN, with signed URLs, AVIF detection and an Accept-normalised cache key. CDN hit ratio settled at 97%, so imgproxy handled about 27 million transformations per month — roughly 10 per second on average, with peaks around 60. Monthly image costs fell by about 70%, p75 image response time from the edge was unchanged, and time to first image for newly uploaded listings improved slightly because transformation no longer queued at the vendor.
Capacity Planning for Cache Misses
The CDN absorbs nearly all traffic once warm, so capacity planning is about misses: new uploads, new sizes after a design change, and cache purges. Estimate peak misses per second, then measure transformation time for typical sources on your hardware (AVIF encoding dominates; a 2000px photo might take 200–600ms of CPU). A rough rule: instances × cores ÷ average seconds per transform gives sustainable misses per second. Keep headroom for purge storms — avoid purging all image paths at once, and pre-warm the most popular variants after a deploy that changes sizes. Origin shield or tiered caching in the CDN reduces duplicate misses from different edge locations to a single request.
Security Hardening Checklist
An image proxy fetches and decodes untrusted input, so treat it as an exposed service. Run it as a non-root container with a read-only filesystem and memory limits enforced by the orchestrator, not just by imgproxy's own settings. Restrict outbound network access to the source buckets, so a crafted URL cannot reach internal services. Keep the signing key and salt in a secret store and rotate them on a schedule, accepting both old and new keys during the overlap. Set IMGPROXY_MAX_SRC_FILE_SIZE and animation frame limits, and keep libvips current through regular image updates, since decoder vulnerabilities are the most common class of issue. Finally, alert on 4xx spikes for invalid signatures — they often indicate probing.
Common Mistakes
- Unsigned URLs. Anyone can request expensive transformations of any size.
- No source allow-list. imgproxy becomes an open proxy for fetching arbitrary URLs.
- Caching without considering Accept. Browsers without AVIF receive AVIF.
- No resolution limit. A malicious or huge upload can exhaust memory.
Edge Cases
Animated images. imgproxy can process animated GIF and WebP, but CPU cost multiplies by frame count; set frame limits.
SVG sources. Pass SVGs through unchanged rather than rasterising, unless you need a raster thumbnail.
Private images. Combine signed URLs with expiring CDN tokens for access-controlled content.
Smart cropping. Content-aware gravity options help thumbnails but cost more CPU; test on representative images.
FAQ
Is imgproxy free?
The open-source version is free. A paid Pro version adds features such as advanced format options and some smart processing capabilities.
Do I need a CDN in front of imgproxy?
Yes, for production traffic. imgproxy does not cache results itself; the CDN makes each variant a one-time cost.
How do I choose between Accept detection and explicit formats?
Accept detection keeps markup simple but requires format-aware CDN caching. Explicit formats in URLs with <picture> are cache-friendly everywhere at the cost of more markup.
What about thumbor or other proxies?
Thumbor and similar tools fill the same role. imgproxy is known for speed and low memory use thanks to libvips; evaluate on your images and operations needs.
Can imgproxy add client hint support?
Yes, it can use Width and DPR hints when enabled, though signing such dynamic sizes needs care. See the client hints guide.
How do I handle a deploy that changes image sizes?
New sizes produce new URLs and cache misses. Roll the change gradually or pre-warm popular variants to avoid a spike in transformations.
Related
- Fixing a slow LCP image behind an image CDN — latency problems on the miss path.
- Caching transformed images at the edge — cache keys and hit rates.
- Tiered caching and origin shield — reducing duplicate misses.