Imager: обработка изображений на лету — установка, настройка и интеграция с сайтом
Пошаговая инструкция по установке Imager — сервиса обработки изображений на лету. Разбираем Docker, ресайз, кадрирование, WebP и AVIF, CDN, кэширование, responsive images, клиентские библиотеки и production-настройку.
Изображения — одна из самых заметных частей веб-приложения с точки зрения производительности. Интернет-магазины, каталоги, новостные сайты, маркетплейсы, социальные сети и корпоративные порталы могут хранить фотографии в нескольких мегабайтах, тогда как браузеру для конкретного места на странице часто нужна картинка размером всего несколько десятков или сотен килобайт.
Традиционный подход — заранее создавать набор миниатюр для каждого изображения: 200x200, 400x400, 800x600, WebP, JPEG, Retina и так далее. При большом количестве изображений количество файлов быстро становится огромным, а требования фронтенда постоянно меняются.
Imager решает эту задачу иначе: изображение хранится в исходном виде, а нужный ассет создаётся автоматически при обращении по URL, сохраняется в кэш и затем отдаётся повторно. Сам URL содержит информацию об исходнике, размере, DPR и формате результата, поэтому результат предсказуем, кешируем и удобен для CDN.
Онлайн-демо позволяет сразу увидеть принцип работы сервиса и клиентской части:
Демо Imager: https://altuh.ru/demo/imager
Что такое Imager
Imager — отдельный микросервис обработки изображений и ассетов. Он получает запрос на конкретный вариант изображения, находит исходник в настроенном хранилище, выполняет необходимые преобразования, сохраняет результат и отдаёт его клиенту.
Архитектурно это выглядит так:
┌─────────────┐
│ Браузер │
└──────┬──────┘
│
▼
┌─────────────┐
│ CDN │
└──────┬──────┘
│ cache miss
▼
┌─────────────┐
│ Imager │
└──────┬──────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
local FS S3 HTTP/SFTP/FTP
│
▼
исходник
Сервис не требует отдельного API-запроса для каждой генерации. Основной интерфейс — обычный GET по URL ассета. Тела обычных запросов не используются; сервис также поддерживает HEAD и OPTIONS.
Это позволяет поставить Imager непосредственно за Nginx, CDN или другим reverse proxy.
Зачем нужен отдельный сервис обработки изображений
Главная проблема предварительной генерации миниатюр — количество комбинаций.
Допустим, на сайте используются:
200x200
400x400
800x600
1200x800
и одновременно нужны:
JPEG
WebP
AVIF
а для Retina:
1x
2x
3x
Для одного исходника уже появляются десятки вариантов.
При добавлении нового размера или формата необходимо либо генерировать новые файлы заранее, либо писать дополнительную логику.
Imager переносит эту задачу в отдельный слой:
исходник
↓
Imager
↓
нужный вариант
Поэтому приложение хранит исходные файлы, а производные изображения создаются только тогда, когда они действительно понадобились.
Особенно хорошо такая схема работает с CDN: канонический URL конкретного варианта остаётся неизменяемым, поэтому результат можно кэшировать на длительное время. В документации Imager для таких URL используется Cache-Control: public, max-age=31536000, immutable.
Главные возможности Imager
Imager умеет не просто менять размер изображения.
Поддерживаются:
- resize;
- центральное кадрирование;
trimдля удаления полей;smart-crop;- кадрирование по лицу;
- object-crop;
- водяные знаки;
- разные форматы вывода;
- DPR 2x и 3x;
- анимированные изображения с ограничениями по количеству кадров и длительности;
- работа с локальной файловой системой, S3, SFTP, FTP/FTPS и HTTP-источниками;
- Prometheus-метрики;
- health-check и readiness-check;
- ограничения ресурсов;
- deny-by-default политика URL и пресетов.
Поддерживаются современные и классические форматы, включая JPEG, PNG, WebP, GIF, AVIF, HEIF/HEIC и JPEG XL. Для видео сервис также может извлекать кадры из поддерживаемых форматов.
Установка Imager через Docker
Самый простой вариант — использовать готовый Docker-образ altrap/imager.
Клонировать репозиторий и собирать сервис вручную для обычного запуска не требуется. Базовые конфигурационные файлы уже находятся в образе.
Создадим каталоги:
mkdir -p setting data/source data/result
chmod -R a+rwX data/result
Запускаем контейнер:
docker run -d \
--name imager \
-p 8080:8080 \
-v ./setting:/etc/imager/setting:rw \
-v ./data/source:/data/source:ro \
-v ./data/result:/data/result:rw \
-e IMAGER_CONFIG_DIR=/etc/imager/setting \
altrap/imager:latest
После запуска проверяем health-check:
curl http://localhost:8080/healthz
Ожидаемый ответ:
{ "status": "alive" }
Теперь можно положить изображение в:
data/source/test.jpg
и запросить, например:
curl -o result.webp \
http://localhost:8080/test-jpg/200x200.webp
Imager создаст нужный вариант изображения и вернёт его клиенту. Структура URL определяет исходник, размер и формат результата.
Docker Compose
Для постоянной эксплуатации удобнее использовать Docker Compose:
services:
imager:
image: altrap/imager:latest
restart: unless-stopped
stop_signal: INT
stop_grace_period: 15s
ports:
- '8080:8080'
environment:
IMAGER_CONFIG_DIR: /etc/imager/setting
volumes:
- ./setting:/etc/imager/setting:rw
- ./data/source:/data/source:ro
- ./data/result:/data/result:rw
Запуск:
docker compose up -d
Для production в самом репозитории предусмотрены дополнительные настройки: лимиты CPU и памяти, health-check и другие параметры hardening.
Как устроен URL изображения
Одна из ключевых особенностей Imager — отсутствие query-параметров для описания преобразования.
URL строится по понятной схеме:
/{path}/{source_name}-{source_format}/{segment}@{dpr}.{output_format}
Например:
/photos/city-skyline-jpg/300x@2.webp
означает:
источник: city-skyline.jpg
ширина: 300 px
DPR: 2
формат: WebP
При DPR 2 фактическое изображение будет сформировано с плотностью, необходимой для соответствующего варианта.
Можно использовать именованный пресет:
/test-jpg/thumb.webp
или произвольный размер:
/test-jpg/640x.webp
/test-jpg/x400.webp
/test-jpg/640x480.webp
Специальный сегмент x означает исходный размер с возможностью преобразовать формат.
Таким образом, само имя URL становится декларацией того, что именно требуется от сервиса.
Пресеты вместо бесконтрольных размеров
В реальном проекте не всегда нужно разрешать пользователю запрашивать абсолютно любой размер.
Например, для каталога можно разрешить:
thumb
product
catalog
и соответствующие размеры.
Это настраивается через policy.
У Imager используется deny-by-default модель: разрешены только явно описанные пресеты и custom-размеры, соответствующие правилам конкретного пути. Дополнительно задаются ограничения на размер исходника, размер результата, количество пикселей, кадров и длительность анимации.
Это одновременно даёт контроль над API и защищает сервис от запросов, которые создают чрезмерную нагрузку.
WebP, AVIF, JPEG и другие форматы
Один исходник не обязательно должен храниться в нескольких форматах.
Допустим, оригинал:
product.jpg
Можно отдавать как:
product-jpg/800x600.webp
или:
product-jpg/800x600.avif
Оригинал при этом остаётся JPEG.
Imager поддерживает JPEG, PNG, WebP, GIF, AVIF, HEIF/HEIC и JPEG XL.
Это позволяет централизовать работу с форматами и не заставлять приложение создавать отдельные физические копии каждого изображения.
Responsive images и Retina
Современный сайт редко должен отдавать одну и ту же картинку всем устройствам.
На смартфоне изображение шириной 1200 пикселей часто избыточно. На Retina-дисплее, наоборот, может понадобиться вариант с повышенной плотностью.
Imager Client умеет автоматически строить наборы ассетов и готовый HTML <picture> / <img>.
Например:
html := img.GetAssetsHtml(
"/images/uploads/example.png",
"200x200",
nil,
nil,
imager.Options{
Class: "photo",
ID: "main",
Alt: "Фото",
Lazy: true,
},
)
Клиентская библиотека умеет формировать srcset в двух режимах: через w-дескрипторы для разных ширин или через x-дескрипторы для разных DPR.
В результате можно построить стандартную responsive-разметку:
<picture>
<source ... />
<source ... />
<img src="..." srcset="..." alt="Фото" />
</picture>
И при этом не требуется вручную вычислять десятки URL.
Клиентские библиотеки Imager
Сам сервис занимается обработкой изображений, а клиентские библиотеки решают другую задачу — формируют правильные URL и HTML.
На данный момент есть реализации для:
Go
PHP
Python
TypeScript
А также интеграции:
Vue
React
Twig
Клиентская часть построена таким образом, чтобы разные языки формировали одинаковый результат. Для этого используется единый набор golden-тестов.
Это особенно полезно в проектах, где backend написан на одном языке, а frontend — на другом.
Например:
PHP backend
+
TypeScript frontend
или:
Go API
+
React
Обе части могут использовать одну модель построения URL.
Установка клиента для Go
Для Go:
go get gitverse.ru/pkg-ru/imager-client/v2
Пример:
import imager "gitverse.ru/pkg-ru/imager-client/v2"
client := imager.New(imager.Options{
BaseURL: "https://images.example.com/",
Format: "webp",
})
Получить URL:
url := client.GetAssetPath(
"/images/photo.jpg",
"800x600",
"webp",
)
Результат будет примерно таким:
https://images.example.com/images/photo-jpg/800x600.webp
Можно также использовать объект размера:
asset := client.GetAsset(
"/images/photo.jpg",
imager.Size{
Width: 800,
Height: 600,
},
"webp",
2,
)
API клиента описывает отдельные методы для одного ассета, набора ассетов, HTML и формирования URL.
Установка клиента для PHP
Установка через Composer:
composer require pkg-ru/imager-client
Использование:
use imagerClient\Imager;
$imager = new Imager([
"baseURL" => "https://images.example.com/",
"format" => "webp",
]);
$url = $imager->GetAssetPath(
"/images/photo.jpg",
"800x600",
"webp"
);
PHP-клиент имеет ту же модель API, что Go, Python и TypeScript.
Установка клиента для Python
pip install imager_client
Пример:
from imager import Imager
imager = Imager({
"baseURL": "https://images.example.com/",
"format": "webp",
})
url = imager.GetAssetPath(
"/images/photo.jpg",
"800x600",
"webp",
)
Для Python доступен тот же набор основных операций построения ассетов.
Установка клиента для TypeScript
npm install imager-client
В браузерном коде достаточно:
import { Imager } from 'imager-client'
const imager = new Imager({
baseURL: 'https://images.example.com/',
format: 'webp',
})
const url = imager.GetAssetPath('/images/photo.jpg', '800x600', 'webp')
Здесь есть важная архитектурная особенность: обычный клиент не содержит административного токена и не выполняет административные HTTP-запросы. Серверные административные методы вынесены в отдельный импорт:
import { ImagerServer } from 'imager-client/server'
Это позволяет не тащить секрет в браузерный bundle.
Vue, React и Twig
Поверх ядра клиентской библиотеки существуют тонкие интеграции с популярными frontend и template-фреймворками.
Для Vue:
npm install @pkg-ru/imager-vue
Для React:
npm install @pkg-ru/imager-react
Для Twig:
composer require pkg-ru/imager-twig
Компоненты не реализуют собственную систему преобразований. Они используют общее ядро и добавляют адаптацию props и рендеринг нативного элемента фреймворка.
Например, React:
<ImagerAssets src="/test.png" width={200} height={200} dpr={2} format="webp" alt="Фото" />
Vue:
createApp(App)
.use(ImagerPlugin, {
baseURL: '...',
format: 'webp',
dpr: 2,
})
.mount('#app')
Twig:
{{ imager_assets('/test.png', {
preset: 'thumb',
format: 'webp',
alt: 'Фото'
})|raw }}
SSR и hydration
Клиентская библиотека не привязана только к браузеру.
Ядро формирования URL и HTML не зависит от DOM или window, поэтому тот же код может использоваться при SSR и prerender. Сервер может сформировать готовый <picture> ещё до отправки HTML клиенту, а затем фронтенд использует тот же результат при hydration.
Это удобно, например, для:
Next.js
Nuxt
SvelteKit
где изображения должны быть корректно сформированы уже во время серверного рендеринга.
Почему клиент практически не создаёт нагрузки
Очень важный момент: клиентская библиотека не отправляет HTTP-запросы при каждом GetAssetPath.
Её задача — вычислить строку URL.
То есть:
url := client.GetAssetPath(...)
не означает:
HTTP → Imager
Это просто генерация URL.
В README клиента отдельно подчёркивается, что методы генерации ассетов работают без HTTP, валидации и исключений и реализованы как операции построения строк. Поэтому их можно вызывать массово, в том числе при генерации больших списков изображений.
Это полезно для страниц с сотнями и тысячами товаров или фотографий.
Например:
for _, product := range products {
product.ImageURL = client.GetAssetPath(
product.Image,
"300x300",
"webp",
)
}
Никаких сетевых запросов к Imager при формировании ответа API не возникает.
Сам браузер обратится к Imager уже тогда, когда получит HTML с соответствующим URL.
Кэширование и CDN
Именно здесь архитектура Imager особенно хорошо раскрывается.
Вместо:
Приложение
↓
генерация файла
↓
сохранение
↓
раздача
получается:
Браузер
↓
CDN
↓ cache miss
Imager
↓
storage
После первой генерации результат может обслуживаться непосредственно CDN или внешним reverse proxy.
Поскольку URL однозначно определяет вариант ассета, можно использовать длительное кеширование и immutable.
При этом изменение исходника должно сопровождаться изменением URL или другой выбранной стратегией инвалидирования. Это позволяет избежать ситуации, когда CDN бесконечно хранит старую версию под тем же URL.
Поддержка S3 и других хранилищ
Imager не ограничивает проект локальной файловой системой.
Источники и результаты могут находиться отдельно и использовать разные backend'ы:
fs
s3
sftp
ftp
ftps
http
Причём хранилище результата может отличаться от хранилища исходника. HTTP-источники работают в режиме read-only.
Например:
Original:
S3 bucket
Generated:
S3 bucket / CDN storage
или:
Original:
локальный filesystem
Generated:
S3
Это позволяет использовать Imager и в небольшом проекте на одном сервере, и в распределённой инфраструктуре.
Водяные знаки
Водяной знак можно сделать частью политики обработки.
Например:
исходник
↓
resize
↓
crop
↓
watermark
↓
WebP
Результат также кэшируется, поэтому повторный запрос уже не требует повторного выполнения всех операций.
Это удобно для:
- каталогов;
- маркетплейсов;
- фотобанков;
- публикации фотографий;
- защищённых превью;
- демонстрации изображений с брендингом.
Smart crop, face crop и object crop
Простое центральное кадрирование не всегда хорошо работает.
Для фотографии человека:
┌──────────────────────┐
│ │
│ лицо │
│ │
│ │
└──────────────────────┘
центральный crop может обрезать человека не там, где нужно.
Поэтому Imager поддерживает smart-crop, а также кадрирование с учётом лиц и объектов. Для соответствующих режимов используется ONNX Runtime и модели детекции.
Это особенно полезно для карточек товаров, аватаров, каталогов и изображений с различным расположением объектов.
Анимация
Imager поддерживает работу с анимированными изображениями, в частности GIF и WebP, с ограничениями количества кадров и длительности. APNG также может обрабатываться как анимированный PNG, а возможности записи APNG зависят от используемой сборки libvips.
Для production это важно не только из-за функциональности, но и с точки зрения защиты ресурсов: анимация может быть значительно тяжелее обычного статического изображения, поэтому ограничения задаются централизованно.
Безопасность
Сервис обработки изображений нельзя оставлять полностью «всеядным».
Если разрешить пользователю произвольные пути, размеры и преобразования, можно получить нежелательные обращения к файловой системе, чрезмерное потребление памяти или генерацию огромного количества комбинаций.
Поэтому Imager использует несколько уровней защиты.
Во-первых, применяется deny-by-default политика для URL и пресетов.
Во-вторых, существуют ограничения:
размер исходного файла
размер результата
количество пикселей
количество кадров
длительность анимации
Также есть ограничения размера HTTP-запроса, защищённые операции с файлами и защита от symlink-атак.
Для Docker предусмотрены возможности hardening и ресурсные ограничения, что позволяет не превращать обработчик изображений в неконтролируемый потребитель ресурсов.
Наблюдаемость
Imager предоставляет готовые точки для мониторинга:
/healthz
/readyz
/metrics
/healthz используется для проверки жизнеспособности процесса, /readyz — готовности сервиса, а /metrics отдаёт данные в формате Prometheus.
Это позволяет без написания дополнительной инфраструктуры встроить Imager в стандартную систему мониторинга:
Prometheus
↓
Grafana
↓
Imager metrics
Для production это особенно полезно при масштабировании нескольких экземпляров сервиса.
Сборка из исходников
Docker — не единственный вариант.
Imager написан на Go и может собираться из исходного кода.
Обычная сборка:
go build -o imager ./cmd/imager
Production-сборка с libvips:
go build \
-tags libvips \
-trimpath \
-ldflags="-s -w" \
-o imager \
./cmd/imager
Для face/object detection:
go build \
-tags "libvips,onnx" \
-trimpath \
-ldflags="-s -w" \
-o imager \
./cmd/imager
Для libvips используются соответствующие системные зависимости и C-компилятор; дополнительные кодеки устанавливаются в зависимости от нужных форматов.
Сам проект использует архитектуру ports-and-adapters: доменная логика отделена от адаптеров хранения, обработки и инфраструктуры. Build tags позволяют выбирать конкретные реализации.
Конфигурация Imager
Конфигурация сервиса полностью вынесена в YAML.
Основные слои:
server.yaml
generate.yaml
failback.yaml
и локальные переопределения:
server-local.yaml
generate-local.yaml
failback-local.yaml
*-local.yaml предназначены в том числе для секретов и не должны попадать в репозиторий. Локальные конфигурации накладываются поверх базовых.
Такой подход удобен для разделения:
Git
├── server.yaml
├── generate.yaml
└── failback.yaml
local
├── server-local.yaml
├── generate-local.yaml
└── failback-local.yaml
При обновлении Docker-образа базовые конфигурации могут синхронизироваться с новой версией образа, а *-local.yaml сохраняются.
Production-схема
Для реального сайта я бы рассматривал Imager как самостоятельный сервис за reverse proxy.
Например:
┌─────────────┐
│ Browser │
└──────┬──────┘
│
▼
┌─────────────┐
│ CDN │
└──────┬──────┘
│
▼
┌─────────────┐
│ Nginx │
└──────┬──────┘
│
┌──────────┴──────────┐
▼ ▼
┌───────────┐ ┌───────────┐
│ Imager 1 │ │ Imager 2 │
└─────┬─────┘ └─────┬─────┘
│ │
└──────────┬──────────┘
▼
┌────────┐
│ S3 │
└────────┘
В этой схеме CDN снимает основной объём повторных запросов, Nginx выполняет роль reverse proxy, а несколько экземпляров Imager позволяют масштабировать обработку.
TLS при этом можно завершать на reverse proxy; само приложение работает по HTTP на порту 8080.
Imager хорошо подходит для highload
Изображения могут создавать огромную нагрузку не только из-за количества запросов, но и из-за стоимости самой обработки.
Поэтому здесь важны сразу несколько уровней оптимизации:
CDN
↓
cache hit
↓
готовый URL
↓
Nginx
↓
Imager
↓
внутренний cache
↓
storage
Плюс клиентская библиотека не делает HTTP-запросы при формировании URL.
В результате разделяются две разные задачи:
Backend
→ быстро формирует URL
Imager
→ только при необходимости обрабатывает изображение
CDN
→ отдаёт уже готовый результат
Для страниц с большим количеством изображений это значительно удобнее, чем создавать десятки файлов во время загрузки контента.
Почему client лучше ручной генерации URL
Теоретически URL можно формировать самостоятельно:
url := baseURL + source + "-jpg/300x300.webp"
Но как только появляются:
presets
DPR
несколько форматов
srcset
picture
разные размеры
fallback
SSR
ручная конкатенация быстро становится ошибкоопасной.
Клиентская библиотека берёт эту логику на себя.
Причём API одинаковый во всех поддерживаемых языках, а набор golden-тестов гарантирует одинаковую структуру результата.
Это означает, что backend может сформировать URL в Go или PHP, а frontend — тот же вариант в TypeScript, не создавая две независимые реализации правил.
Что в итоге получает разработчик
Если собрать всё вместе, Imager закрывает сразу несколько задач:
┌──────────────────────┐
│ Imager │
├──────────────────────┤
│ resize │
│ crop │
│ trim │
│ smart crop │
│ face crop │
│ object crop │
│ watermark │
│ formats │
│ animation │
│ DPR │
│ caching │
│ storage │
│ security │
│ metrics │
└──────────────────────┘
А клиентские библиотеки закрывают вторую половину задачи:
┌──────────────────────┐
│ Imager Client │
├──────────────────────┤
│ URL │
│ presets │
│ размеры │
│ DPR │
│ formats │
│ srcset │
│ picture │
│ HTML │
│ Go │
│ PHP │
│ Python │
│ TypeScript │
│ Vue │
│ React │
│ Twig │
└──────────────────────┘
Это позволяет не смешивать обработку изображений с бизнес-логикой самого приложения.
Когда стоит использовать Imager
Imager особенно хорошо подходит для проектов, где:
- много пользовательских изображений;
- размеры изображений зависят от места использования;
- нужен WebP или AVIF без хранения отдельных оригиналов;
- требуется responsive images и
srcset; - используются Retina-дисплеи;
- изображения хранятся в S3;
- нужен CDN-friendly URL;
- необходимо автоматически создавать превью;
- есть несколько backend/frontend языков;
- требуется централизованный контроль обработки;
- приложение должно масштабироваться горизонтально.
Особенно заметный эффект появляется в каталогах, интернет-магазинах, медиа-сервисах, маркетплейсах, CMS и больших веб-платформах.
Быстрый путь от нуля до production
Практический сценарий можно свести к нескольким шагам.
Сначала запускаем Imager:
docker compose up -d
Проверяем:
curl http://localhost:8080/healthz
Кладём исходное изображение в storage.
После этого проверяем URL:
/images/photo-jpg/300x300.webp
Затем подключаем клиентскую библиотеку:
go get gitverse.ru/pkg-ru/imager-client/v2
или для PHP:
composer require pkg-ru/imager-client
или для TypeScript:
npm install imager-client
После чего приложение начинает генерировать URL через клиент, а не вручную.
Следующим шагом можно вынести Imager за Nginx, подключить CDN, определить пресеты, настроить ограничения и включить Prometheus.
В итоге приложение перестаёт заниматься физическим созданием и хранением десятков разновидностей каждого изображения.
Заключение
Imager — это не просто конвертер JPEG в WebP или сервис для создания thumbnails. Его основная идея — вынести работу с производными изображениями в отдельный слой инфраструктуры и перейти от предварительной генерации файлов к генерации по запросу по каноническому URL.
Приложение хранит оригинал:
photo.jpg
а клиент получает именно тот вариант, который нужен:
photo-jpg/300x300.webp
photo-jpg/600x400.webp
photo-jpg/600x400@2.webp
photo-jpg/1200x800.avif
Все эти варианты описываются URL, могут кэшироваться и отдаваться через CDN.
При этом разработчику не приходится самостоятельно писать генератор URL, собирать srcset, поддерживать несколько языков реализации клиента или создавать отдельную систему для ресайза.
Демо: https://altuh.ru/demo/imager
Imager: https://github.com/pkg-ru/imager
Imager Client: https://github.com/pkg-ru/imager-client
Для production-развёртывания доступны отдельные руководства по конфигурации, установке, storage, безопасности, обработке изображений и настройке Nginx.
Релевантные разделы
Читайте также
Архитектура веб-платформ: как проектировать систему, которая растёт
Как проектировать архитектуру веб-платформ, которая выдерживает рост функциональности, команды и нагрузки: модульность, границы ответственности, данные, интеграции, наблюдаемость и выбор между монолитом и микросервисами.
Высоконагруженные веб-системы: как проектировать архитектуру под рост нагрузки
Как проектировать высоконагруженные веб-системы: поиск узких мест, масштабирование backend и базы данных, кэширование, очереди, отказоустойчивость, нагрузочное тестирование и наблюдаемость.
Чистый код и поддерживаемость: почему читаемость важнее скорости
Как писать поддерживаемый код: понятные имена, небольшие функции, явные зависимости, простые интерфейсы, тесты и рефакторинг без усложнения проекта.