MinIO is an S3-compatible object storage server. Any tool that speaks the Amazon S3 API — backup software, application SDKs, media uploads, static site storage — can use it instead of AWS. On a VPS with a large disk, it gives you your own private S3 endpoint.

A note on licensing and editions: MinIO is licensed under the AGPLv3, and in recent years the project has moved features out of the community edition’s web console and changed how binaries are distributed. Check the current state of the community edition before building on it. Alternatives with an S3 API include Garage (lightweight, geo-distributed — see our Garage guide), SeaweedFS and Ceph RGW.

When object storage on a VPS makes sense

  • Backup target for restic, Borg (via rclone) or database dumps.
  • Media and upload storage for apps (WordPress offload, Nextcloud external storage, Mastodon, PeerTube).
  • Development and testing against an S3 API without cloud costs.

For data you can’t afford to lose, remember a single VPS is a single location — keep a second copy elsewhere.

1. Run MinIO with Docker

mkdir -p ~/minio && cd ~/minio
sudo mkdir -p /srv/minio/data

compose.yaml:

services:
  minio:
    image: minio/minio:latest
    restart: unless-stopped
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
      MINIO_SERVER_URL: https://s3.example.com
      MINIO_BROWSER_REDIRECT_URL: https://console.example.com
    volumes:
      - /srv/minio/data:/data
    ports:
      - "127.0.0.1:9000:9000"   # S3 API
      - "127.0.0.1:9001:9001"   # web console
cat > .env <<EOF
MINIO_ROOT_USER=admin-$(openssl rand -hex 4)
MINIO_ROOT_PASSWORD=$(openssl rand -base64 30)
EOF
chmod 600 .env
docker compose up -d

Pin a specific release tag for production.

2. HTTPS for the API and console

Two hostnames keep things clean: s3.example.com for the API, console.example.com for the UI.

server {
    listen 80;
    server_name s3.example.com;

    client_max_body_size 0;          # no upload limit
    proxy_buffering off;
    proxy_request_buffering off;

    location / {
        proxy_pass http://127.0.0.1:9000;
        proxy_set_header Host $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_http_version 1.1;
        proxy_set_header Connection "";
        chunked_transfer_encoding off;
    }
}

server {
    listen 80;
    server_name console.example.com;

    location / {
        proxy_pass http://127.0.0.1:9001;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

The Host header must be passed unchanged — S3 request signatures include it. Then sudo certbot --nginx -d s3.example.com -d console.example.com. Restrict the console to your IP or VPN if you can.

3. The mc client

curl -O https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc && sudo mv mc /usr/local/bin/
mc alias set myminio https://s3.example.com "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"
mc admin info myminio

Create a bucket and copy files:

mc mb myminio/backups
mc cp ./db.sql.gz myminio/backups/
mc ls myminio/backups
mc mirror --overwrite ./uploads myminio/uploads

4. Users and least-privilege policies

Never give applications the root credentials. Create a user per application with access to its bucket only.

backup-policy.json:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket"],
    "Resource": ["arn:aws:s3:::backups", "arn:aws:s3:::backups/*"]
  }]
}
mc admin policy create myminio backup-rw backup-policy.json
mc admin user add myminio backupuser "$(openssl rand -base64 24)"
mc admin policy attach myminio backup-rw --user backupuser

5. Use it with restic

export AWS_ACCESS_KEY_ID=backupuser
export AWS_SECRET_ACCESS_KEY=the-secret
export RESTIC_REPOSITORY=s3:https://s3.example.com/backups/web1
export RESTIC_PASSWORD_FILE=/root/.restic-pass
restic init
restic backup /var/www /etc

See restic backups and backing up a VPS to cloud storage.

6. Protect against deletion: versioning and object locking

mc version enable myminio/backups

Versioning keeps previous versions of overwritten or deleted objects. For ransomware protection, create a bucket with object lock (mc mb --with-lock) and a retention period — then even valid credentials can’t delete backups early.

Single-disk limitations

A single-node, single-drive MinIO has no erasure coding: if the disk fails, data is gone. That’s acceptable for a secondary backup copy, not as the only copy. For redundancy, use multiple drives/nodes or replicate to another location (mc mirror or bucket replication).

A Storage VPS with 500 GB of SSD makes a practical private S3 endpoint for backups and app uploads.

  • MinIO
  • S3
  • Self-Hosting