Как поднять свой S3-совместимый объектный склад на MinIO для staging
Рассказываем, как поднять MinIO на staging-сервере, настроить HTTPS и presigned URL, и почему это дешевле облачного S3 на этапе разработки.
Если в приложении есть загрузка файлов — аватары, документы, отчёты, записи звонков — каждый тестовый файл на staging утекает в облачный счёт. AWS S3, Cloudflare R2 и Yandex Object Storage берут деньги за хранение и трафик, а в staging это чистый перерасход: тут нет SLA, зато полно битых загрузок, ночных сбросов базы и файлов, которые никто не удаляет.
Выход — поднять собственное S3-совместимое хранилище на том же VPS, где крутится staging. MinIO реализует API Amazon S3, понимает те же SDK и presigned URL, но стоит ровно столько, сколько стоит диск сервера. Один и тот же код приложения работает и с MinIO на dev, и с R2 в проде — меняются только переменные окружения.
Что такое MinIO и почему он подходит для staging
MinIO — это open-source сервер объектного хранилища, написанный на Go. Он поддерживает основные операции S3: бакеты, объекты, multipart upload, versioning, lifecycle, шифрование SSE-S3, CORS и IAM-политики. Для приложения он выглядит как обычный S3-эндпоинт, поэтому подходит почти любой SDK: AWS SDK, boto3, minio-js, aws-sdk-go.
На staging важно не столько масштабирование, сколько идентичность поведения продакшена. Если в проде R2 или S3, а в staging — локальная файловая система, вы тестируете не тот код. MinIO закрывает этот разрыв: тот же PutObjectCommand, те же presigned URL, те же ошибки SignatureDoesNotMatch.
Ключевые выводы
- MinIO — полноценный S3-совместимый сервер, который можно развернуть в Docker на VPS за 10–15 минут.
- Для staging это экономия на хранении тестовых файлов и единый код с продакшеном.
- HTTPS и домен лучше отдавать reverse proxy — Traefik или NGINX — с Let’s Encrypt.
- Presigned URL позволяют загружать и скачивать файлы напрямую из браузера, не проксируя байты через бэкенд.
- Root-ключи MinIO нельзя отдавать приложению: создавайте отдельного пользователя с IAM-политикой только на нужный бакет.
Архитектура: прод против staging
В продакшене обычно используется управляемое хранилище: AWS S3, Cloudflare R2, Yandex Object Storage, Hetzner Object Storage. Там работают репликация, резервное копирование и чужой дежурный. В staging достаточно одного MinIO-контейнера на сервере с регулярным зеркалированием в дешёвое холодное хранилище.
Приложение не знает, с кем оно говорит: код инициализации клиента одинаков. Разница только в переменных окружения: эндпоинте, регионе, ключах и флаге forcePathStyle, который нужен большинству S3-совместимых сервисов, включая MinIO и R2.
Важно: виртуальный хостинг bucket.example.com в MinIO работает, но на staging проще включить path-style и не мучиться с DNS-записями под каждый бакет.Что понадобится
- VPS с Linux, публичным IP и открытыми портами 80/443.
- Два A-записи:
minio-staging.example.comиminio-console.example.com. - Docker и Docker Compose v2.
- Reverse proxy с TLS — в примере Traefik v2 + Let’s Encrypt.
- Около 10 ГБ свободного места под тестовые данные.
Разворачиваем MinIO в Docker Compose
Минимальный docker-compose.staging.yml заводит один контейнер, внешнюю сеть для Traefik и именованный том для данных.
Два параметра критичны для presigned URL. MINIO_SERVER_URL говорит серверу, на каком публичном домене подписывать ссылки. Без него ссылка будет подписана для http://minio:9000 и браузер отклонит подпись. MINIO_BROWSER_REDIRECT_URL нужен для корректных редиректов веб-консоли.
Прокидываем HTTPS через Traefik
Добавляем лейблы к сервису minio, чтобы Traefik маршрутизировал API и консоль на разные порты и автоматически выпускал сертификаты.
Проверяем здоровье сервера с локальной машины: curl -I https://minio-staging.example.com/minio/health/live должен вернуть HTTP 200.
Cloudflare: для поддомена с API лучше выключить оранжевое облако. Бесплатный тариф Cloudflare обрезает тело запроса на 100 МБ и может убирать S3-заголовки, из-за чего ломается подпись.
Бакеты, политики и отдельный пользователь для приложения
После запуска создаём бакет и отдельного IAM-пользователя. Делать это root-ключами приложения — плохая идея: root может удалить всё.
Теперь создаём пользователя staging-app и IAM-политику, ограничивающую права только этим бакетом.
Логин staging-app и его секрет — это и есть S3_ACCESS_KEY и S3_SECRET_KEY для приложения.
Код приложения не меняется
Пример на AWS SDK v3 для Node.js. Обратите внимание на forcePathStyle: true: без него SDK попытается обратиться к bucket.minio-staging.example.com, и запрос уйдёт в никуда.
Переменные для staging:
Для продакшена — только другой набор значений, код идентичен.
Presigned URL: загрузка и скачивание без проксирования
Presigned URL — это обычный HTTPS URL с короткой подписью в query string. Кто угодно может выполнить ровно то действие, на которое выдана подпись: PUT для загрузки или GET для скачивания. Бэкенд проверяет права, подписывает URL и отдаёт клиенту — сам файл идёт напрямую в MinIO.
Загрузка из браузера
Важный подводный камень: Content-Type, который браузер отправляет при PUT, должен точно совпадать с тем, что было передано в PutObjectCommand. Иначе MinIO вернёт SignatureDoesNotMatch.
Скачивание приватных файлов
Почему это лучше проксирования: при прямой загрузке через ваше API все байты проходят через приложение, съедая CPU, RAM и пропускную способность. С presigned URL трафик идёт между клиентом и MinIO — бэкенд только подписывает ссылку.
CORS, lifecycle и безопасность
Несколько команд, которые стоит выполнить сразу после создания бакета.
CORS для браузерных загрузок
Автоудаление старых тестовых файлов
Шифрование данных в покое
И ещё раз: root-ключи храните в менеджере секретов и используйте только для mc admin. Консоль MinIO, если она доступна из интернета, закрывайте IP-allowlist или базовой авторизацией на уровне Traefik.
Бэкапы и мониторинг
Staging не должен хранить что-то ценное, но периодическое зеркалирование в дешёвое холодное хранилище спасает от случайного удаления. Команда mc mirror синхронизирует бакет в Backblaze B2, Yandex Object Storage или другой S3-совместимый бэкенд.
Для метрик MinIO отдаёт Prometheus-экспортёр по пути /minio/v2/metrics/cluster. В Grafana можно импортировать дашборд ID 13502 и сразу видеть занятое место, RPS, задержки и ошибки.
Типичные проблемы
- SignatureDoesNotMatch при PUT — браузер отправил Content-Type, отличный от подписанного. Проверьте заголовок PUT.
- Подписанная ссылка работает локально, но не в браузере — не задан MINIO_SERVER_URL. Ссылка подписана для внутреннего http://minio:9000.
- 403 после Cloudflare — бесплатный тариф Cloudflare модифицирует заголовки. Переведите A-запись в режим DNS-only.
- CORS preflight failed — на бакете не настроены CORS-правила.
- Консоль редиректит на http://minio:9001 — не задан MINIO_BROWSER_REDIRECT_URL.
Часто задаваемые вопросы
Можно ли использовать MinIO в продакшене?
Можно, если есть экспертиза и ресурсы на поддержку кластера. Но управляемые S3/R2/Selectel Object Storage проще в эксплуатации: репликация, резервирование и дежурства уже на стороне провайдера. На staging MinIO часто выигрывает по цене и скорости развёртывания.
Что такое path-style и почему его включают?
Path-style означает, что бакет идёт в пути URL: example.com/bucket/key. Виртуальный хостинг — bucket.example.com/key. MinIO поддерживает оба варианта, но path-style не требует дополнительных DNS-записей, поэтому удобнее на staging.
Какие SDK совместимы с MinIO?
Практически все S3-SDK: AWS SDK для JavaScript, Python boto3, Go aws-sdk-go, Java aws-sdk-java, а также официальный minio-js/minio-py. Главное — указать правильный endpoint, region и forcePathStyle.
Нужен ли отдельный бакет под каждое окружение?
Не обязательно, но удобно. Можно завести бакеты staging-files, review-files, demo-files и политики с разным доступом. Это упрощает lifecycle-правила и изоляцию данных.
Как ограничить размер загружаемых файлов?
MinIO не ограничивает размер объекта, но обычно перед ним стоит reverse proxy. В Traefik лимита по умолчанию нет, а в NGINX используйте client_max_body_size. Если стоит Cloudflare, помните о лимите 100 МБ на бесплатном тарифе.
Выводы
Self-hosted MinIO на staging — это не попытка заменить облако, а способ сделать тестовую среду дешевле и ближе к продакшену. Тот же API, те же SDK, те же presigned URL, но без счетов за хранение битых файлов и ночных сбросов базы.
Ключевые моменты, которые стоит запомнить: всегда указывайте MINIO_SERVER_URL для корректных подписей, не используйте root-ключи в приложении, включайте path-style на staging и настраивайте lifecycle, чтобы мусор не копился.
Самое дорогое в staging — не железо, а различия в кодовых путях между dev и prod. MinIO помогает убрать одну из этих разниц почти бесплатно.
Источник: freeCodeCamp — How to Self-Host an S3-Compatible Object Store with MinIO on Your Staging Server.