Image Optimizer
An imgix/Cloudinary-style image service in a single Go binary — on-the-fly resize/convert/compress, HMAC-signed URLs, a three-tier cache, and objective quality metrics.
The problem
Developers and teams want a Cloudinary-style workflow — modern formats, responsive picture sets, colour-accurate output — without a SaaS bill or vendor lock-in. Image Optimizer runs as one Go binary plus a one-command Docker stack, distinguished by real colour management and objective, measured quality on every result.
Challenges
On-the-fly transforms without re-processing
Generating an image variant on every request would burn CPU re-encoding the same thing over and over. Each unique variant is built exactly once and then served from a three-tier cache (CDN in front, an in-memory cache, and a persistent store behind it). When many requests ask for the same not-yet-built variant at once, they're collapsed into a single build instead of a stampede, and requested widths snap to a fixed ladder so the number of distinct variants — and cache entries — stays bounded.
Tamper-proof signed URLs
An open image-transform endpoint is an abuse magnet: anyone could request endless one-off variants and force an expensive encode each time. Every delivery URL is therefore cryptographically signed over its path and parameters, with parameter order normalized so equivalent URLs verify identically, an optional expiry baked into the signature, and constant-time verification that rejects anything unsigned or altered.
Bounding image-engine CPU and memory
The underlying image library is powerful but native and memory-hungry, so unbounded concurrency could exhaust the machine. Simultaneous encodes are capped by a semaphore, the engine is configured for predictable low-memory operation, and upload sizes are limited, keeping the service stable under load rather than letting a burst of large images take it down.
Colour fidelity over naive pipelines
Most image pipelines assume every image is plain sRGB and silently shift the colours of anything wider-gamut. This one reads each image's embedded colour profile (Adobe RGB, Display P3, CMYK) and converts it correctly to sRGB before resizing or encoding, and it fixes orientation from photo metadata. An inspection endpoint even quantifies the difference, reporting how far a naive, profile-ignoring render drifts from the colour-managed one.
Implementation
Shared transform pipeline
A single core routine runs the full sequence — decode, auto-rotate, colour-convert, resize and fit, encode — offering multiple fit strategies (centre crop, content-aware crop, or contain without upscaling) and modern output formats with metadata stripped. It knows nothing about HTTP or storage, so both the live server and the batch processor reuse exactly the same code.
Visually-lossless auto-quality
Rather than picking a fixed quality number, the encoder searches for the smallest file whose visual similarity to the original stays above a chosen threshold. The similarity and error metrics are computed directly, so every result carries objective, measured quality figures rather than a guess.
Tiered cache and storage abstraction
A size-bounded in-memory cache sits in front of a pluggable persistent store, with local-disk and S3-compatible implementations behind one interface, and original images stored separately. A lookup walks memory, then the persistent tier, then builds on miss — and every response reports which tier served it, so cache behaviour is observable.
Signed delivery and responsive generation
The delivery endpoint picks the best format the browser accepts, sets long-lived immutable caching with proper revalidation, and never returns a re-encode larger than the source. Companion endpoints hand back a signed (optionally time-limited) URL and a full set of signed URLs across the width ladder, ready to paste straight into responsive image markup.
Developer API and self-hosting stack
A keyed developer API is protected by hashed API keys and rate limiting per key or per IP. A one-command container stack wires a CDN cache in front of the app in front of object storage, provisioning the storage bucket automatically on boot, so the whole production-shaped topology runs locally exactly as it would in production.
Why this stack
- Go
- A single static binary serves the UI, API, and image delivery, and its concurrency tools are what the request-coalescing, rate-limiting caching design is built on.
- libvips
- A streaming, low-memory image engine with fast thumbnailing, content-aware cropping, and proper colour-profile handling — the exact capabilities the product is built around.
- S3-compatible storage
- One storage client covers AWS, self-hosted MinIO, and other providers, for both originals and the shared variant cache.
- SvelteKit
- Compiles to a static dashboard and playground the Go binary serves directly — a rich UI with no separate runtime in production.
- Docker Compose
- Ships the whole CDN → app → storage topology as a single command, so the caching architecture is reproducible and production-shaped from the first run.
What it does
- AVIF/WebP/JPEG/PNG with Accept-based content negotiation
- Content-aware smart crop and EXIF auto-orientation
- ICC→sRGB colour management with a ΔE inspector
- Visually-lossless auto-quality with SSIM/PSNR/RMSE/ΔE metrics
- HMAC-signed, optionally time-boxed URLs with immutable caching and 304s
- Responsive picture/srcset generation, LQIP placeholders, batch processing, and a keyed developer API