---
# === IDENTITY ===
id: software/devops/docker-minio/2026
canonical_question: "Docker Compose reference: MinIO (S3-compatible)"
aliases:
  - "MinIO Docker Compose setup"
  - "self-hosted S3 storage with Docker"
  - "MinIO S3-compatible object storage Docker"
  - "docker compose minio distributed mode"
  - "MinIO docker-compose.yml example"
  - "local S3 development with MinIO Docker"
  - "MinIO standalone and distributed Docker setup"
entity_type: software_reference
domain: software > devops > Docker MinIO
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === VERIFICATION ===
last_verified: 2026-02-27
confidence: 0.92
version: 1.0
first_published: 2026-02-27

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "RELEASE.2025-05-24: embedded admin console removed from Community Edition; boringcrypto removed in favor of GOFIPS env var"
  next_review: 2026-08-26
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "MINIO_ROOT_PASSWORD must be at least 8 characters; use a strong, randomly generated password in production"
  - "Standalone mode is for development/evaluation only -- use distributed mode (minimum 4 nodes, 4 drives) for production"
  - "Port 9000 is the S3 API port and port 9001 is the Console UI port -- both must be explicitly mapped in Docker Compose"
  - "MinIO uses RELEASE.YYYY-MM-DDTHH-MM-SSZ versioning -- always pin to a specific release tag, never use :latest in production"
  - "As of RELEASE.2025-05-24, the web admin console is no longer included in Community Edition -- use MinIO Client (mc) or a separate object-browser image"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need AWS S3 service configuration, not self-hosted S3"
    use_instead: "AWS S3 documentation"
  - condition: "Need Kubernetes MinIO deployment, not Docker Compose"
    use_instead: "MinIO Operator for Kubernetes"
  - condition: "Docker container won't start or crashes"
    use_instead: "software/debugging/docker-container-wont-start/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "deployment_mode"
    question: "Do you need standalone (single node, dev/test) or distributed (multi-node, production) mode?"
    type: choice
    options: ["standalone", "distributed"]
  - key: "client_language"
    question: "What programming language will connect to MinIO?"
    type: choice
    options: ["Python (boto3)", "JavaScript (AWS SDK)", "Go (minio-go)", "Java (AWS SDK)", "CLI (mc)"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/devops/docker-minio/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-02-27)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "software/debugging/docker-container-wont-start/2026"
      label: "Docker Container Won't Start"
    - id: "software/debugging/docker-oomkilled/2026"
      label: "Docker OOMKilled Debugging"
  solves: []
  alternative_to: []
  often_confused_with: []

# === SOURCES (7 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "MinIO Docker Compose Orchestration README"
    author: MinIO Inc.
    url: https://github.com/minio/minio/blob/master/docs/orchestration/docker-compose/README.md
    type: official_docs
    published: 2025-09-06
    reliability: authoritative
  - id: src2
    title: "MinIO Docker Quickstart Guide"
    author: MinIO Inc.
    url: https://github.com/minio/minio/blob/master/docs/docker/README.md
    type: official_docs
    published: 2025-09-06
    reliability: authoritative
  - id: src3
    title: "MinIO Docker Compose docker-compose.yaml"
    author: MinIO Inc.
    url: https://github.com/minio/minio/blob/master/docs/orchestration/docker-compose/docker-compose.yaml
    type: official_docs
    published: 2025-09-06
    reliability: authoritative
  - id: src4
    title: "minio/minio Docker Hub"
    author: MinIO Inc.
    url: https://hub.docker.com/r/minio/minio
    type: official_docs
    published: 2025-01-01
    reliability: high
  - id: src5
    title: "MinIO Client Quickstart Guide"
    author: MinIO Inc.
    url: https://minio.github.io/mc/
    type: official_docs
    published: 2025-06-01
    reliability: authoritative
  - id: src6
    title: "MinIO Docker: Setup Guide for S3-Compatible Object Storage"
    author: DataCamp
    url: https://www.datacamp.com/tutorial/minio-docker
    type: technical_blog
    published: 2025-03-15
    reliability: moderate_high
  - id: src7
    title: "Publish Events to Webhook -- MinIO Documentation"
    author: MinIO Inc.
    url: https://min.io/docs/minio/container/administration/monitoring/publish-events-to-webhook.html
    type: official_docs
    published: 2025-01-01
    reliability: authoritative
---

# Docker Compose Reference: MinIO (S3-Compatible Object Storage)

## TL;DR

