MinIO как локальный S3
Большая часть DE-пайплайнов сегодня работает с S3: data lake на S3 (или S3-совместимый GCS/Cloudflare R2), Parquet-файлы на S3, dump’ы Postgres на S3, intermediate-данные между шагами Airflow на S3. Когда ты пишешь DAG на localhost, поднимать настоящий AWS-аккаунт для тестов — это медленно (round-trip к AWS US-East), дорого (storage + GET requests) и хрупко (credentials в коде, забыл — закоммитил).
MinIO — это open-source S3-совместимый сторадж, написанный на Go. Одна команда docker run — и у тебя локально работает «AWS S3». Любой клиент, который умеет S3 API (boto3, AWS CLI, s3fs, polars, Spark) — работает без изменений кода, нужно только указать другой endpoint URL.
Анатомия HTTP-запроса — что внутри request и response
Что такое MinIO
MinIO появился в 2014 как distributed object storage для on-premise / private cloud. Сейчас это две вещи:
- MinIO Server — сам сервер, S3 API-совместимый. Образ
minio/minioна Docker Hub. - MinIO Client (
mc) — CLI, по аналогии сaws s3, но проще и быстрее. Образminio/mc.
S3-совместимость значит: те же endpoint’ы (PUT /bucket/key, GET /bucket/key, LIST /bucket?prefix=...), та же signed-URL-логика, тот же multipart upload. Не 100% (некоторые edge-cases типа Object Lock Legal Hold отличаются), но 99% реальных DE-задач идут без модификаций.
В production AWS S3 заменяется на настоящий S3 / GCS / R2 одной правкой env-переменной endpoint URL. Это и есть смысл — локально пишешь и тестируешь без сети до AWS, в проде — обычный AWS.
Запуск MinIO
Минимальная команда:
docker run -d \
--name minio \
-p 9000:9000 \
-p 9001:9001 \
-e MINIO_ROOT_USER=admin \
-e MINIO_ROOT_PASSWORD=admin12345 \
-v minio-data:/data \
minio/minio server /data --console-address ":9001"
Разберём по строкам:
-p 9000:9000— S3 API endpoint. Сюда стучатся boto3 / mc / aws cli.-p 9001:9001— Web Console. Открываешь в браузереhttp://localhost:9001, логинишься admin/admin12345.MINIO_ROOT_USER/MINIO_ROOT_PASSWORD— root credentials. Пароль минимум 8 символов, иначе MinIO откажется стартовать. В проде, естественно, неadmin12345.-v minio-data:/data— volume для самих объектов. MinIO хранит бакеты как поддиректории в/data.minio/minio server /data --console-address ":9001"— это команда внутри контейнера.server /data= запустить сервер с data-dir =/data. Флаг--console-addressвыносит UI на отдельный порт.
Запустим, проверим логи:
docker logs minio --tail 10
В выводе увидим что-то типа:
MinIO Object Storage Server
Copyright: 2015-2026 MinIO, Inc.
License: GNU AGPLv3
Version: RELEASE.2026-04-15T19-23-45Z (go1.23.0 linux/amd64)
API: http://172.17.0.2:9000 http://127.0.0.1:9000
WebUI: http://172.17.0.2:9001 http://127.0.0.1:9001
Docs: https://docs.min.io
Открой http://localhost:9001 в браузере, залогинься admin/admin12345 — увидишь Web Console с пустым списком бакетов. Через UI можно создавать бакеты, заливать файлы, генерить access keys. Но мы будем делать это из CLI — это более воспроизводимо и удобно для скриптов.
Создание bucket через mc
mc — это MinIO Client, аналог aws s3. Можно установить локально (brew install minio/stable/mc на macOS) или запускать в контейнере:
docker run --rm -it \
--network host \
minio/mc \
alias set local http://localhost:9000 admin admin12345
Команда alias set local ... создаёт алиас local для нашего MinIO. После этого все mc-команды можно адресовать через local/bucket/key.
--network host нужно, чтобы из контейнера mc дотянуться до localhost:9000, который пробрасывает другой контейнер. На macOS / Windows с Docker Desktop --network host не работает так же, как на Linux — там надо использовать host.docker.internal:9000. На OrbStack — --network host работает.
Альтернатива — установить mc локально:
brew install minio/stable/mc
mc alias set local http://localhost:9000 admin admin12345
mc mb local/datalake
mc ls local
mc mb local/datalake создаст bucket datalake. mc ls local покажет список бакетов:
[2026-05-15 09:35:42 MSK] 0B datalake/
Положим файл:
echo "hello s3" > test.txt
mc cp test.txt local/datalake/test.txt
mc ls local/datalake
mc cat local/datalake/test.txt
Вывод:
[2026-05-15 09:36:10 MSK] 9B STANDARD test.txt
hello s3
Работает как S3.
Подключение через AWS CLI
Можно использовать привычный AWS CLI с флагом --endpoint-url:
aws configure set aws_access_key_id admin
aws configure set aws_secret_access_key admin12345
aws --endpoint-url http://localhost:9000 s3 ls
aws --endpoint-url http://localhost:9000 s3 mb s3://logs
aws --endpoint-url http://localhost:9000 s3 cp test.txt s3://logs/test.txt
aws --endpoint-url http://localhost:9000 s3 ls s3://logs/
Ровно та же логика, что и с настоящим S3 — только --endpoint-url меняется. В скриптах можно завести env-переменную:
export AWS_ENDPOINT_URL=http://localhost:9000
aws s3 ls # пойдёт в MinIO
unset AWS_ENDPOINT_URL
aws s3 ls # пойдёт в настоящий AWS
Подключение из Python (boto3)
Это самый частый use case для DE — Python-скрипт, который пишет Parquet в MinIO:
import boto3
s3 = boto3.client(
"s3",
endpoint_url="http://localhost:9000",
aws_access_key_id="admin",
aws_secret_access_key="admin12345",
region_name="us-east-1", # MinIO игнорирует, но boto3 требует
)
s3.create_bucket(Bucket="warehouse")
s3.put_object(
Bucket="warehouse",
Key="raw/users.json",
Body=b'{"id": 1, "name": "Alice"}',
)
for obj in s3.list_objects_v2(Bucket="warehouse")["Contents"]:
print(obj["Key"], obj["Size"])
Если убрать endpoint_url, тот же код пойдёт в настоящий AWS S3 (если есть AWS credentials в ~/.aws/credentials). Это означает, что pipeline-код тестируется локально без модификаций.
Compose-вариант MinIO
В реальных DE-стендах MinIO живёт рядом с Postgres и Airflow в одном compose-файле:
services:
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: admin
MINIO_ROOT_PASSWORD: admin12345
volumes:
- minio-data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 5s
timeout: 3s
retries: 5
minio-init:
image: minio/mc:latest
depends_on:
minio:
condition: service_healthy
entrypoint: >
/bin/sh -c "
mc alias set local http://minio:9000 admin admin12345;
mc mb -p local/datalake;
mc mb -p local/warehouse;
mc mb -p local/logs;
exit 0;
"
volumes:
minio-data:
Сервис minio-init — это одноразовый init-container: ждёт healthy MinIO, создаёт бакеты, выходит. После docker compose up -d есть готовые 3 bucket’а.
Реальный кейс: ETL pipeline на MinIO локально
Допустим, ты пишешь DAG, который читает Parquet из data lake и пишет агрегаты обратно. В проде это AWS S3, локально — MinIO. Pipeline:
import polars as pl
import boto3
import os
S3_ENDPOINT = os.getenv("S3_ENDPOINT_URL")
S3_KEY = os.getenv("AWS_ACCESS_KEY_ID")
S3_SECRET = os.getenv("AWS_SECRET_ACCESS_KEY")
storage_options = {
"aws_endpoint_url": S3_ENDPOINT,
"aws_access_key_id": S3_KEY,
"aws_secret_access_key": S3_SECRET,
"aws_region": "us-east-1",
}
df = pl.read_parquet(
"s3://datalake/raw/events/2026-05-15/*.parquet",
storage_options=storage_options,
)
agg = df.group_by("user_id").agg(pl.count().alias("events_total"))
agg.write_parquet(
"s3://datalake/agg/users/2026-05-15.parquet",
storage_options=storage_options,
)
Локально запускаем с S3_ENDPOINT_URL=http://localhost:9000 и admin/admin12345 — pipeline читает и пишет в MinIO. В проде убираем S3_ENDPOINT_URL, ставим реальные AWS credentials — тот же код идёт в AWS S3. Pipeline не знает, что под ним.
Попробуй сам
- Подними MinIO с командой выше (с
MINIO_ROOT_USER=admin,MINIO_ROOT_PASSWORD=admin12345). - Открой
http://localhost:9001, залогинься. Создай bucketlabчерез UI. - Поставь
mc(brew install minio/stable/mcилиapt install mc) и сделайmc alias set local http://localhost:9000 admin admin12345. - Залей файл:
echo "data" > a.txt && mc cp a.txt local/lab/a.txt. - Из Python (
pip install boto3) подключись кendpoint_url=http://localhost:9000, выведи список объектов вlab. - Удали MinIO-контейнер (
docker rm -f minio), запусти снова с тем же volume — bucketlabи файлa.txtдолжны остаться.