Skip to Content

MinIO Self-Host Setup: Build S3-Compatible Object Storage in 30 Minutes

Deploy MinIO with Docker, configure buckets and policies, and replace AWS S3 for a fraction of the cost—without losing compatibility.

MinIO Self-Host Setup: Build S3-Compatible Object Storage in 30 Minutes

Cloud object storage bills creep up. Egress fees sting. And sometimes you just need data sitting on hardware you control. MinIO gives you a drop-in S3-compatible API without the AWS lock-in. This guide walks you through a production-ready MinIO self-host setup—from a single Docker container to a secured, bucket-polished object store you can actually use.

If you want a faster, more focused walkthrough, check out MinIO Self-Host Setup: Build Your Own S3-Compatible Object Storage in Under an Hour.

What You Will Build

By the end of this guide you will have:

  • MinIO running in Docker with persistent storage
  • Erasure-coded data protection against disk failure
  • HTTPS access behind a reverse proxy
  • Bucket policies and user access controls configured
  • A working S3 endpoint tested with the AWS CLI

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 and YAML
  • Ports 9000 (S3 API) and 9001 (console) available

Verify Docker is ready:

docker --version
docker compose version

If not installed, run:

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, the volumes, and the environment.

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.2025-07-11T18-11-10Z
    container_name: minio
    restart: unless-stopped
    ports:
      - "9000:9000"
      - "9001:9001"
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: ChangeMeInProduction123
      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: Add Erasure Coding with Multiple Drives

A single drive is fine 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 if you want rollback protection
  4. 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: Integrate MinIO into Your Application Stack

MinIO shines when it is part of a larger platform. If you are running Budibase, N8N, or any app that needs S3-backed file storage, MinIO slots right in.

Here is a typical environment configuration for an application connecting to MinIO:

S3_ENDPOINT=https://s3.yourdomain.com
S3_BUCKET=app-uploads
S3_ACCESS_KEY=your-access-key
S3_SECRET_KEY=your-secret-key
S3_REGION=us-east-1
S3_FORCE_PATH_STYLE=true

The key setting is S3_FORCE_PATH_STYLE=true. MinIO uses path-style URLs (https://s3.yourdomain.com/bucket-name) rather than virtual-hosted style (https://bucket-name.s3.yourdomain.com). Most S3 SDKs need this flag to work correctly with self-hosted endpoints.

If you are building a full application platform, you might find our Production Guide: Deploy Budibase with Docker Compose + Caddy + CouchDB + Redis + MinIO on Ubuntu useful. It shows how MinIO fits into a real multi-service architecture.

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, and S3 API compatibility. That is more than most teams start with. From here, consider:

  • Enabling bucket versioning and object locking for compliance
  • Setting up lifecycle rules to move old objects to cold storage
  • Configuring bucket notifications to trigger webhooks on uploads
  • Running MinIO in distributed mode across multiple servers for true HA
  • Using the MinIO Client (mc) for scripting and batch operations

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

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.

How to Self-Host Grafana: Build Production-Ready Monitoring Dashboards
Deploy Grafana with Docker, wire up Prometheus, provision dashboards as code, and stop flying blind in production.