- **Bottom line**: MinIO provides a high-performance, S3-compatible object storage server that runs as a single Docker container (standalone) or a 4+ node cluster (distributed) with erasure coding, accessed via the standard AWS S3 API on port 9000.
- **Key tool/command**: `docker compose up -d` with `quay.io/minio/minio` image and `server /data --console-address ":9001"` command.
- **Watch out for**: Port 9000 is the S3 API and port 9001 is the Console UI -- mixing them up or forgetting `--console-address ":9001"` causes the console to bind to a random port.
- **Works with**: Docker Compose V2, any S3-compatible client (boto3, AWS CLI, minio-go, mc). Image available for linux/amd64 and linux/arm64.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- MINIO_ROOT_PASSWORD must be at least 8 characters; use a strong, randomly generated password in production
- Standalone mode is for development/evaluation only -- use distributed mode (minimum 4 nodes, 4 drives) for production
- Port 9000 is the S3 API port and port 9001 is the Console UI port -- both must be explicitly mapped
- Always pin to a specific release tag (RELEASE.YYYY-MM-DDTHH-MM-SSZ), never use `:latest` in production
- As of RELEASE.2025-05-24, the web admin console was removed from Community Edition -- use `mc` CLI or the separate object-browser image

## Quick Reference

### Service Configuration Summary

| Service | Image | Ports | Volumes | Key Env |
|---|---|---|---|---|
| minio (standalone) | `quay.io/minio/minio:RELEASE.2025-09-06T17-38-46Z` | `9000:9000`, `9001:9001` | `minio_data:/data` | `MINIO_ROOT_USER`, `MINIO_ROOT_PASSWORD` |
| minio1-4 (distributed) | `quay.io/minio/minio:RELEASE.2025-09-06T17-38-46Z` | (internal) | `data{n}-1:/data1`, `data{n}-2:/data2` | `MINIO_ROOT_USER`, `MINIO_ROOT_PASSWORD` |
| nginx (load balancer) | `nginx:1.19.2-alpine` | `9000:9000`, `9001:9001` | `./nginx.conf:/etc/nginx/nginx.conf:ro` | -- |

### Environment Variables

| Variable | Required | Default | Description |
|---|---|---|---|
| `MINIO_ROOT_USER` | Yes | `minioadmin` | Root access key (admin username) |
| `MINIO_ROOT_PASSWORD` | Yes | `minioadmin` | Root secret key (min 8 chars) |
| `MINIO_ROOT_USER_FILE` | No | -- | Docker secret path for access key |
| `MINIO_ROOT_PASSWORD_FILE` | No | -- | Docker secret path for secret key |
| `MINIO_BROWSER` | No | `on` | Enable/disable web console (`on`/`off`) |
| `MINIO_BROWSER_REDIRECT_URL` | No | -- | External URL for console (behind reverse proxy) |
| `MINIO_SERVER_URL` | No | -- | External URL for S3 API (behind reverse proxy) |
| `MINIO_NOTIFY_WEBHOOK_ENABLE_<target>` | No | `off` | Enable webhook notifications |
| `MINIO_NOTIFY_WEBHOOK_ENDPOINT_<target>` | No | -- | Webhook receiver URL |
| `MINIO_DOMAIN` | No | -- | Enable virtual-hosted-style bucket access |
| `MINIO_REGION_NAME` | No | `us-east-1` | Region name for S3 compatibility |

### Key Endpoints

| Endpoint | Port | Purpose |
|---|---|---|
| `http://localhost:9000` | 9000 | S3 API (GET/PUT/DELETE objects, list buckets) |
| `http://localhost:9001` | 9001 | MinIO Console (web UI) |
| `http://localhost:9000/minio/health/live` | 9000 | Liveness health check |
| `http://localhost:9000/minio/health/cluster` | 9000 | Cluster health check (distributed mode) |

## Decision Tree

```
START: What deployment mode do you need?
|-- Development/testing with single node?
|   |-- YES --> Use Standalone docker-compose.yml (Step 1)
|   +-- NO |
|-- Production with data redundancy?
|   |-- YES --> Use Distributed docker-compose.yml with 4 nodes + nginx (Step 2)
|   +-- NO |
|-- Need bucket event notifications?
|   |-- YES --> Add webhook env vars + mc event configuration (Step 5)
|   +-- NO |
|-- Need TLS encryption?
|   |-- YES --> Mount certs to /root/.minio/certs/ or terminate TLS at reverse proxy
|   +-- NO |
+-- DEFAULT --> Start with Standalone, migrate to Distributed when needed
```

## Step-by-Step Guide

### 1. Deploy MinIO standalone (development)

Create a minimal `docker-compose.yml` for local development and testing. This runs a single MinIO server with persistent storage. [src2]

