Self-Hosted S3-Compatible Storage: Run MinIO on Docker for HomeLab Backups and Media

HomeLab tutorial - IT technology blog
HomeLab tutorial - IT technology blog

Three months after setting up my HomeLab, my AWS S3 bill crept past $40/month. Not because I was doing anything unusual — just storing VM snapshots, Grafana dashboard exports, and the occasional Docker volume backup. That number kept climbing every time I added a new service.

If that sounds familiar, MinIO is the answer. It’s an S3-compatible object storage server you can run on your own hardware, and after running it in my HomeLab for over a year, I’ve never looked back.

The Real Problem: Cloud Storage Creep

Most people underestimate how fast object storage costs accumulate in a HomeLab context. AWS S3 isn’t expensive for a few gigabytes, but the moment you start automating backups — daily snapshots of databases, Docker volumes, config files — you’re dealing with hundreds of gigabytes without noticing.

Beyond cost, there’s the latency problem. If your backup process writes to S3 in us-east-1 and your HomeLab is in Tokyo, you’re adding 150ms+ per request to every backup job. For large files, this turns a 10-minute backup window into 45 minutes.

The deeper issue: you’re building dependency on a third-party service for data that never leaves your local network. Restic, Duplicati, Nextcloud, and half a dozen other self-hosted tools all support S3 — which means they can all point at your local MinIO instance instead.

Why Cloud S3 Isn’t Always the Right Tool

AWS S3 is genuinely excellent for production workloads at scale. But HomeLab usage doesn’t match that profile:

  • Data rarely leaves your local network, so egress costs are friction, not value
  • You don’t need 11 nines of durability for a Jellyfin media cache or a Grafana backup
  • API rate limits on free tiers break automated backup schedulers
  • Privacy: some data you simply don’t want sitting on a third-party cloud provider

The root cause is a mismatch between cloud storage’s pricing model (pay per GB + per request + per transfer) and HomeLab usage patterns (bursty writes, local reads, flexible retention).

Self-Hosted Object Storage: What Are Your Options?

Before committing to MinIO, I evaluated three alternatives:

Ceph

The enterprise choice. Ceph handles object, block, and file storage in a distributed setup. It’s production-grade but wildly over-engineered for a single-node HomeLab. Minimum viable deployment requires 3 nodes and significant RAM overhead. Skip it unless you’re running a multi-server rack.

Garage

A lightweight, Rust-based alternative designed specifically for self-hosted use cases. Very small memory footprint, runs fine on a Raspberry Pi. The ecosystem support is still catching up though — some S3 clients need workarounds to function correctly with Garage’s API implementation.

MinIO

The sweet spot for HomeLab. S3-compatible API means any tool that supports S3 just works — zero configuration changes on the client side. Runs efficiently on a single node. Has a clean web UI. Active development with solid documentation. The community edition is fully open source under AGPL.

I’ve applied MinIO in production for both client projects and my own HomeLab, and the results have been consistently stable. It handles everything from Restic backup targets to Nextcloud external storage without any issues.

Setting Up MinIO on Docker

Here’s the setup I actually use. Create a docker-compose.yml in a dedicated directory:

version: '3.8'

services:
  minio:
    image: quay.io/minio/minio:latest
    container_name: minio
    restart: unless-stopped
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: admin
      MINIO_ROOT_PASSWORD: your_strong_password_here
      MINIO_VOLUMES: /data
    volumes:
      - /opt/minio/data:/data
    ports:
      - "9000:9000"   # S3 API
      - "9001:9001"   # Web console
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 10s
      retries: 3

A few things worth getting right from the start:

  • Use a proper path for /opt/minio/data — ideally a dedicated disk or ZFS dataset, not your OS drive
  • Set a strong root password immediately; MinIO’s API is accessible over the network, and default credentials are the #1 security mistake in HomeLab setups
  • --console-address ":9001" separates the API port (9000) from the web UI port (9001) — important when you’re putting Nginx or Traefik in front

Start it and verify:

docker compose up -d
docker compose logs -f minio

Access the console at http://your-server-ip:9001 and create your first bucket.

