Jun 17, 20268 min read/2026/06/17/imgproxy-the-image-layer-my-fleet-was-missing/

imgproxy: Getting Back the One Thing I Missed from Appcelerator Titanium

The problem I'm about to describe is one I've been carrying around for more than a decade:
getting a mobile app and a web app to share the same images — the same photos, but each
delivered at the right size and format for the screen asking for it. It sounds trivial. It is not,
and it has quietly cost me real time across every cross-platform project I've ever shipped.

The Appcelerator Titanium days

The first time I had this solved was back when I built mobile apps with Appcelerator Titanium
— the cross-platform framework that let you write one JavaScript codebase and ship native iOS and
Android apps. (Appcelerator was acquired by Axway in 2016; Titanium lives on today as an
open-source project under the TiDev community.) What made it click for me wasn't just the SDK — it
was Appcelerator Cloud Services, the backend that came with it. Among a pile of
backend-as-a-service features, it handled photos: your app uploaded an image once, and the
service stored it and handed back properly sized versions on demand. My mobile app and my web app
could point at the same photo and each get a sensible variant. I didn't have to think about it.

Then, like a lot of all-in-one platform services, it went away. The convenient capability had
been welded to a proprietary backend, and when the backend's priorities changed, the capability
left with it. I was back to square one — and back to the realization that the nice part (one
photo, many right-sized variants, shared across platforms) is exactly the part you don't want to
depend on a single vendor for.

Square one, for years

Here's the confession part: in the years since, across a fleet of apps that all accept image
uploads — vendor photos, avatars, attachments, galleries — most of them never actually processed
images at all.
They accepted an upload, dropped the raw file on blob storage, and handed the exact
same bytes back. A vendor snaps a 4 MB, 4032×3024 JPEG on their phone; it goes up; and it gets
served, untouched, to a 400-pixel feed card on a phone over mobile data — and to the web app's
gallery thumbnail, also untouched.

The "right" fix in each app is the same boring, easy-to-postpone work: pull in an image library,
write resize/crop/format code, generate a few sizes on upload, store them, invalidate them when the
design changes — and then do it again in the mobile client, or build an API contract so both
share it. Nobody wants to do that five times across a fleet. So it just… doesn't get done.

imgproxy is the tool that lets you do it zero times —
and, crucially, gets me back the Appcelerator capability without the Appcelerator lock-in.

What imgproxy is

imgproxy is a small, standalone HTTP server — written in Go, built on top of the excellent
libvips image library — that does one job: it resizes, converts, crops, and optimizes images
on the fly, in response to a URL. You don't pre-generate anything. You don't add image code to
your app. You point imgproxy at your source images and ask for what you want in the URL itself.

The mental model is: instead of your app owning image processing, you put a dumb, fast, stateless
service in front of your images and let it do the transformation per request. As the project puts
it, it "offloads all the image processing work from your application." That single-responsibility
framing is exactly why it fits a fleet — it's one service every app can share, and no app has to
know anything about libvips.

It's open source under the Apache 2.0 license, with a commercial imgproxy Pro tier for the
fancier stuff (more on that below).

How it actually works

You build a URL that encodes the processing you want, then the path to your source image. Roughly:

https://imgproxy.example.com/SIGNATURE/rs:fill:400:300/plain/https://storage.example.com/photo.jpg

Reading that left to right:

  • SIGNATURE — a signed token (more on this in a second).
  • rs:fill:400:300 — the processing options: resize, fill mode, 400×300.
  • plain/https://... — the source image, fetched by imgproxy at request time.

imgproxy fetches the original, runs it through libvips, and streams back a 400×300 result —
re-encoded, metadata stripped, and (if you ask, or set it as default) converted to a modern format
like WebP or AVIF. Change the design tomorrow and need 600×400 instead? Change the number in
the URL. There's nothing to regenerate and nothing to clean up.

Why this is the right shape for a fleet