```yaml
# docker-compose.yml -- MinIO Standalone (development only)
services:
  minio:
    image: quay.io/minio/minio:RELEASE.2025-09-06T17-38-46Z
    container_name: minio
    ports:
      - "9000:9000"
      - "9001:9001"
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: minioadmin123
    command: server /data --console-address ":9001"
    volumes:
      - minio_data:/data
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  minio_data:
```

**Verify**: `curl -s http://localhost:9000/minio/health/live` --> returns HTTP 200

### 2. Deploy MinIO distributed (production)

The official 4-node distributed setup uses erasure coding for data protection. Each node has 2 drives, creating an 8-drive erasure set that tolerates up to 4 drive failures. [src1, src3]

```yaml
# docker-compose.yml -- MinIO Distributed (4 nodes, erasure coding)
x-minio-common: &minio-common
  image: quay.io/minio/minio:RELEASE.2025-09-06T17-38-46Z
  command: server --console-address ":9001" http://minio{1...4}/data{1...2}
  expose:
    - "9000"
    - "9001"
  environment:
    MINIO_ROOT_USER: minioadmin
    MINIO_ROOT_PASSWORD: strongpassword123
  healthcheck:
    test: ["CMD", "mc", "ready", "local"]
    interval: 5s
    timeout: 5s
    retries: 5

services:
  minio1:
    <<: *minio-common
    hostname: minio1
    volumes:
      - data1-1:/data1
      - data1-2:/data2

  minio2:
    <<: *minio-common
    hostname: minio2
    volumes:
      - data2-1:/data1
      - data2-2:/data2

  minio3:
    <<: *minio-common
    hostname: minio3
    volumes:
      - data3-1:/data1
      - data3-2:/data2

  minio4:
    <<: *minio-common
    hostname: minio4
    volumes:
      - data4-1:/data1
      - data4-2:/data2

  nginx:
    image: nginx:1.27-alpine
    hostname: nginx
    ports:
      - "9000:9000"
      - "9001:9001"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - minio1
      - minio2
      - minio3
      - minio4

volumes:
  data1-1:
  data1-2:
  data2-1:
  data2-2:
  data3-1:
  data3-2:
  data4-1:
  data4-2:
```

**Verify**: `curl -s http://localhost:9000/minio/health/cluster` --> returns HTTP 200

### 3. Configure MinIO Client (mc)

Install and configure the MinIO Client to manage buckets, users, and policies. [src5]

```bash
# Install mc (MinIO Client)
# Linux/macOS:
curl -O https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc && sudo mv mc /usr/local/bin/

# Configure alias pointing to your MinIO instance
mc alias set myminio http://localhost:9000 minioadmin minioadmin123

# Create a bucket
mc mb myminio/my-bucket

# Upload a file
mc cp myfile.txt myminio/my-bucket/

# List bucket contents
mc ls myminio/my-bucket/

# Set bucket policy to public read
mc anonymous set download myminio/my-bucket
```

**Verify**: `mc admin info myminio` --> shows server status, disks, uptime

### 4. Create users and apply bucket policies

Set up access control with users and JSON policies for fine-grained permissions. [src5]

```bash
# Create a new user
mc admin user add myminio appuser appuser-secret-key

# Create a policy file (read-write access to one bucket)
cat > /tmp/app-policy.json << 'POLICY'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::app-data",
        "arn:aws:s3:::app-data/*"
      ]
    }
  ]
}
POLICY

# Apply the policy
mc admin policy create myminio app-readwrite /tmp/app-policy.json

# Attach policy to user
mc admin policy attach myminio app-readwrite --user appuser
```

**Verify**: `mc admin user info myminio appuser` --> shows attached policy

### 5. Configure bucket event notifications (webhook)

Set up webhook notifications for object events (create, delete, access). [src7]

```bash
# Add webhook target to MinIO
mc admin config set myminio notify_webhook:primary \
  endpoint="http://webhook-receiver:8080/events" \
  queue_dir="/tmp/events"

# Restart MinIO to apply config (or docker compose restart)
mc admin service restart myminio

# Enable events on a bucket (all object events)
mc event add myminio/my-bucket arn:minio:sqs::primary:webhook \
  --event put,get,delete

# Verify event configuration
mc event ls myminio/my-bucket
```

**Verify**: Upload a file with `mc cp test.txt myminio/my-bucket/` and check webhook receiver logs for the event payload.

## Code Examples

### Python (boto3): Connect to MinIO and Upload Files