Configure Service Accounts, Not Root Credentials

Never use your root credentials in application configs. MinIO has proper IAM support — create a service account for each application that needs access.

From the MinIO console: Identity → Service Accounts → Create

Or via the MinIO client (mc):

# Install mc
curl -O https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc
sudo mv mc /usr/local/bin/

# Configure alias
mc alias set local http://localhost:9000 admin your_strong_password_here

# Create a dedicated user for Restic backups
mc admin user add local restic-user restic_password_here

# Create a policy with limited permissions
cat > restic-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket"],
      "Resource": ["arn:aws:s3:::restic-backups/*", "arn:aws:s3:::restic-backups"]
    }
  ]
}
EOF

mc admin policy create local restic-policy restic-policy.json
mc admin policy attach local restic-policy --user restic-user

Now your Restic job only has access to its own bucket, and a compromised credential can’t touch anything else.

Pointing Your Tools at MinIO

The S3 compatibility is what makes MinIO genuinely useful. Here’s how to configure the most common HomeLab tools:

Restic

export AWS_ACCESS_KEY_ID=restic-user
export AWS_SECRET_ACCESS_KEY=restic_password_here
export RESTIC_REPOSITORY=s3:http://your-server:9000/restic-backups

restic init
restic backup /path/to/data

rclone

rclone config create minio-local s3 \
  provider=Minio \
  access_key_id=your-access-key \
  secret_access_key=your-secret \
  endpoint=http://your-server:9000

# Test it
rclone ls minio-local:restic-backups

Nextcloud External Storage

Install the External Storage Support app, then add an S3-compatible storage entry pointing to http://your-server:9000 with your MinIO credentials. It works identically to a real AWS S3 bucket from Nextcloud’s perspective.

Tips from Running MinIO for Over a Year

These are the lessons that weren’t in the documentation:

Set Up Bucket Lifecycle Policies

Without lifecycle rules, old backups accumulate forever. Configure automatic expiry for buckets that don’t need long retention:

mc ilm add local/restic-backups --expiry-days 90

Enable Versioning for Critical Buckets

For any bucket storing config files or important application data, enable versioning. It saved me once when a broken backup script overwrote good data with a corrupted archive:

mc version enable local/config-backups

Monitor Disk Usage Proactively

MinIO doesn’t throttle writes when the disk is nearly full — it just starts failing silently. Add a simple cron check:

# Add to crontab: check every hour
0 * * * * df -h /opt/minio/data | awk 'NR==2{if($5+0 > 85) print "WARNING: MinIO disk at "$5}' | mail -s "MinIO Disk Alert" [email protected]

Put It Behind a Reverse Proxy for TLS

If you’re accessing MinIO from outside your LAN, add Nginx or Traefik in front with TLS. Sending S3 credentials over unencrypted HTTP is a real risk on any shared network segment. A basic Nginx config:

server {
    listen 443 ssl;
    server_name minio.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/minio.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/minio.yourdomain.com/privkey.pem;

    # S3 API
    location / {
        proxy_pass http://localhost:9000;
        proxy_set_header Host $host;
        client_max_body_size 0;  # Required for large uploads
    }
}

Integrate with Your Existing Monitoring

MinIO exposes Prometheus metrics at /minio/v2/metrics/cluster. If you’re already running Prometheus and Grafana in your HomeLab, add MinIO as a scrape target and you get disk I/O, request latency, and per-bucket storage graphs without any extra work.

What MinIO Doesn’t Replace

Single-node MinIO has no built-in redundancy — a disk failure means data loss unless you’re running on a ZFS mirror or RAID underneath. Treat it as one tier of your backup strategy, not the only copy.

The rule I follow: critical data lives in MinIO (local, fast, always available) and gets synced offsite via rclone to Backblaze B2 (cheap cold storage at $6/TB/month). The 3-2-1 backup rule applies even when you’re self-hosting.

For anyone who’s been putting off cutting their cloud storage bill, the MinIO setup above takes about 20 minutes. Most S3-compatible tools will connect without any code changes — just swap the endpoint URL and credentials, and your existing backup jobs start writing to your own hardware.

Share: