No results found.

Dev tooling

Magento 2 Media Proxy

Serve missing Magento 2 product and CMS images from staging or production, so local and integration installs work without a pub/media sync.

Source on GitHub ↗Other ways to manage media

Serving modes
3
Placeholder fingerprint cached
24h
Failed lookups cached
5 min

A fresh local copy of a store has the database but not the images. Syncing pub/media is slow and eats disk, and skipping it leaves every product page full of placeholders. That gets worse across several large projects, and it is the first thing slowing down a new developer.

This module fills the gaps from an upstream environment, such as staging or production, as each image is requested. It is for development and integration environments only, and the README says so in bold.

Which mode should I use?

Proxy when the upstream is public, cache when you want local speed, stream when nothing should be written to disk.

ModeWhat it doesTrade-off
proxy302 redirect to the same path on the upstreamWrites nothing, but the browser fetches the image, so the upstream must be publicly reachable
cacheDownloads the file once, then serves the local copyCosts disk, improves TTFB, and keeps third-party image extensions happy
streamFetches through PHP on every request, stores nothingNo disk and credentials never leave the server, but each image costs a Magento bootstrap and an upstream round trip

Stream mode arrived in v2.0.0.

How do I install Media Proxy?

composer require samjuk/m2-module-media-proxy
php bin/magento module:enable SamJUK_MediaProxy && php bin/magento cache:flush
php bin/magento config:set --lock-env samjuk_media_proxy/general/enabled 1
php bin/magento config:set --lock-env samjuk_media_proxy/general/mode 'proxy'
php bin/magento config:set --lock-env samjuk_media_proxy/general/upstream_host 'https://www.example.com'

The default configuration does nothing until the module is enabled and given an upstream. Config is read at store scope, so on a multi-site install MAGE_RUN_CODE gives each website its own upstream.

An upstream behind HTTP basic auth works in cache and stream modes, with the password stored through Magento’s encrypted backend model. Proxy mode cannot present credentials, because the browser makes the request.

How does it spot a missing image?

It only acts on images that are genuinely missing. Magento routes a media request through pub/get.php only when the file is not on disk, and a plugin runs before Magento\MediaStorage\App\Media::launch() on that path.

Catalog cache segments (/cache/<hash>/) are stripped first. That hash comes from the image settings of whichever environment generated it, so it rarely matches across installs. Stripping it fetches the original, which Magento then resizes locally.

A Magento upstream answers a request for missing media with its own placeholder and a 200, which looks just like a real image. So on first use the module asks for a path that cannot exist and remembers the SHA-256 of the response for 24 hours. Anything matching it later counts as missing. Failed lookups are remembered for five minutes, and anything that fails falls through to Magento’s usual placeholder rather than an error.

Where does it fit?

Local and integration installs that start from an anonymised production database. If you would rather solve missing media below Magento, with a union file system, rsync or a remote drive, the managing media docs page covers those.

It has two limits. Cache mode never revalidates a file once it has it, and .webp and .avif derivatives from next-generation image modules are not resolved back to their source.

What went wrong

Version 1.x was broken in ways the 2.0.0 changelog lists in full:

  • It depended on an interface Magento never binds. It worked on stores where another module happened to declare a preference, and fataled on every media miss everywhere else.
  • An upstream that resolved back to the same install made cache mode recurse through PHP-FPM until the pool ran out. Upstream requests now carry an X-SamJUK-Media-Proxy header and are refused on the way back in.
  • Cache mode created a directory where the image should go, which left that image returning 404 for good.
  • Website-scope config was silently ignored.

The same release fixed security problems I should have caught first time. A host configured as https://user:pass@host put the password into every failure in var/log. Basic auth went out as a literal header, which curl replays across a cross-host redirect. And each request for a unique nonexistent path triggered an upstream fetch and a disk write, so cache mode could fill the disk and amplify traffic onto what is usually a production site.