```python
import boto3  # pip install boto3>=1.35.0
from botocore.config import Config

# Input:  MinIO endpoint, credentials, file to upload
# Output: Uploaded object in MinIO bucket

s3 = boto3.client(
    "s3",
    endpoint_url="http://localhost:9000",
    aws_access_key_id="minioadmin",
    aws_secret_access_key="minioadmin123",
    config=Config(signature_version="s3v4"),
    region_name="us-east-1",
)

# Create bucket (ignore if exists)
try:
    s3.create_bucket(Bucket="my-bucket")
except s3.exceptions.BucketAlreadyOwnedByYou:
    pass

# Upload file
s3.upload_file("local-file.txt", "my-bucket", "remote-file.txt")

# List objects
resp = s3.list_objects_v2(Bucket="my-bucket")
for obj in resp.get("Contents", []):
    print(f"{obj['Key']} ({obj['Size']} bytes)")
```

### JavaScript (AWS SDK v3): Upload and Download Objects

> Full script: [javascript-aws-sdk-v3-upload-and-download-objects.js](scripts/javascript-aws-sdk-v3-upload-and-download-objects.js) (26 lines)

```javascript
// npm install @aws-sdk/client-s3@^3.500.0
const { S3Client, PutObjectCommand, GetObjectCommand }
  = require("@aws-sdk/client-s3");
const fs = require("fs");
// Input:  MinIO endpoint, credentials, file buffer
# ... (see full script)
```

### Go (minio-go): Bucket Operations

```go
// go get github.com/minio/minio-go/v7@latest
package main

import (
    "context"
    "log"
    "github.com/minio/minio-go/v7"
    "github.com/minio/minio-go/v7/pkg/credentials"
)

func main() {
    // Input:  MinIO endpoint, credentials
    // Output: Bucket created, file uploaded
    client, err := minio.New("localhost:9000", &minio.Options{
        Creds:  credentials.NewStaticV4("minioadmin", "minioadmin123", ""),
        Secure: false,
    })
    if err != nil { log.Fatal(err) }

    ctx := context.Background()
    _ = client.MakeBucket(ctx, "my-bucket", minio.MakeBucketOptions{})

    _, err = client.FPutObject(ctx, "my-bucket", "photo.jpg",
        "/path/to/photo.jpg", minio.PutObjectOptions{ContentType: "image/jpeg"})
    if err != nil { log.Fatal(err) }
}
```

## Anti-Patterns

### Wrong: Using default credentials in production

```yaml
# BAD -- default credentials are publicly known
services:
  minio:
    image: quay.io/minio/minio
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: minioadmin
```

### Correct: Use strong credentials via Docker secrets or .env

```yaml
# GOOD -- credentials from .env file, never committed to version control
services:
  minio:
    image: quay.io/minio/minio:RELEASE.2025-09-06T17-38-46Z
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    # Or use Docker secrets:
    # secrets:
    #   - minio_root_user
    #   - minio_root_password
    # environment:
    #   MINIO_ROOT_USER_FILE: /run/secrets/minio_root_user
    #   MINIO_ROOT_PASSWORD_FILE: /run/secrets/minio_root_password
```

### Wrong: Missing --console-address flag

```yaml
# BAD -- console binds to random port, inaccessible
command: server /data
ports:
  - "9000:9000"
  - "9001:9001"   # This won't work without --console-address
```

### Correct: Explicitly set console address

```yaml
# GOOD -- console reliably available on port 9001
command: server /data --console-address ":9001"
ports:
  - "9000:9000"
  - "9001:9001"
```

### Wrong: Using :latest tag

```yaml
# BAD -- :latest is mutable, can break on update
image: quay.io/minio/minio:latest
```

### Correct: Pin to specific release

```yaml
# GOOD -- immutable tag, reproducible deployments
image: quay.io/minio/minio:RELEASE.2025-09-06T17-38-46Z
```

### Wrong: Using standalone for production data

```yaml
# BAD -- no redundancy, single point of failure
services:
  minio:
    image: quay.io/minio/minio:RELEASE.2025-09-06T17-38-46Z
    command: server /data --console-address ":9001"
    # Single node = no erasure coding, no fault tolerance
```

### Correct: Distributed mode for production

```yaml
# GOOD -- 4-node cluster with erasure coding
command: server --console-address ":9001" http://minio{1...4}/data{1...2}
# Tolerates up to 4 drive failures out of 8 total drives
```

## Common Pitfalls

