Anyone who has inherited a mid-sized Magento 2 store knows the feeling: you open pub/media/catalog/product/ over SFTP, wait for the listing to load, and realize you're staring at tens of thousands of product photos that were uploaded straight from a DSLR and never touched. PageSpeed Insights is flashing red on LCP, your hosting bill has a suspicious line item for outbound bandwidth, and the "fix images" ticket has been sitting in the backlog for six months because nobody wants to be the person who breaks the media gallery.
I've been reaching for the same tool on these cleanups lately: ShortPixel Optimizer CLI, a single self-contained bash script that talks to the ShortPixel API and chews through a directory tree in parallel. No Composer package, no Magento module, no PHP extension to enable. It runs on bash and curl and nothing else, which matters a lot when the server is a locked-down managed host where you can't install anything globally.
This guide is specifically about using it on Magento 2: which folders actually matter, how to handle the image cache, when to convert to WebP, and how to wire it into cron so fresh uploads get crunched automatically going forward.
Why a CLI tool makes sense for Magento
Magento's media layout is genuinely awkward to optimize through the admin UI. Every product image you upload lives at pub/media/catalog/product/a/b/abcdef.jpg, sharded by the first two letters of the filename. On top of that, Magento generates a cache tree at pub/media/catalog/product/cache/<hash>/a/b/abcdef.jpg with resized variants for every theme image type, usually dozens per original. A fresh cache flush regenerates all of them.
The practical consequence: if you optimize the cache folder, a single bin/magento cache:flush or catalog reindex throws all your work in the bin. The only durable target is the source tree, the original uploads, with the cache folder excluded from the sweep. Magento will then regenerate the resized variants from the already-optimized originals, and those regenerations will inherit the smaller size.
That's exactly the shape of job ShortPixel Optimizer CLI is built for:
- It descends recursively through any directory you point it at.
- It keeps a
.splogstate file in every folder it processes, so re-runs skip files that are already done. - It copies every original to a mirrored backup tree before hitting the API.
- It dispatches work to a pool of parallel workers using a FIFO as a semaphore.
- It shows an analytics dashboard when it finishes (and can email it to you).
- The entire dependency list is bash and curl.
On a Magento server where half the usual tooling is missing and you don't have root, that last point is the one that seals it.
Getting it onto the server
SSH in as your deploy user and drop the repo somewhere outside pub/:
cd ~
git clone https://github.com/short-pixel-optimizer/shortpixel-sh.git
cd shortpixel-sh
chmod +x shortpixel-optimize.sh
The first invocation with no .env present launches an interactive setup wizard. It walks you through the API key, lossy/lossless/glossy compression mode, worker count, whether to enable backups and where, and an optional email address for reports. Every prompt has a sensible default, so you can mostly just hit Enter.
If you're provisioning this via Ansible or a deploy script and don't want an interactive prompt, skip the wizard by writing .env directly:
API_KEY=xxxxxxxxxxxxxxxxxxxxxxxx
LOSSY=1
CONCURRENCY=6
BACKUP_DIR=/home/deploy/sp-backups
EMAIL=devops@yourstore.com
The wizard won't run in a non-TTY context (cron, CI), it just prints a warning and exits, so you don't need to worry about automated installs hanging on a prompt.
Figuring out what to sweep
Before running anything, take a real filesystem-level snapshot of pub/media/catalog/. The script does its own backups, but production media is the one thing where you want redundancy on redundancy. rsync -a to a sibling folder, an LVM snapshot, whatever your ops flow prefers.
The folder you actually want to point the script at is:
pub/media/catalog/product
Category images live at pub/media/catalog/category and are usually a much smaller set, so I like to warm up there:
./shortpixel-optimize.sh -j 4 /var/www/magento/pub/media/catalog/category
A quick run on categories confirms the progress output, the dashboard, and whether your API key is actually working, all without committing to a multi-hour sweep of the full product catalog.
If the numbers look reasonable, it's time for the real run on the product tree. There's one important gotcha: skip cache/. Optimizing cached variants is wasted effort because Magento is going to regenerate them from sources anyway, so the cleanest flow is a three-step:
# 1. Wipe the image cache so it isn't in the sweep
rm -rf /var/www/magento/pub/media/catalog/product/cache
# 2. Sweep the originals
./shortpixel-optimize.sh -j 4 /var/www/magento/pub/media/catalog/product
# 3. Let Magento rebuild the cache from optimized sources
cd /var/www/magento && bin/magento catalog:images:resize
A few comments on the flags that matter for Magento specifically:
-
-j 4: four parallel workers is a decent starting point for most VPS-class machines. Internally the script hands out tokens through a named pipe, which means additional workers just wait their turn once the pool fills up, picking an absurdly high number won't DDoS you, but it also won't gain anything past your CPU count or ShortPixel's per-account limits. -
No
--overwriteon the very first run. The default behavior drops optimized copies into a siblingoptimized/folder inside whichever directory was being processed, which gives you room to eyeball a handful before making it permanent. After you've confirmed the compression looks right, run it again with--overwriteto swap the originals in place. Either way, backups are generated before anything gets modified. -
.splogfiles will show up inside each folder the script touched. They're pipe-delimited and track the md5, original size, optimized size, savings percentage, and timestamp for every file that was successfully processed. Re-runs read this file and skip anything already listed, which means you can kill the script mid-sweep and resume later without burning credits twice on the same image.
Converting the catalog to WebP
This is where the Lighthouse numbers actually move. WebP typically shaves 40-60% off JPEG weight for photographic product imagery, and the script can emit WebP copies alongside the optimized originals. On image-heavy catalogs, this alone can shave hundreds of MB off total transfer size per day.
./shortpixel-optimize.sh \
-l 1 \
--convertto +webp \
--overwrite \
-j 4 \
/var/www/magento/pub/media/catalog/product
The +webp means "add WebP versions", not "replace with WebP", so you end up with both .jpg and .jpg.webp files on disk. Serving them is a separate concern, out of scope for the script itself, but on nginx the standard pattern is:
location ~* ^/pub/media/catalog/.+\.(jpe?g|png)$ {
add_header Vary Accept;
try_files $uri$webp_suffix $uri =404;
}
with $webp_suffix set in the http {} block from a map on the Accept header. Magento 2.4+ also has native WebP support in the media gallery if you'd rather handle it at the application layer; either path works, the script's job ends when the .webp files are on disk.
After the sweep, run bin/magento catalog:images:resize once to rebuild the cache tree from your freshly optimized originals. This step is what actually benefits the storefront, since customers are served images from cache/, not from the sources.
Scheduling it for new uploads
The whole point of this setup is that it stops being a one-time cleanup and starts being a background hygiene job. Because .splog makes re-runs idempotent, the exact same command works fine as a scheduled task, it'll only spend API credits on files that have been added or changed since the previous invocation:
# Every night at 02:40, optimize new Magento product uploads
40 2 * * * cd /home/deploy/shortpixel-sh && ./shortpixel-optimize.sh --overwrite --convertto +webp -j 6 /var/www/magento/pub/media/catalog/product >> /var/log/shortpixel.log 2>&1
Two quick notes about running it unattended:
- When there's no terminal attached, the onboarding wizard notices and bails out without prompting, so a cron job will never sit there waiting for input.
- With
EMAIL=populated in.env, the dashboard summary gets delivered to that address once the run completes. Under the hood it looks formailfirst andsendmailsecond, or you can pin it explicitly withMAIL_CMD=.
I rely on the nightly email as a cheap liveness check. On a healthy store the report shows a handful of new files processed every night, corresponding to whatever the merchandising team uploaded. If one morning it shows zero, or a spike of errors, I know before the customer does that something upstream broke (expired API key, full disk, permissions change on a deploy, etc).
One thing to add: after the nightly optimization cron, you probably also want a separate job that runs bin/magento catalog:images:resize so the cache stays in sync with the updated originals. I usually stagger it by 15 minutes to avoid overlap.
Rolling back when you don't like the result
Every original is mirrored to BACKUP_DIR before the API call fires, and the script verifies the backup exists and is non-empty before it touches the source file. If verification fails, the file is skipped and counted as an error, the source is never modified when the safety net isn't in place.
If a run produces artifacts you don't like, say, JPEG compression got too aggressive on transparent-background product shots, you can undo the entire thing in one command:
./shortpixel-optimize.sh --restore /var/www/magento/pub/media/catalog/product
This walks through the backup mirror, pushes every saved original back into place, drops a restore_audit.log in the script's folder, and clears out the .splog files so the next invocation starts from a clean slate. After a restore, don't forget to bin/magento catalog:images:resize again to rebuild the cache from the restored originals.
Once you're confident the results are good and you want the disk space back, prune old backups:
./shortpixel-optimize.sh --purge-backups 30 /var/www/magento/pub/media/catalog/product
What this does is remove backups that satisfy both conditions: aged past 30 days and referenced by a .splog entry. Anything without a matching log line — files that errored out, got skipped, or for whatever reason never made it into the state file — survives the purge no matter how old it is. Conservative by design, which is what you want when the subject is production media.
My typical Magento onboarding checklist
Putting it all together, here's roughly what I do the first time I touch a neglected Magento 2 store's media folder:
- Clone the repo into the deploy user's home, walk through the wizard, pick
LOSSY=1and plug in an email. -
rsync -aa snapshot ofpub/media/catalogsomewhere safe on the same disk. - Smoke test on
catalog/categorywithout--overwrite. - Wipe
pub/media/catalog/product/cache/. - Full sweep on
catalog/productwith--overwrite --convertto +webp -j 6. - Run
bin/magento catalog:images:resizeto regenerate the cache. - Add the nginx map +
try_filesrule so browsers that sendAccept: image/webpget the.webpvariant. - Schedule a nightly job that re-invokes the same optimize command, followed by a delayed
catalog:images:resizerun. - After a fortnight of clean nightly reports, run
--purge-backups 14.
Initial setup usually eats less than half an hour of active attention, and most of that is just waiting on the first sweep to grind through the catalog. The recurring cost is whatever ShortPixel credits your new uploads consume, which for most mid-sized stores is a rounding error against the bandwidth savings. And because the whole thing lives outside Magento, there's no module to keep updated, no admin screen to break on the next bin/magento setup:upgrade, and no composer dependency to audit. For Magento stores, this is one of those rare changes that improves both performance and infrastructure cost at the same time.
If you want to dig into the less-common flags, the script's --help output is thorough, it covers custom output directories, per-extension exclusions, lossless mode for line art and logos, glossy mode for when you want to be extra gentle with JPEG gradients, and the full list of exit codes. The repo is on GitHub here.
Top comments (0)