Claude Code with MinIO: Self-Hosted S3 for Developers
MinIO is the open-source object store that drops in as a replacement for Amazon S3. Same API, same client libraries, same access patterns. The difference is you run it. That means lower cost at scale, full control over where the data lives, and the ability to run identical infrastructure in dev, staging, and production without paying per-GB twice. Claude Code is a strong partner for MinIO work because the S3 API surface is well-known and the patterns are mechanical.
This post covers how to use Claude Code with MinIO across the workflows that come up most often: setting up buckets, uploading and serving files, signing URLs, replicating across regions, and the production patterns that hold up.
Why MinIO over native S3 or another store
The case for MinIO is not "S3 is bad". The case is that you sometimes want object storage that:
- Runs on hardware you control (HIPAA, GDPR, customer self-hosting deals).
- Costs less at high volume (no per-GB egress fees within your data centre).
- Works identically across local dev, staging, and production.
- Stays available when AWS is having a regional incident.
For most projects under 10TB of storage, native S3 is fine. For projects approaching or beyond that, or with strict data residency requirements, MinIO earns its place. It is also a clean way to run S3-compatible storage on a Raspberry Pi, a homelab, or any small cluster.
If you want managed object storage with a usage-based billing model, our guide to Claude Code with Cloudflare R2 covers that pattern.
Running MinIO locally
The fastest path to a working MinIO is Docker. Add this to your docker-compose.yml:
services:
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
ports:
- "9000:9000" # API
- "9001:9001" # Console
volumes:
- minio-data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
volumes:
minio-data:
docker compose up -d minio
The S3 API is at http://localhost:9000. The web console is at http://localhost:9001 (log in with minioadmin / minioadmin). For production, generate strong root credentials and immediately create scoped users with limited permissions.
You can also install the mc CLI for scripted bucket management:
brew install minio/stable/mc
mc alias set local http://localhost:9000 minioadmin minioadmin
mc mb local/uploads
Pattern 1: S3 SDK against MinIO
Because MinIO speaks the S3 API, you use the AWS S3 SDK in your application. There is no MinIO-specific library you need to learn. The only change is the endpoint URL.
// lib/storage.ts
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3"
export const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT, // http://localhost:9000 in dev
region: process.env.S3_REGION ?? "us-east-1",
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY!,
secretAccessKey: process.env.S3_SECRET_KEY!,
},
forcePathStyle: true, // required for MinIO
})
export async function uploadFile(key: string, body: Buffer, contentType: string) {
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET!,
Key: key,
Body: body,
ContentType: contentType,
}))
}
The forcePathStyle: true flag is the only MinIO-specific detail. Without it, the SDK tries virtual-host style URLs that MinIO does not support by default.
In production, set S3_ENDPOINT to your MinIO cluster's public URL. In dev, point at the Docker container. Everything else stays the same. This is the whole reason to use MinIO: identical code paths, different storage backends per environment.
Claude Code is good at scaffolding the upload, download, and delete helpers around this client. Ask it to "write a typed storage layer with upload, download, and signed URL helpers" and you get a working module in one pass.
Pattern 2: Presigned URLs for direct uploads
Streaming user uploads through your API server is wasteful. The better pattern is presigned URLs: your server signs a URL that the browser uses to PUT directly to MinIO. The bytes never touch your application.
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
export async function presignUpload(key: string, contentType: string) {
const cmd = new PutObjectCommand({
Bucket: process.env.S3_BUCKET!,
Key: key,
ContentType: contentType,
})
return getSignedUrl(s3, cmd, { expiresIn: 600 }) // 10 minutes
}
The frontend:
async function upload(file: File) {
const { url, key } = await fetch("/api/upload-url", {
method: "POST",
body: JSON.stringify({ contentType: file.type }),
}).then(r => r.json())
await fetch(url, {
method: "PUT",
body: file,
headers: { "Content-Type": file.type },
})
// Now register the upload in your DB by key
await fetch("/api/uploads", {
method: "POST",
body: JSON.stringify({ key }),
})
}
For MinIO, the URL host needs to be reachable from the browser. If your MinIO is at http://localhost:9000 in dev, that works. In production, expose MinIO behind a real domain like https://storage.example.com and update S3_ENDPOINT accordingly. CORS needs configuring on the bucket to allow the frontend origin.
Get Claudify. A Claude Code starter kit with S3-compatible storage patterns ready to use.
Pattern 3: Bucket policies and scoped access
You should not use root credentials in your application. Create a scoped user per service:
# Create a user
mc admin user add local app-uploader $(openssl rand -hex 16)
# Create a policy
cat > /tmp/uploader-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"],
"Resource": ["arn:aws:s3:::uploads/*"]
},
{
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": ["arn:aws:s3:::uploads"]
}
]
}
EOF
mc admin policy create local uploader-policy /tmp/uploader-policy.json
mc admin policy attach local uploader-policy --user app-uploader
The application uses these scoped credentials. The blast radius of a leaked key is contained to that bucket and those actions. Claude Code can wrap these calls into a bootstrap script that runs on cluster setup.
Pattern 4: Replication for HA
A single MinIO instance is a single point of failure. For production, run a distributed MinIO cluster with replication.
The minimum production setup is four nodes with erasure coding:
docker run -d --name minio1 \
-e MINIO_ROOT_USER=admin \
-e MINIO_ROOT_PASSWORD=$STRONG_PASSWORD \
-v /mnt/data1:/data \
minio/minio server \
http://minio{1...4}/data \
--console-address ":9001"
# Same on minio2, minio3, minio4 with their respective data volumes
Erasure coding spreads each object across the four nodes so two can fail without data loss. For cross-region replication (active-active between two clusters):
mc admin replicate add cluster-a cluster-b
That sets up bucket replication between two clusters with their own MinIO deployments. Any write to one propagates to the other. This is the pattern for "regionally redundant object storage we operate ourselves".
For replication patterns at the database layer, see our guide on Claude Code with PostgreSQL.
Pattern 5: Lifecycle policies and tiering
Storage cost adds up. MinIO supports lifecycle rules so old objects get archived or deleted automatically.
mc ilm rule add local/uploads \
--expire-days 365 \
--noncurrent-expire-days 30
This expires objects after a year and removes non-current versions after 30 days. You can also tier to cold storage:
mc ilm tier add local cold-tier \
--endpoint https://cold-storage.example.com \
--access-key $COLD_KEY \
--secret-key $COLD_SECRET \
--bucket cold-archive
mc ilm rule add local/uploads \
--transition-days 90 \
--transition-tier cold-tier
Files older than 90 days move to the cold tier. Reads still work transparently. Cost drops because cold storage is cheaper. This works the same way you would set up S3 tiering, which means Claude Code's prior knowledge of S3 lifecycle policies transfers directly.
Pattern 6: Using MinIO from Claude Code agents
Object storage is often where AI workflows park their intermediate artefacts. Embeddings indexes, model outputs, generated assets, training data snapshots. The pattern that works:
- Each agent run gets a unique prefix in a shared bucket:
runs/2026-05-23/abc123/. - Inputs are uploaded with presigned URLs by the agent at start.
- Outputs are written under the same prefix as the run progresses.
- A lifecycle policy expires the prefix after N days.
This gives you durable artefact storage across agent runs without polluting your application database. Claude Code can drive both the upload and the cleanup as part of its workflow. See our guide on Claude Code custom subagents for how to structure this.
Common pitfalls
Browser CORS errors. When using presigned URLs from a browser, the bucket needs a CORS policy:
[
{
"AllowedOrigin": ["https://app.example.com"],
"AllowedMethod": ["PUT", "GET"],
"AllowedHeader": ["*"],
"ExposeHeader": ["ETag"]
}
]
Apply with mc anonymous set-json /tmp/cors.json local/uploads.
Path-style versus virtual-host-style. MinIO defaults to path-style (http://endpoint/bucket/key). AWS S3 defaults to virtual-host (http://bucket.endpoint/key). Your client library needs to be told which one to use. The forcePathStyle: true flag in the AWS SDK is essential.
HTTPS in production. MinIO supports TLS natively. Generate or provision certs, mount them into the container at /root/.minio/certs/, and MinIO will serve HTTPS automatically. Behind a reverse proxy like Caddy, this is even simpler.
Performance under load. A single-node MinIO instance can saturate a 1Gbps NIC pretty fast on large objects. For high throughput, run a distributed cluster and use multipart upload (the SDK does this automatically for files above 5MB).
Conclusion
MinIO is the S3-compatible store you run yourself. It is the right tool when you need control over data residency, want predictable costs at scale, or want identical storage in dev and prod. Claude Code is the right partner because S3 patterns are well-understood and the integration is mechanical: scaffold the SDK calls, generate the bucket policies, write the bootstrap scripts.
If you want a Claude Code project that already understands S3-compatible storage patterns, with example presigned URL flows and a working CLAUDE.md, that is what Claudify packages.
Get Claudify. The official Claude Code starter kit for developers shipping production storage and asset pipelines.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify