Install

# Debian / Ubuntu
curl -L -o /usr/local/bin/minio \
    https://dl.min.io/server/minio/release/linux-amd64/minio
chmod +x /usr/local/bin/minio

# Add the matching mc client (admin/scripting CLI)
curl -L -o /usr/local/bin/mc \
    https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x /usr/local/bin/mc

# Create a dedicated user and storage directory
sudo useradd -r -s /sbin/nologin minio-user
sudo mkdir -p /srv/minio/data
sudo chown -R minio-user:minio-user /srv/minio

Single-node, single-drive (homelab pattern)

Create /etc/default/minio:

MINIO_VOLUMES="/srv/minio/data"
MINIO_OPTS="--address :9000 --console-address :9001"
MINIO_ROOT_USER="<long-random-string>"
MINIO_ROOT_PASSWORD="<another-long-random-string>"

Systemd unit:

# /etc/systemd/system/minio.service
[Unit]
Description=MinIO
After=network-online.target
Wants=network-online.target

[Service]
WorkingDirectory=/usr/local/
User=minio-user
Group=minio-user
EnvironmentFile=/etc/default/minio
ExecStart=/usr/local/bin/minio server $MINIO_OPTS $MINIO_VOLUMES
Restart=always
LimitNOFILE=1048576
TasksMax=infinity

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now minio
sudo systemctl status minio
journalctl -u minio -f

The web console is now on http://<host>:9001; the S3 API is on port 9000. Put both behind a reverse proxy with HTTPS — many S3 clients flat-out refuse HTTP, and others (restic, the AWS SDKs) require a trustworthy CA chain or an explicit override.

Reverse proxy

# Caddy
s3.example.com {
    reverse_proxy 127.0.0.1:9000
    request_body { max_size 50GB }
}

s3-console.example.com {
    reverse_proxy 127.0.0.1:9001
}
# nginx
server {
    listen 443 ssl http2;
    server_name s3.example.com;
    # ssl_certificate / ssl_certificate_key ...

    client_max_body_size 50G;
    proxy_request_buffering off;
    proxy_buffering off;

    location / {
        proxy_pass http://127.0.0.1:9000;
        proxy_set_header Host              $http_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_connect_timeout 300s;
    }
}

proxy_request_buffering off matters: large object uploads stream straight through to MinIO instead of being spooled to nginx's disk.

mc: the CLI client

# Register the local MinIO as an alias
mc alias set local https://s3.example.com <root-user> <root-password>

# Create a bucket
mc mb local/backups
mc mb local/photos

# Upload / download / sync
mc cp file.txt local/backups/
mc cp -r ./photos/ local/photos/
mc mirror ./website/ local/website/

# List
mc ls local/backups

# Bucket info
mc admin info local
mc admin info local --json | jq .info.servers

Create a scoped user (not the root account)

The MINIO_ROOT_USER should not be in any application's config. Create a scoped user with a policy:

# Custom policy: read/write only the "backups" bucket
cat > backups-policy.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": ["arn:aws:s3:::backups"]
    },
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:GetObjectVersion"],
      "Resource": ["arn:aws:s3:::backups/*"]
    }
  ]
}
EOF

mc admin policy create local backups-rw backups-policy.json
mc admin user add local restic-backup <random-secret>
mc admin policy attach local backups-rw --user restic-backup

Now restic-backup can only act on the backups bucket. Point restic at s3.example.com with that user's credentials.

Object lock for immutable backups

For backup buckets, object lock turns the bucket into write-once-read-many for a retention window — even the bucket owner can't delete an object before its lock expires.

mc mb --with-lock local/backups-immutable
mc retention set --default GOVERNANCE 90d local/backups-immutable

Combined with restic's append-only mode, this is the cleanest defense against ransomware that wants to delete its target's backups.

Distributed mode (high availability)

For real production, MinIO is most useful as a distributed cluster: four or more nodes, each with one or more disks, with erasure-coded redundancy. Single command on each node:

minio server \
    http://node{1...4}.s3.lab/srv/minio/data{1...4}

That command, run on each of the four nodes, brings up a cluster with 4 nodes × 4 drives = 16 drives total. Default erasure code is EC:N/2 — the cluster tolerates loss of half the drives before any data is unavailable. Reads are served from any node.

Adding nodes later is the "server pool" concept: a separate command line that names a second pool, MinIO routes new writes across both pools. Existing data isn't redistributed unless you run a rebalance.

Lifecycle rules

Auto-tier or auto-expire objects:

mc ilm rule add --expire-days 30 --tag "purpose=tmp" local/logs
mc ilm rule add --transition-days 60 --transition-tier glacier-mirror local/archive

Useful for log buckets that should self-clean, or for tiering cold objects to a remote MinIO (or actual S3 Glacier) after some interval.

Bucket replication to another MinIO / actual S3

mc admin replicate add local --remote-bucket arn:minio:replication:::remote/backups

Useful for one-direction backup of a primary cluster to a smaller secondary cluster in a different building — the replication is asynchronous and runs per object change.

Backups for MinIO itself

  • The data directory — this is the objects themselves; back it up via filesystem snapshots (ZFS, Btrfs) or replication to another MinIO.
  • The metadata — MinIO stores object metadata inline with the objects, so a consistent snapshot of MINIO_VOLUMES captures both.
  • The IAM state — users, policies, service accounts. mc admin cluster bucket export and mc admin user export local > users.zip dump these as portable files.

For a single-node homelab, ZFS snapshot + send is the simplest. For distributed clusters, the cluster's own erasure coding plus a second cluster replica is more idiomatic.