Разворачиваем MinIO на VPS, настраиваем HTTPS через Traefik и presigned URL для загрузки файлов. Экономим на облачном S3 на этапе разработки.— Читать дальше «Как поднять свой S3-совместимый объектный склад на MinIO для staging»
Если в приложении есть загрузка файлов — аватары, документы, отчёты, записи звонков — каждый тестовый файл на staging утекает в облачный счёт. AWS S3, Cloudflare R2 и Yandex Object Storage берут деньги за хранение и трафик, а в staging это чистый перерасход: тут нет SLA, зато полно битых загрузок, ночных сбросов базы и файлов, которые никто не удаляет.
Выход — поднять собственное S3-совместимое хранилище на том же VPS, где крутится staging. MinIO реализует API Amazon S3, понимает те же SDK и presigned URL, но стоит ровно столько, сколько стоит диск сервера. Один и тот же код приложения работает и с MinIO на dev, и с R2 в проде — меняются только переменные окружения.
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.
В продакшене обычно используется управляемое хранилище: AWS S3, Cloudflare R2, Yandex Object Storage, Hetzner Object Storage. Там работают репликация, резервное копирование и чужой дежурный. В staging достаточно одного MinIO-контейнера на сервере с регулярным зеркалированием в дешёвое холодное хранилище.
Приложение не знает, с кем оно говорит: код инициализации клиента одинаков. Разница только в переменных окружения: эндпоинте, регионе, ключах и флаге forcePathStyle, который нужен большинству S3-совместимых сервисов, включая MinIO и R2.
Важно: виртуальный хостинг bucket.example.com в MinIO работает, но на staging проще включить path-style и не мучиться с DNS-записями под каждый бакет.minio-staging.example.com и minio-console.example.com.Минимальный docker-compose.staging.yml заводит один контейнер, внешнюю сеть для Traefik и именованный том для данных.
# docker-compose.staging.yml
networks:
proxy:
external: true
name: proxy
internal:
name: internal
volumes:
minio-data:
services:
minio:
image: minio/minio:latest
container_name: minio-staging
restart: unless-stopped
environment:
- MINIO_ROOT_USER=${MINIO_ROOT_USER:-admin}
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-change-me-please}
- MINIO_SERVER_URL=https://minio-staging.example.com
- MINIO_BROWSER_REDIRECT_URL=https://minio-console.example.com
command: server /data --console-address ":9001"
volumes:
- minio-data:/data
networks:
- proxy
- internal
ports:
- "9000:9000"
- "9001:9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
Два параметра критичны для presigned URL. MINIO_SERVER_URL говорит серверу, на каком публичном домене подписывать ссылки. Без него ссылка будет подписана для http://minio:9000 и браузер отклонит подпись. MINIO_BROWSER_REDIRECT_URL нужен для корректных редиректов веб-консоли.
Добавляем лейблы к сервису minio, чтобы Traefik маршрутизировал API и консоль на разные порты и автоматически выпускал сертификаты.
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
# S3 API
- "traefik.http.routers.minio-staging.rule=Host(`minio-staging.example.com`)"
- "traefik.http.routers.minio-staging.entrypoints=websecure"
- "traefik.http.routers.minio-staging.tls.certresolver=letsencrypt"
- "traefik.http.routers.minio-staging.service=minio-staging"
- "traefik.http.services.minio-staging.loadbalancer.server.port=9000"
# Web Console
- "traefik.http.routers.minio-console.rule=Host(`minio-console.example.com`)"
- "traefik.http.routers.minio-console.entrypoints=websecure"
- "traefik.http.routers.minio-console.tls.certresolver=letsencrypt"
- "traefik.http.routers.minio-console.service=minio-console"
- "traefik.http.services.minio-console.loadbalancer.server.port=9001"
Проверяем здоровье сервера с локальной машины: curl -I https://minio-staging.example.com/minio/health/live должен вернуть HTTP 200.
Cloudflare: для поддомена с API лучше выключить оранжевое облако. Бесплатный тариф Cloudflare обрезает тело запроса на 100 МБ и может убирать S3-заголовки, из-за чего ломается подпись.
После запуска создаём бакет и отдельного IAM-пользователя. Делать это root-ключами приложения — плохая идея: root может удалить всё.
# Подключаем mc к локальному MinIO
docker exec -it minio-staging \
mc alias set local http://localhost:9000 admin change-me-please
# Создаём бакет
docker exec -it minio-staging mc mb local/staging-files
# Делаем его приватным
docker exec -it minio-staging mc anonymous set none local/staging-files
Теперь создаём пользователя staging-app и IAM-политику, ограничивающую права только этим бакетом.
cat > /tmp/policy.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:*"],
"Resource": [
"arn:aws:s3:::staging-files",
"arn:aws:s3:::staging-files/*"
]
}
]
}
EOF
docker cp /tmp/policy.json minio-staging:/tmp/policy.json
docker exec -it minio-staging mc admin policy create local staging-rw /tmp/policy.json
docker exec -it minio-staging mc admin user add local staging-app a-long-random-secret-key
docker exec -it minio-staging mc admin policy attach local staging-rw --user staging-app
Логин staging-app и его секрет — это и есть S3_ACCESS_KEY и S3_SECRET_KEY для приложения.
Пример на AWS SDK v3 для Node.js. Обратите внимание на forcePathStyle: true: без него SDK попытается обратиться к bucket.minio-staging.example.com, и запрос уйдёт в никуда.
// src/lib/s3.js
import { S3Client } from "@aws-sdk/client-s3";
export const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY,
secretAccessKey: process.env.S3_SECRET_KEY,
},
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === "true",
});
export const BUCKET = process.env.S3_BUCKET;
Переменные для staging:
S3_ENDPOINT=https://minio-staging.example.com
S3_PUBLIC_ENDPOINT=https://minio-staging.example.com
S3_BUCKET=staging-files
S3_ACCESS_KEY=staging-app
S3_SECRET_KEY=a-long-random-secret-key
S3_REGION=us-east-1
S3_FORCE_PATH_STYLE=true
Для продакшена — только другой набор значений, код идентичен.
Presigned URL — это обычный HTTPS URL с короткой подписью в query string. Кто угодно может выполнить ровно то действие, на которое выдана подпись: PUT для загрузки или GET для скачивания. Бэкенд проверяет права, подписывает URL и отдаёт клиенту — сам файл идёт напрямую в MinIO.
// src/lib/presign.js
import { PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { s3, BUCKET } from "./s3.js";
import { randomUUID } from "node:crypto";
export async function presignUpload({ filename, contentType, userId }) {
const key = `users/${userId}/${randomUUID()}-${filename}`;
const cmd = new PutObjectCommand({
Bucket: BUCKET,
Key: key,
ContentType: contentType,
});
const uploadUrl = await getSignedUrl(s3, cmd, { expiresIn: 60 * 5 });
return { uploadUrl, key };
}
Важный подводный камень: Content-Type, который браузер отправляет при PUT, должен точно совпадать с тем, что было передано в PutObjectCommand. Иначе MinIO вернёт SignatureDoesNotMatch.
export async function presignDownload(key, expiresIn = 60 * 10) {
const cmd = new GetObjectCommand({ Bucket: BUCKET, Key: key });
return getSignedUrl(s3, cmd, { expiresIn });
}
// Эндпоинт в приложении
app.get("/api/documents/:id/url", requireAuth, async (req, res) => {
const doc = await db.documents.findByPk(req.params.id);
if (!doc || !canUserSee(req.user, doc)) return res.sendStatus(403);
const url = await presignDownload(doc.key, 600);
res.json({ url });
});
Почему это лучше проксирования: при прямой загрузке через ваше API все байты проходят через приложение, съедая CPU, RAM и пропускную способность. С presigned URL трафик идёт между клиентом и MinIO — бэкенд только подписывает ссылку.
Несколько команд, которые стоит выполнить сразу после создания бакета.
cat > /tmp/cors.json <<'EOF'
{
"CORSRules": [
{
"AllowedOrigins": ["https://app-staging.example.com", "http://localhost:3000"],
"AllowedMethods": ["GET", "PUT", "POST", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]
}
EOF
docker cp /tmp/cors.json minio-staging:/tmp/cors.json
docker exec -it minio-staging mc cors set local/staging-files /tmp/cors.json
docker exec -it minio-staging mc ilm rule add --expire-days 30 local/staging-files
docker exec -it minio-staging mc encrypt set sse-s3 local/staging-files
И ещё раз: root-ключи храните в менеджере секретов и используйте только для mc admin. Консоль MinIO, если она доступна из интернета, закрывайте IP-allowlist или базовой авторизацией на уровне Traefik.
Staging не должен хранить что-то ценное, но периодическое зеркалирование в дешёвое холодное хранилище спасает от случайного удаления. Команда mc mirror синхронизирует бакет в Backblaze B2, Yandex Object Storage или другой S3-совместимый бэкенд.
mc alias set cold https://s3.us-east-005.backblazeb2.com $KEY $SECRET
mc mirror --overwrite --remove staging/staging-files cold/staging-backup
Для метрик MinIO отдаёт Prometheus-экспортёр по пути /minio/v2/metrics/cluster. В Grafana можно импортировать дашборд ID 13502 и сразу видеть занятое место, RPS, задержки и ошибки.
Часто задаваемые вопросы
1
Можно ли использовать MinIO в продакшене?
Можно, если есть экспертиза и ресурсы на поддержку кластера. Но управляемые S3/R2/Selectel Object Storage проще в эксплуатации: репликация, резервирование и дежурства уже на стороне провайдера. На staging MinIO часто выигрывает по цене и скорости развёртывания.
2
Что такое path-style и почему его включают?
Path-style означает, что бакет идёт в пути URL: example.com/bucket/key. Виртуальный хостинг — bucket.example.com/key. MinIO поддерживает оба варианта, но path-style не требует дополнительных DNS-записей, поэтому удобнее на staging.
3
Какие SDK совместимы с MinIO?
Практически все S3-SDK: AWS SDK для JavaScript, Python boto3, Go aws-sdk-go, Java aws-sdk-java, а также официальный minio-js/minio-py. Главное — указать правильный endpoint, region и forcePathStyle.
4
Нужен ли отдельный бакет под каждое окружение?
Не обязательно, но удобно. Можно завести бакеты staging-files, review-files, demo-files и политики с разным доступом. Это упрощает lifecycle-правила и изоляцию данных.
5
Как ограничить размер загружаемых файлов?
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 помогает убрать одну из этих разниц почти бесплатно.
Команда tprogerРедакция
Источник: freeCodeCamp — How to Self-Host an S3-Compatible Object Store with MinIO on Your Staging Server.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Как выбрать VPS/VDS под свой проект: гайд по параметрам и 6 провайдеров | 0 | 8 | 30-06-2026 |
| 2 | Зарубежный хостинг для сайта: ТОП-25 иностранных хостинг-провайдеров | 0 | 5 | 26-06-2026 |
| 3 | VPS vs VDS vs виртуальный хостинг: что выбрать в 2026 году | 0 | 7 | 30-06-2026 |
| 4 | Кроссплатформенный VPS: 6 провайдеров с Linux и Windows в 2026 году | 0 | 8 | 30-06-2026 |
| 5 | Deckhouse Conf 2026: зачем инженерам самописный SDN и виртуалки в Kubernetes | 0 | 5 | 02-07-2026 |
| 6 | Как объединить инфраструктуру 35 продуктов в единую платформу данных | 0 | 7 | 26-06-2026 |
| 7 | Рег.облако открыл корпоративное объектное хранилище S3 с полной изоляцией данных | 5 | 7 | 25-11-2025 |
| 8 | Рег.ру привлек в S3 собственной разработки более тысячи новых клиентов за полгода | 5 | 7 | 31-07-2025 |
| 9 | Рег.облако увеличил облачное хранилище в 40 раз | 5 | 7 | 06-03-2026 |