Skip to Content

MinIO Self-Host Setup: The Developer's Guide to S3-Compatible Storage

Deploy MinIO with Docker Compose, erasure coding, and HTTPS — your own S3-compatible object storage in under 30 minutes.

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.

How to Self-Host Gitea: The Developer's Guide to Private Git Hosting
Run your own Git server with Gitea, PostgreSQL, and Docker Compose — complete with SSH, HTTPS, and automated backups.