This is the part that sold me — and it's the direct answer to my decade-old problem. When you run
several apps, the value isn't "imgproxy resizes images" — lots of things resize images. It's that
one deployment serves all of them, and none of them carry image code:

  • One URL contract, every platform. This is the Appcelerator capability, rebuilt the right way.
    My web app and my mobile app both build the same kind of imgproxy URL against the same source
    image — each just asks for the size it needs. No shared image library to keep in sync across a
    JavaScript web app and a native client; the "contract" is just the URL format. And because
    imgproxy can pick the output format from the client's Accept header, the same URL can hand AVIF
    to one device and WebP or JPEG to another.
  • No per-app image pipeline. Every app just stores the original and builds imgproxy URLs. The
    resize/crop/format logic lives in exactly one place.
  • Modern formats for free. This is the big mobile win. Serving WebP/AVIF instead of the
    original JPEG/PNG can cut payloads dramatically — and imgproxy can pick the format based on what
    the client accepts. That 4 MB phone photo becomes a right-sized, modern-format image without
    anyone touching app code.
  • Processing offloaded from the app servers. Image work is CPU- and memory-hungry; libvips is
    specifically chosen because it's fast and frugal. Pushing it to a dedicated service keeps it off
    your application servers, and you can scale it independently.

Supported formats out of the box include JPEG, PNG, GIF, WebP, AVIF, and JPEG XL; the Pro tier
adds things like PDF, animated GIF→MP4, and video thumbnails.

The security model (don't skip this)

The instinct when you first see imgproxy is "wait, it fetches arbitrary URLs and resizes them?" —
and yes, which is exactly why the security features matter. If you're going to expose image
processing to the internet, you need guardrails, and imgproxy ships them:

  • URL signing. That SIGNATURE in the path is an HMAC over the URL using a secret key. Without
    it, anyone could hit your imgproxy with millions of arbitrary sizes and turn it into a very
    effective way to burn your CPU (and bandwidth). Signing means only URLs your apps generate are
    valid. Turn this on.
  • Source restriction. You can limit which origins imgproxy is allowed to fetch from and cap
    source file sizes — so it only ever pulls from your own storage, not the whole web.
  • Image-bomb protection. It checks image dimensions before fully decoding, so a maliciously
    tiny file that decompresses to a 50000×50000 monster gets rejected instead of eating all your RAM.
  • Pre-download type checks and header-based authorization round it out.

For my case — images living on blob storage, served to public apps — signing plus source
restriction is the baseline I wouldn't deploy without.

Deploying it

It's a single container, and the happy path is genuinely one line:

docker run -p 8080:8080 -it ghcr.io/imgproxy/imgproxy:latest

A couple of things worth knowing before it goes anywhere real:

  • Put a CDN/cache in front. imgproxy processes per request; you do not want it recomputing the
    same thumbnail on every hit. A CDN (or even an nginx cache) in front means imgproxy does each
    transformation once and then mostly sleeps. This is the difference between "cheap" and "expensive."
  • It deliberately doesn't do HTTPS. By design — single responsibility again. TLS is the job of
    your reverse proxy or CDN, which you're putting in front anyway.
  • Configuration is environment variables. Keys, allowed sources, default format, size limits —
    all IMGPROXY_* env vars. (Which, given how I manage secrets, slots right into my usual setup.)

OSS vs Pro

The open-source build under Apache 2.0 covers the things most people actually need: resize, crop,
rotate, watermark, format conversion, metadata stripping, the security features. Pro is where
the smart stuff lives — object-detection-aware cropping, advanced compression and watermarking,
PDF/video support, and smarter automatic format selection. For a fleet of fairly normal "make this
upload web-friendly" needs, the OSS edition is plenty; Pro is there if and when smart cropping
becomes worth paying for.

Where I'm slotting it in

The plan for my fleet is the unglamorous, correct one: keep storing the original upload on blob
storage exactly as I do today, stop serving those originals directly, and route every image through
a single shared imgproxy behind a CDN. Apps stop owning image code entirely — they just build signed
URLs with the size and format they need. One service, every app, modern formats, and a 4 MB phone
photo never reaches a phone screen as 4 MB again.

That's the kind of small, does-one-thing-well tool worth telling people about — the image layer my
fleet was missing, and didn't need to be five separate copies of.