MinIO Self-Host Setup: The Developer's Guide to S3-Compatible Storage
AWS S3 is the gold standard for object storage, but the costs add up fast — especially when you're storing terabytes of backups, media, or application assets. MinIO is a high-performance, S3-compatible object store that you can run yourself. Same API. Same SDKs. Zero per-GB egress fees. This guide shows you exactly how to set up MinIO with Docker Compose, erasure coding for data protection, HTTPS, and proper access controls.
If you're integrating MinIO into a larger stack, check out our guides for Deploy Budibase with Docker Compose + Caddy + CouchDB + Redis + MinIO on Ubuntu and Deploy Plane with Docker Compose + Caddy + PostgreSQL + Redis + MinIO on Ubuntu. For a systemd + Nginx approach, see Production Guide: Deploy MinIO with systemd + NGINX on Ubuntu.
What You'll Need Before You Start
MinIO is lightweight for basic setups, but production storage deserves proper specs.
- VPS or bare metal server with 2 CPU cores, 4GB RAM, and 50GB+ disk minimum
- Ubuntu 22.04/24.04 or Debian 12 with Docker and Docker Compose v2 installed
- A domain or subdomain pointed at your server (required for HTTPS and presigned URLs)
- One or more block storage volumes for data (local disk is fine to start; attach dedicated volumes for production)
- Basic familiarity with S3 concepts, Docker, and reverse proxies
Standalone MinIO works for development. For production, this guide uses a multi-drive setup with erasure coding so a disk failure doesn't mean data loss.
1. Deploy MinIO with Docker Compose
Create a project directory and drop in a Compose file that runs MinIO with four data drives for erasure coding.
sudo mkdir -p /opt/minio && cd /opt/minio
sudo mkdir -p data/{1,2,3,4}
sudo chown -R $USER:$USER /opt/minio
Create the Docker Compose file:
version: "3.8"
services:
minio:
image: quay.io/minio/minio:RELEASE.2026-06-01T-xxxZ
container_name: minio
restart: unless-stopped
command: server /data/{1...4} --console-address ":9001"
environment:
- MINIO_ROOT_USER=admin
- MINIO_ROOT_PASSWORD=***
- MINIO_SERVER_URL=https://s3.yourdomain.com
- MINIO_BROWSER_REDIRECT_URL=https://console.yourdomain.com
volumes:
- ./data/1:/data/1
- ./data/2:/data/2
- ./data/3:/data/3
- ./data/4:/data/4
ports:
- "9000:9000"
- "9001:9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
Start it:
docker compose up -d
docker compose logs -f minio
Visit http://your-server-ip:9001 and log in with admin / changeme. You now have a working MinIO instance with erasure coding across four drives.
Why four drives?
MinIO's erasure coding protects data against drive failures. With four drives, you can lose one drive and still recover all data. With eight drives, you can lose two. The formula is simple: half your drives store data, half store parity. More drives = more resilience and better performance.
2. Configure the MinIO Client (mc)
The MinIO Client mc is your Swiss Army knife for managing buckets, policies, users, and replication. Install it on your host or run it from a container.
Install mc on the host
curl -O https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc
sudo mv mc /usr/local/bin/
Alias your local server
mc alias set local http://localhost:9000 admin ***
Create a bucket
mc mb local/backups
mc mb local/app-assets
Set bucket policies
# Private bucket (default) — good for backups
mc anonymous set none local/backups
# Public read for static assets
mc anonymous set download local/app-assets
Create a dedicated app user
Never use root credentials in your applications. Create scoped users with limited permissions:
mc admin user add local app-user a-long-random-secret-key
Then create a policy JSON file and attach it:
cat > /opt/minio/app-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:*"],
"Resource": [
"arn:aws:s3:::app-assets",
"arn:aws:s3:::app-assets/*"
]
}
]
}
EOF
mc admin policy create local app-rw /opt/minio/app-policy.json
mc admin policy attach local app-rw --user app-user
3. Add HTTPS with a Reverse Proxy
MinIO serves the S3 API on port 9000 and the web console on port 9001. Both need TLS in production. Use Caddy, Nginx, or Traefik to terminate HTTPS.
Caddy (simplest)
# Caddyfile
s3.yourdomain.com {
reverse_proxy localhost:9000
}
console.yourdomain.com {
reverse_proxy localhost:9001
}
Run Caddy in Docker alongside MinIO:
services:
caddy:
image: caddy:2
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
volumes:
caddy-data:
caddy-config:
Nginx (if you already use it)
server {
listen 443 ssl http2;
server_name s3.yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
client_max_body_size 0;
location / {
proxy_pass http://localhost:9000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_request_buffering off;
}
}
After setting up the reverse proxy, verify that MINIO_SERVER_URL and MINIO_BROWSER_REDIRECT_URL point to your HTTPS domains. Without this, presigned URLs and console redirects will break.
4. Connect Your Application
Any S3-compatible SDK works with MinIO. The only differences are the endpoint, credentials, and forcePathStyle: true.
Node.js / AWS SDK v3
import { S3Client } from "@aws-sdk/client-s3";
const s3 = new S3Client({
endpoint: "https://s3.yourdomain.com",
region: "us-east-1",
credentials: {
accessKeyId: "app-user",
secretAccessKey: "a-long-random-secret-key",
},
forcePathStyle: true,
});
Python / boto3
import boto3
s3 = boto3.client(
"s3",
endpoint_url="https://s3.yourdomain.com",
aws_access_key_id="app-user",
aws_secret_access_key="a-long-random-secret-key",
region_name="us-east-1",
)
Go / minio-go
import "github.com/minio/minio-go/v7"
minioClient, err := minio.New("s3.yourdomain.com:443", &minio.Options{
Creds: credentials.NewStaticV4("app-user", "a-long-random-secret-key", ""),
Secure: true,
})
5. Generate Presigned URLs
Presigned URLs let users upload or download directly from MinIO without your server handling the bytes. The URL contains a time-limited signature.
Presigned PUT (upload)
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const command = new PutObjectCommand({
Bucket: "app-assets",
Key: "uploads/photo.jpg",
ContentType: "image/jpeg",
});
const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 300 });
// Give this URL to the browser; it PUTs the file directly to MinIO
Presigned GET (download)
import { GetObjectCommand } from "@aws-sdk/client-s3";
const command = new GetObjectCommand({
Bucket: "app-assets",
Key: "uploads/photo.jpg",
});
const downloadUrl = await getSignedUrl(s3, command, { expiresIn: 600 });
Critical: the Content-Type used during the actual PUT must match what was signed, or MinIO rejects it with SignatureDoesNotMatch.
6. Back Up and Monitor
Erasure coding protects against drive failure, not deletion or ransomware. Back up your buckets to another location.
Mirror to a remote MinIO or S3
# Add a remote target
mc alias set remote https://backup-s3.example.com backup-user backup-pass
# Mirror a bucket continuously
mc mirror --watch local/backups remote/backups
Check server health
curl -f http://localhost:9000/minio/health/live # liveness
curl -f http://localhost:9000/minio/health/ready # readiness
mc admin info local
Set up bucket lifecycle rules
Auto-delete old files to control disk growth:
cat > /tmp/lifecycle.json << 'EOF'
{
"Rules": [
{
"ID": "expire-old-logs",
"Status": "Enabled",
"Filter": {
"Prefix": "logs/"
},
"Expiration": {
"Days": 30
}
}
]
}
EOF
mc ilm import local/backups /tmp/lifecycle.json
7. Tips, Gotchas, and Troubleshooting
Here is what breaks when you self-host MinIO — and how to fix it fast.
Presigned URL fails with SignatureDoesNotMatch
The most common cause is a mismatch between the Content-Type signed and the Content-Type sent during PUT. Another cause is MINIO_SERVER_URL not matching the domain the client hits. Triple-check both.
Console redirect loops or shows wrong URL
Set MINIO_BROWSER_REDIRECT_URL to your console's public HTTPS URL. Without it, MinIO redirects to its internal container address.
Uploads fail with "413 Request Entity Too Large"
Your reverse proxy is capping upload size. In Nginx, set client_max_body_size 0;. In Caddy, there's no default limit — check Cloudflare or another upstream proxy.
Disk full but bucket shows space available
MinIO spreads data evenly across all drives. If one drive fills up, writes fail even if others have space. Monitor per-drive usage, not just total.
mc admin info local --json | jq '.info.servers[].drives[].usedSpace'
Container won't start after upgrade
MinIO is strict about drive count in distributed mode. You cannot change the number of drives after the first start. If you need more capacity, add a new server pool, not new drives to an existing pool.
Root password lost
There is no password recovery. Set MINIO_ROOT_PASSWORD via environment variable and store it in your secrets manager. If you lose it, you lose admin access — though existing app users continue working.
Closing Thoughts
Self-hosting MinIO gives you S3-compatible object storage without the metered pricing, egress fees, or vendor lock-in. With erasure coding, your data survives disk failures. With presigned URLs, your users upload and download directly without touching your application servers. With bucket policies and scoped users, you control exactly who can do what.
The setup is straightforward: Docker Compose with four drives, the MinIO Client for management, a reverse proxy for HTTPS, and your existing S3 SDK for application integration. From there, scale by adding server pools, enabling replication, or tiering to cold storage.
If you're running MinIO in production and need help with distributed clusters, multi-site replication, or integrating it into your application stack — reach out to our team. We design and deploy object storage infrastructure for teams that need performance and control.