- **Port 9000 already in use**: Another service (SonarQube, PHP-FPM) occupies port 9000 causing `bind: address already in use`. Fix: Map to a different host port with `"9002:9000"` or stop the conflicting service. [src2]
- **Permission denied on /data volume**: MinIO process cannot write to the mounted volume because the host directory has restrictive permissions. Fix: Run `chmod 777 /path/to/data` on the host or use `--user $(id -u):$(id -g)` in Docker run. [src6]
- **Console shows random port in logs**: Forgetting `--console-address ":9001"` makes the console bind to a random ephemeral port. Fix: Always include `--console-address ":9001"` in the `command`. [src4]
- **S3 requests hitting console port**: Sending S3 API requests to port 9001 (console) instead of 9000 (API) returns HTML instead of S3 XML responses. Fix: Ensure your S3 client's `endpoint_url` uses port 9000. [src2]
- **Distributed mode hostname resolution failure**: MinIO nodes cannot discover each other because hostnames (`minio1`, `minio2`, etc.) don't resolve. Fix: Ensure all services share the same Docker network and `hostname` values match the server command pattern. [src1]
- **AWS SDK missing forcePathStyle**: AWS SDK v3 defaults to virtual-hosted-style URLs (`bucket.endpoint`) which fail with MinIO. Fix: Set `forcePathStyle: true` in the S3Client config. [src6]
- **Health check fails on startup**: `mc ready local` fails because MinIO hasn't finished initializing. Fix: Increase `start_period` in healthcheck (e.g., `start_period: 10s`) or increase retries. [src3]

## Diagnostic Commands

```bash
# Check MinIO is running and healthy
curl -sf http://localhost:9000/minio/health/live && echo "OK" || echo "FAIL"

# Check cluster health (distributed mode)
curl -sf http://localhost:9000/minio/health/cluster && echo "OK" || echo "FAIL"

# View MinIO server info (disk, uptime, version)
mc admin info myminio

# Check container logs for errors
docker compose logs minio --tail=50

# Verify bucket exists and list contents
mc ls myminio/my-bucket/

# Check disk usage per bucket
mc du myminio/my-bucket/

# Test S3 connectivity from Python
python -c "import boto3; s3=boto3.client('s3',endpoint_url='http://localhost:9000',aws_access_key_id='minioadmin',aws_secret_access_key='minioadmin123'); print(s3.list_buckets()['Buckets'])"

# View configured event notifications
mc event ls myminio/my-bucket

# Check MinIO server configuration
mc admin config get myminio
```

## Version History & Compatibility

| Release | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| RELEASE.2025-09-06 | Recommended | None | Stable release, recommended baseline |
| RELEASE.2025-05-24 | Breaking | Admin console removed from CE; boringcrypto removed | Use `mc` CLI or separate object-browser; use GOFIPS env instead of boringcrypto |
| RELEASE.2025-10-15 | Security fix | None | Fixes privilege escalation via session policy bypass (CVE) |
| RELEASE.2024-xx | Previous | None | Standard upgrade path; data format compatible |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Local S3-compatible development/testing | Need managed cloud storage with SLA | AWS S3, Google Cloud Storage, Azure Blob |
| Self-hosted object storage for CI/CD artifacts | Storing <1 GB of files in a simple project | Docker volumes or bind mounts directly |
| S3 API-compatible backup target | Need a full-featured data lake with analytics | Delta Lake, Apache Iceberg on S3 |
| Air-gapped or on-premise deployments requiring S3 API | Single-node dev environment needing erasure coding | Standalone MinIO (no erasure coding) is fine |
| Multi-tenant object storage with IAM policies | Need POSIX filesystem semantics | NFS, GlusterFS, or Ceph filesystem |

## Important Caveats

- MinIO Community Edition (AGPLv3) removed the embedded web admin console in RELEASE.2025-05-24 -- you must use the `mc` CLI, the separate `minio/object-browser` container, or upgrade to the commercial AIStor edition for a web UI
- Docker named volumes do not persist across `docker compose down -v` -- never use `-v` flag unless you intend to destroy all stored data
- Distributed mode requires all nodes to use the same MinIO version -- mixed-version clusters are not supported
- MinIO health check endpoint `/minio/health/live` returns 200 even when the server is running but cannot serve requests due to quorum loss in distributed mode -- use `/minio/health/cluster` for a full cluster check
- When running behind a reverse proxy (nginx, Traefik), set `MINIO_SERVER_URL` and `MINIO_BROWSER_REDIRECT_URL` to the external URLs to prevent redirect loops and presigned URL mismatches
- The `MINIO_ROOT_USER` and `MINIO_ROOT_PASSWORD` cannot be changed after initial setup without data migration -- plan credentials before first deployment

## Related Units
<!-- Generated from related_kos frontmatter -->

- [Docker Container Won't Start](/software/debugging/docker-container-wont-start/2026)
- [Docker OOMKilled Debugging](/software/debugging/docker-oomkilled/2026)
