imgproxy снимает ресайз с приложения и добавляет новый узел в схему
Если приложение ресайзит картинки прямо в коде — через ImageMagick, Sharp или Pillow — рано или поздно CPU начинает упираться именно в это, а не в бизнес-логику. imgproxy решает конкретную задачу: обработку изображений «на лету» по URL-параметрам, вне процесса приложения. Но это не бесплатная магия — вы меняете нагрузку на бэкенд на новый сервис, который нужно разворачивать, кэшировать и защищать, и честно разобраться, когда этот обмен того стоит, важнее, чем просто установить пакет.
Содержание
- Что делает imgproxy и зачем разгружать приложение от ресайза
- URL-параметры вместо кода: как это выглядит на практике
- Деплой imgproxy в docker-compose и базовая конфигурация
- Новый узел в схеме: что вы на самом деле добавляете в инфраструктуру
- nginx и CDN перед imgproxy: кто и что кэширует
- Безопасность: подпись URL, доверенные источники, лимиты
- Когда это того стоит, а когда — лишняя сложность
Что делает imgproxy и зачем разгружать приложение от ресайза
Типичная схема без imgproxy: пользователь загружает фото, бэкенд принимает файл, сразу или по требованию прогоняет его через sharp (Node.js) или Pillow/ImageMagick (Python/PHP), генерирует несколько размеров (thumbnail, preview, full), сохраняет на диск или в S3. Каждый ресайз — это декодирование картинки, изменение размера, повторное кодирование в JPEG/WebP, и всё это в том же процессе, что обслуживает HTTP-запросы. На малой нагрузке разницы не видно. На средней — становится заметно, что при массовой загрузке фотографий API начинает отвечать медленнее не из-за базы данных, а из-за того, что воркеры заняты пережатием JPEG.
imgproxy — открытый (есть и платная Pro-версия с дополнительными фичами) сервис на Go, который делает ровно одну вещь: принимает URL с параметрами обработки, скачивает исходное изображение (с диска, из S3-совместимого хранилища или по HTTP), применяет ресайз/кроп/конвертацию и отдаёт результат. Никакой базы данных, никакой бизнес-логики — просто HTTP-сервис для трансформации картинок. Логика в приложении сводится к формированию правильного URL вместо вызова библиотеки ресайза.
Ключевое отличие от «встроенного» ресайза:
- Приложение больше не декодирует и не кодирует изображения само. Эта работа (CPU- и memory-intensive) уходит в отдельный процесс, который можно масштабировать независимо.
- Варианты размеров не нужно хранить заранее. Вместо генерации thumbnail/preview/full при загрузке вы храните один оригинал, а нужный размер imgproxy строит по запросу и кэширует результат.
- Формат подбирается под клиента. imgproxy умеет отдавать WebP или AVIF автоматически, если браузер их поддерживает (по заголовку
Accept), без отдельной логики в приложении.
Это не единственный инструмент такого рода — есть Thumbor (Python, старше и тяжелее), публичный сервис wsrv.nl (бесплатный прокси-ресайзер без самостоятельного хостинга), платные Cloudinary и Cloudflare Images. imgproxy выделяется тем, что это компактный бинарник на Go без рантайм-зависимостей: разворачивается одним Docker-контейнером и не требует администрирования кроме конфигурации через переменные окружения.
URL-параметры вместо кода: как это выглядит на практике
Вместо того чтобы бэкенд вызывал sharp(input).resize(300, 300).toFormat('webp'), он просто формирует ссылку на imgproxy с нужными параметрами прямо в HTML или JSON-ответе API. Базовый формат URL:
http://imgproxy.internal:8080/{signature}/{processing_options}/{encoded_source_url}
{processing_options} — это цепочка модификаторов через слэш, например:
rs:fill:300:300— resize с типомfill(обрезка под точный размер) до 300×300;g:sm— гравитацияsmart(imgproxy сам ищет «интересную» область кадра для кропа);q:80— качество сжатия 80;f:webp— принудительный формат WebP.
Пример полного пути обработки (в «незащищённом» режиме, без подписи, — только для локальной отладки, в проде так делать нельзя):
http://localhost:8080/insecure/rs:fill:300:300/g:sm/q:80/f:webp/plain/https://cdn.example.com/uploads/photo.jpg
imgproxy поддерживает и «plain»-формат исходного URL (как в примере выше — просто https://... после plain/), и классический вариант с base64url-кодированием ссылки — это удобно, когда в исходном URL есть символы, которые ломают путь. В своей реализации ориентируйтесь на актуальную документацию imgproxy на GitHub: набор опций обработки (кроп, водяные знаки, паддинг, повороты, размытие лиц) периодически расширяется.
На стороне бэкенда правки минимальны: там, где раньше был вызов библиотеки ресайза, теперь функция, которая собирает URL с нужными опциями и, если включена подпись, считает HMAC-подпись пути — небольшая прослойка кода, но в сотни раз дешевле по CPU, чем реальное декодирование JPEG.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверДеплой imgproxy в docker-compose и базовая конфигурация
imgproxy официально распространяется как Docker-образ darthsim/imgproxy (это организация автора проекта). Минимальный docker-compose.yml для отдельного сервиса:
services:
imgproxy:
image: darthsim/imgproxy:latest
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
environment:
IMGPROXY_BIND: ":8080"
IMGPROXY_KEY: "${IMGPROXY_KEY}"
IMGPROXY_SALT: "${IMGPROXY_SALT}"
IMGPROXY_TTL: "2592000"
IMGPROXY_MAX_SRC_RESOLUTION: "20"
IMGPROXY_MAX_SRC_FILE_SIZE: "20971520"
IMGPROXY_ALLOWED_SOURCES: "https://cdn.example.com/,https://storage.example.com/"
deploy:
resources:
limits:
cpus: "2"
memory: 1g
Пояснения по ключевым переменным (полный список — в документации imgproxy, он большой и меняется от релиза к релизу):
IMGPROXY_KEYиIMGPROXY_SALT— hex-строки для подписи URL (генерируются, например,openssl rand -hex 32). Без них сервис работает в «insecure»-режиме — это допустимо только за закрытым периметром.IMGPROXY_TTL— время жизниCache-Controlв заголовке ответа, в секундах.IMGPROXY_MAX_SRC_RESOLUTION— ограничение на разрешение исходного изображения в мегапикселях, чтобы кто-то не положил сервис, подсунув гигантский TIFF.IMGPROXY_MAX_SRC_FILE_SIZE— лимит размера исходного файла в байтах.IMGPROXY_ALLOWED_SOURCES— белый список источников, откуда imgproxy разрешено скачивать оригиналы (подробнее — в разделе про безопасность).
Если оригиналы лежат в S3-совместимом хранилище (MinIO, Selectel, Yandex Object Storage), можно подключить его напрямую через IMGPROXY_USE_S3 и IMGPROXY_S3_ENDPOINT вместо HTTP-скачивания — быстрее, потому что трафик идёт по внутренней сети. Разворачивание такого хранилища разбирали отдельно в статье про MinIO на VPS.
Проверка после запуска:
curl -I "http://localhost:8080/insecure/rs:fill:100:100/plain/https://cdn.example.com/test.jpg"
Ответ 200 с Content-Type: image/jpeg (или webp/avif, если добавить f:webp) означает, что сервис поднялся и видит источник.
Новый узел в схеме: что вы на самом деле добавляете в инфраструктуру
Здесь начинается честная часть. imgproxy решает проблему CPU в приложении, но не исчезает бесследно — он становится новым звеном в цепочке запроса.
Новая точка отказа. Раньше картинка отдавалась статикой (или из того же приложения). Теперь между пользователем и картинкой появляется ещё один процесс: если imgproxy упал, недоступен по сети или перегружен — картинки перестают открываться, даже если само приложение работает штатно. Нужен restart: unless-stopped (или systemd/supervisor-эквивалент), мониторинг доступности порта 8080 и план на случай деградации — например, отдавать оригинал напрямую, если imgproxy не отвечает.
CPU и память переезжают, а не исчезают. Ресайз JPEG 4000×3000 всё так же требует декодирования и кодирования — просто теперь этим занят контейнер imgproxy, а не воркер приложения. Если у вас один общий сервер, выигрыш будет только в изоляции («приложение отвечает быстро, картинки медленно», а не «всё тормозит одновременно»), но не в суммарной нагрузке. Реальную разгрузку CPU вы получаете, только если imgproxy стоит на отдельном сервере/контейнере с собственными ресурсами, либо если задача — снять пиковую нагрузку с процесса, который также держит соединения с базой и внешние API.
Диск или память под кэш. Без кэширования каждый уникальный набор параметров ресайза пересчитывается заново при каждом запросе — расточительно, если одну и ту же миниатюру запрашивают тысячи раз в день. imgproxy сам кэш не хранит (это не CDN), он лишь выставляет заголовки Cache-Control/Expires, а кэшировать должен слой перед ним — nginx с proxy_cache, внешний CDN или browser cache. То есть нужен ещё и дисковый кэш на прокси-слое, который тоже надо мониторить на заполнение диска.
Сетевой путь усложняется. Раньше: клиент → приложение → диск/S3. Теперь: клиент → nginx/CDN → imgproxy → S3/HTTP-источник. Каждое звено — дополнительная задержка на первый (некэшированный) запрос и точка, которую нужно логировать при разборе инцидентов «почему картинка не грузится».
Итого: imgproxy не убирает работу по ресайзу из системы — он локализует её в отдельный, специализированный, горизонтально масштабируемый сервис. Это оправдано, когда вам нужна независимая масштабируемость (реплики imgproxy за балансировщиком, без изменений в приложении) или изоляция нагрузки. Для одного маленького VPS и сотни картинок в день это не разгрузка, а усложнение ради усложнения.
nginx и CDN перед imgproxy: кто и что кэширует
В проде imgproxy почти никогда не выставляют напрямую в интернет — перед ним ставят nginx (для TLS-терминации, кэширования и ограничения доступа) и опционально внешний CDN. Пример конфигурации nginx с проксирующим кэшем:
proxy_cache_path /var/cache/nginx/imgproxy levels=1:2 keys_zone=imgproxy_cache:50m
max_size=5g inactive=30d use_temp_path=off;
server {
listen 443 ssl;
server_name img.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_cache imgproxy_cache;
proxy_cache_valid 200 30d;
proxy_cache_valid 404 5m;
proxy_cache_key $uri;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
add_header X-Cache-Status $upstream_cache_status;
}
}
proxy_cache_key $uri работает, потому что путь imgproxy уже содержит все параметры обработки (размер, качество, формат) — одинаковый набор параметров даёт одинаковый URL и одинаковый ключ кэша. Заголовок X-Cache-Status полезен на диагностике: HIT значит, что nginx отдал закэшированную версию, не дёргая imgproxy вообще, а MISS/EXPIRED — что запрос дошёл до сервиса обработки.
Если у вас уже есть внешний CDN перед сайтом, логичнее кэшировать результат imgproxy именно там — тогда повторные запросы вообще не доходят до вашего сервера. Общие принципы работы CDN разобраны в статье про CDN изнутри; настройку кэширования на nginx — в статье про кэширование nginx на VPS. Комбинация «CDN → nginx-кэш → imgproxy → хранилище» избыточна для маленького проекта, но оправдана, когда трафик на изображения ощутимо больше трафика на сам HTML/API.
Отдельный нюанс: TTL кэша на разных уровнях (CDN, nginx, IMGPROXY_TTL, браузер) должны быть согласованы. Если вы поменяли логику обработки (например, дефолтное качество), но старые URL с теми же параметрами ещё в кэше на недели вперёд — пользователи увидят старый результат, пока кэш не протухнет сам или вы не сделаете принудительную инвалидацию (ручной purge для CDN, rm в директории кэша или смена proxy_cache_key для nginx).
Безопасность: подпись URL, доверенные источники, лимиты
Три вещи, которые нельзя пропускать при выводе imgproxy за пределы локальной сети:
Подпись URL (IMGPROXY_KEY/IMGPROXY_SALT). Без неё любой человек, знающий адрес вашего imgproxy, может подставить произвольный URL как источник и заставить ваш сервер скачать и обработать что угодно — это путь к злоупотреблению вычислительными ресурсами и к SSRF, если параметр источника не ограничен.
IMGPROXY_ALLOWED_SOURCES. Даже с подписью URL стоит явно перечислить домены/префиксы, откуда imgproxy разрешено брать оригиналы. Без этого ограничения подписанный URL теоретически может указывать на http://169.254.169.254/... (метаданные облачного провайдера) или на внутренний IP вашей сети — сервис послушно сходит и туда, потому что для него это просто HTTP-запрос.
Лимиты на размер и разрешение источника (IMGPROXY_MAX_SRC_FILE_SIZE, IMGPROXY_MAX_SRC_RESOLUTION) защищают от простого DoS: скормить сервису картинку 30000×30000 пикселей и занять весь CPU и память на декодирование может кто угодно, если лимитов нет.
Дополнительно держите imgproxy не в публичном интернете напрямую, а за nginx/reverse-proxy с собственным TLS, слушая только на 127.0.0.1, как в примере docker-compose выше. Принцип работы reverse-proxy как прослойки между внешним миром и внутренним сервисом разобран в статье про то, как работает reverse proxy.
Когда это того стоит, а когда — лишняя сложность
imgproxy оправдан, если верно хотя бы одно из условий:
- у вас много уникальных вариантов обработки одной картинки (разные размеры под экраны, разные форматы под браузеры, разные кропы под виджеты) — генерировать их все заранее при загрузке дорого и негибко;
- CPU приложения ощутимо тратится на декодирование/кодирование изображений, и это видно в профилировании, а не предполагается «на глаз»;
- нужно масштабировать обработку изображений независимо от остального приложения (реплики за балансировщиком, пока бэкенд остаётся на одном инстансе);
- меняется дизайн и нужно быстро поменять политику ресайза для всего каталога без миграции уже загруженных файлов.
Не стоит вводить imgproxy, если у вас единичные, редко меняющиеся размеры картинок (например, только «оригинал» и «миниатюра 200×200») — сгенерировать оба варианта один раз при загрузке проще и надёжнее, чем держать отдельный сервис; если нагрузка небольшая и текущий ресайз в приложении не создаёт заметных задержек; или если у вас нет ресурсов сопровождать ещё один сервис (мониторинг, обновления образа, настройка кэша) — тогда лучше меньше движущихся частей.
Средний путь, который часто недооценивают: если картинок немного и трафик умеренный, можно обойтись без отдельного сервиса и просто генерировать 2–3 фиксированных размера при загрузке штатной библиотекой — imgproxy решает проблему масштаба и гибкости, а не заменяет здравый смысл там, где масштаба нет.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
imgproxy работает с S3 напрямую или только по HTTP?
Поддерживает оба варианта: можно указать HTTP(S)-URL источника, а можно настроить прямой доступ к S3-совместимому хранилищу через IMGPROXY_USE_S3 — тогда трафик идёт по внутренней сети.
Нужно ли хранить оригиналы отдельно от обработанных версий?
Да, imgproxy не заменяет хранилище — он читает оригинал из источника (диск, S3, HTTP) и отдаёт обработанную версию «на лету», ничего не сохраняя сам по себе (кэширование — задача слоя перед ним, как nginx или CDN).
Что будет, если imgproxy упадёт?
Все URL, которые на него ссылаются, перестанут отдавать изображения, пока сервис не поднимется снова. Поэтому в проде нужны политика restart, мониторинг и, желательно, план деградации.
Можно ли использовать imgproxy без подписи URL?
Технически да (/insecure/...), но это допустимо только за закрытым периметром — в публичном доступе без подписи сервис превращается в открытый ресайзер и вектор SSRF/DoS.
imgproxy заменяет CDN?
Нет: imgproxy обрабатывает изображение по параметрам, CDN раздаёт готовый результат географически близко к пользователю и кэширует его. Их часто используют вместе.
Сколько ресурсов закладывать под imgproxy?
Зависит от объёма и размера исходных изображений и от того, сколько запросов реально долетает мимо кэша до сервиса — универсальных цифр здесь нет, ориентируйтесь на профилирование своей нагрузки, а не на чужие бенчмарки.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →