Skip to Content

MinIO Self-Host Setup: Production-Ready S3-Compatible Object Storage

Deploy MinIO with erasure coding, configure bucket policies, and build a self-hosted object store that scales without AWS lock-in.

MinIO Self-Host Setup: Production-Ready S3-Compatible Object Storage

Cloud object storage bills grow silently. Egress fees surprise you at month-end. And sometimes you simply need data on hardware you control. MinIO gives you a drop-in S3-compatible API without vendor lock-in. This guide walks you through a production-grade MinIO self-host setup—from single-node Docker deployment to erasure-coded storage, bucket policies, and HTTPS termination you can actually run in production.

If you want a faster walkthrough focused on speed, check out MinIO Self-Host Setup: Build S3-Compatible Object Storage in 30 Minutes.

What You Will Build

By the end of this guide you will have:

  • MinIO running in Docker with erasure-coded data protection
  • HTTPS access behind a reverse proxy with auto-renewing certificates
  • Bucket policies and scoped user access controls
  • Versioning and lifecycle rules configured
  • A working S3 endpoint tested with the AWS CLI and SDK

Prerequisites

Before you start, gather the following:

  • A Linux server (Ubuntu 22.04+ or Debian 12+) with at least 2 CPU cores, 4 GB RAM, and 50 GB free disk
  • Docker and Docker Compose installed
  • A domain or subdomain pointed at your server (for HTTPS)
  • Basic comfort with the command line, YAML, and S3 concepts
  • Ports 9000 (S3 API) and 9001 (console) available

Verify Docker is ready:

docker --version
docker compose version

If not installed:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker

Step 1: Deploy MinIO with Docker Compose

The simplest reliable MinIO self-host setup uses Docker Compose. One file defines the service, volumes, environment, and health checks.

Create a project directory:

mkdir ~/minio-stack && cd ~/minio-stack
mkdir -p data

Write a docker-compose.yml:

version: "3.8"

services:
  minio:
    image: quay.io/minio/minio:RELEASE.2026-01-18T03-15-33Z
    container_name: minio
    restart: unless-stopped
    ports:
      - "9000:9000"
      - "9001:9001"
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: Change…n123
      MINIO_BROWSER: "on"
    volumes:
      - ./data:/data
    command: server /data --console-address ":9001"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 20s
      retries: 3

Start the container:

docker compose up -d

Open the MinIO Console at http://your-server-ip:9001 and log in with the root credentials from your Compose file.

Step 2: Enable Erasure Coding with Multiple Drives

A single drive works for testing. For anything serious, you want erasure coding. MinIO stripes data across drives and can survive losing up to half of them.

MinIO requires drives in sets of 4, 6, 8, 10, 12, 14, or 16. For a standalone server, use 4 drives:

mkdir -p data/disk{1..4}

Update the command in your Compose file:

    volumes:
      - ./data/disk1:/data1
      - ./data/disk2:/data2
      - ./data/disk3:/data3
      - ./data/disk4:/data4
    command: server http://minio/data{1...4} --console-address ":9001"

Recreate the container:

docker compose down
docker compose up -d

Check the console. You should see 4 drives online and a parity ratio displayed. With 4 drives, MinIO uses 2 parity drives—you can lose 2 drives without losing data.

Step 3: Configure Users, Buckets, and Policies

Running everything as root user is a liability. Create scoped users and lock down bucket access with policies.

Create a Bucket

From the MinIO Console:

  1. Click Buckets → Create Bucket
  2. Name it app-uploads
  3. Enable Versioning for rollback protection
  4. Enable Object Locking only if you need compliance (this is irreversible)
  5. Click Create

Create a Read-Write User Policy

Go to Policies → Create Policy and paste this JSON:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",
        "s3:GetBucketLocation",
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": [
        "arn:aws:s3:::app-uploads",
        "arn:aws:s3:::app-uploads/*"
      ]
    }
  ]
}

Create a Service User

Go to Users → Create User. Name it app-service, assign the policy above, and save the generated access and secret keys. You will need them for your application.

For a deeper dive into policy patterns and multi-user setups, see MinIO Self-Host Setup: The Developer's Guide to S3-Compatible Storage.

Step 4: Test the S3 API with the AWS CLI

MinIO speaks S3. That means any tool built for AWS S3 works here with only endpoint and credential changes.

Install the AWS CLI if you do not have it:

curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

Configure a profile for your MinIO instance:

aws configure --profile minio
# AWS Access Key ID: your-minio-access-key
# AWS Secret Access Key: your-minio-secret-key
# Default region: us-east-1
# Default output: json

Test a bucket listing:

aws --profile minio --endpoint-url http://localhost:9000 s3 ls

Upload a file:

echo "hello minio" > test.txt
aws --profile minio --endpoint-url http://localhost:9000 s3 cp test.txt s3://app-uploads/test.txt

Download it back:

aws --profile minio --endpoint-url http://localhost:9000 s3 cp s3://app-uploads/test.txt -

If that prints hello minio, your S3-compatible endpoint is live.

Step 5: Secure MinIO with HTTPS and a Reverse Proxy

Exposing port 9000 over HTTP is fine for local testing. In production, you need TLS. The easiest path is Caddy.

Add Caddy to Your Compose File

  caddy:
    image: caddy:2.8
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
      - caddy-config:/config
    networks:
      - minio-net

volumes:
  caddy-data:
  caddy-config:

Create Caddyfile:

s3.yourdomain.com {
    reverse_proxy minio:9000
}

console.yourdomain.com {
    reverse_proxy minio:9001
}

Update your MinIO service to join the same network:

    networks:
      - minio-net

networks:
  minio-net:
    driver: bridge

Restart the stack:

docker compose up -d

Caddy will fetch certificates from Let's Encrypt automatically. Your S3 API is now at https://s3.yourdomain.com and the console at https://console.yourdomain.com.

Step 6: Enable Versioning and Lifecycle Rules

Production storage needs more than buckets. You need versioning for recovery and lifecycle rules for cost control.

Enable Versioning

From the MinIO Console:

  1. Go to Buckets → app-uploads → Manage
  2. Click the Versioning tab
  3. Toggle Enable Versioning

Now every overwrite creates a new version. You can recover deleted or corrupted objects.

Add a Lifecycle Rule

Old versions accumulate storage. Set a lifecycle rule to expire them after 30 days:

  1. Go to Buckets → app-uploads → Lifecycle
  2. Click Add Lifecycle Rule
  3. Set Rule Name to expire-old-versions
  4. Choose Expire non-current versions
  5. Set Non-current days to 30
  6. Click Save

This keeps your storage costs predictable. Current objects stay forever. Old versions clean themselves up.

Tips and Troubleshooting

MinIO console shows a blank page

  • Make sure MINIO_BROWSER=on is set in your environment
  • Check that port 9001 is not blocked by a firewall: sudo ufw allow 9001

Cannot connect with AWS CLI

  • Verify the endpoint URL includes the protocol: http:// or https://
  • Confirm the access key has the correct policy attached in the MinIO Console
  • If using HTTPS with a self-signed certificate, add --no-verify-ssl to your AWS CLI commands

Data disappears after container restart

  • Ensure your volumes map host directories, not anonymous Docker volumes
  • Without a host bind mount, data lives inside the container and is destroyed on removal

Erasure coding will not start

  • Drive counts must be 4, 6, 8, 10, 12, 14, or 16 per erasure set
  • All drives in a set must be empty when MinIO first starts, or it will refuse to format them

Slow upload or download speeds

  • Check disk I/O with iotop or iostat. Spinning rust will bottleneck large file transfers
  • Ensure your network interface is not saturated. MinIO benefits from 10 Gbps+ in production
  • Run MinIO on locally attached drives. Network-attached storage adds latency

Permission denied on the data directory

  • MinIO runs as a non-root user inside the container. Fix ownership on the host:
    sudo chown -R 1000:1000 ./data

Next Steps

You now have a working MinIO self-host setup with erasure coding, user policies, HTTPS, versioning, and S3 API compatibility. That is more than most teams start with. From here, consider:

  • Running MinIO in distributed mode across multiple servers for true high availability
  • Configuring bucket replication to a second MinIO cluster for disaster recovery
  • Setting up bucket notifications to trigger webhooks on uploads
  • Using the MinIO Client (mc) for scripting, batch operations, and policy management
  • Integrating MinIO with backup tools like Restic or Velero for off-site backups

Object storage does not have to mean cloud vendor lock-in. With MinIO, you own the stack, the data, and the bill.

For a focused walkthrough on getting started quickly, see MinIO Self-Host Setup: Build Your Own S3-Compatible Object Storage in Under an Hour.

Need Help at Scale?

If you are running production workloads and need distributed MinIO, SSO integration, fine-grained IAM, or migration from AWS S3, we can help. Contact the Sysbrix team and we will architect an object storage setup that fits your stack.

LiteLLM Setup Proxy: One Gateway for Every LLM in Your Stack
Deploy LiteLLM with Docker, route requests to OpenAI, Anthropic, and local models through a single OpenAI-compatible API, and stop managing API keys in every app.