Skip to Content

MinIO Self-Host Setup: Build Your Own S3-Compatible Object Storage in Under an Hour

Deploy a production-ready MinIO cluster with Docker Compose, erasure coding, HTTPS, and proper access controls — no AWS bill required.

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 self-host setup gives you a high-performance, S3-compatible object store that you control. Same API. Same SDKs. Zero per-GB egress fees. This guide shows you exactly how to deploy MinIO with Docker Compose, erasure coding for data protection, HTTPS, and proper access controls.

Related reads:

Prerequisites

Before you start, make sure you have:

  • 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

Confirm Docker is ready:

docker --version
docker compose version

What Is MinIO and Why Self-Host It?

MinIO is an open-source, high-performance object storage server that implements the Amazon S3 API. It's written in Go, runs as a single static binary or container, and scales from a single node to distributed clusters spanning hundreds of drives.

Why Teams Choose MinIO Over Cloud S3

  • No egress fees — move data in and out without surprise bills. Cloud providers charge per-GB for egress; MinIO doesn't.
  • Full S3 API compatibility — every S3 SDK, CLI tool, and application works with MinIO. Just change the endpoint.
  • Erasure coding built-in — protect data against drive failures without RAID or external backup systems.
  • Identity and access management — create users, policies, and scoped access keys with fine-grained permissions.
  • Presigned URLs — let users upload and download directly from storage without proxying through your application servers.
  • Data sovereignty — keep sensitive data on infrastructure you control. No third-party access to your buckets.

The tradeoff is operational responsibility: you manage backups, monitoring, and capacity. For teams that value control and predictable costs, that's a fair deal.

Deploy MinIO with Docker Compose

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.

Prepare the Data Directories

sudo mkdir -p /opt/minio && cd /opt/minio
sudo mkdir -p data/{1,2,3,4}
sudo chown -R $USER:$USER /opt/minio

The Docker Compose File

Create a Compose file that runs MinIO with four data drives for erasure coding:

version: "3.8"

services:
  minio:
    image: quay.io/minio/minio:latest
    container_name: minio
    restart: unless-stopped
    command: server /data/{1...4} --console-address ":9001"
    environment:
      - MINIO_ROOT_USER=admin
      - MINIO_ROOT_PASSWORD=changeme
      - 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 the stack:

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.

Related read: Production Guide: Deploy Budibase with Docker Compose + Caddy + CouchDB + Redis + MinIO on Ubuntu

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 changeme

Create Buckets

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

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.

Related read: Production Guide: Deploy MinIO with systemd + NGINX on Ubuntu

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,
})

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.

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

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.

Pro Tips

  • Use separate buckets per application — isolation limits blast radius if a key leaks or a lifecycle rule goes wrong.
  • Enable versioning on critical buckets — recover from accidental overwrites or deletions without restoring from backup.
  • Monitor disk usage per drive — not just aggregate. A single full drive stops writes even when the pool has capacity.
  • Test your restore procedure quarterly — erasure coding isn't backup. Verify you can recover from deletion, corruption, and ransomware scenarios.
  • Use mc mirror for cross-site replication — run it in a cron job or systemd timer for continuous protection against site-level failures.

Wrapping Up

A proper MinIO self-host setup 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.

Start with the Compose stack in this guide, create your first buckets, and wire up your application. Once you're serving files through presigned URLs and watching disk usage in the console, you'll wonder why you ever paid per-GB for object storage.

Need Help with Production MinIO?

If you're running MinIO in production and need help with distributed clusters, multi-site replication, backup strategies, or integrating it into your application stack — the sysbrix team can design and deploy it. We build object storage infrastructure that's production-ready, not just proof-of-concept.

Talk to Us →

LiteLLM Setup Proxy: A Complete Developer Guide to Building a Unified LLM Gateway
Deploy a production-ready LiteLLM proxy to route, manage, and monitor every LLM provider in your stack through a single OpenAI-compatible API.