MinIO on a VPS: Self-Hosted S3-Compatible Object Storage for Backups and Application Assets

MinIO on a VPS: Self-Hosted S3-Compatible Object Storage for Backups and Application Assets

MinIO is a high-performance, self-hosted object storage server fully compatible with the Amazon S3 API. Any application using the AWS S3 SDK can point to MinIO instead — no code changes, just a different endpoint URL. Self-hosting MinIO on a VPS gives you private S3-compatible storage for application file uploads, backups, media assets, and data lake workflows at disk cost instead of $0.023/GB/month on AWS S3.

When to Use MinIO vs Cloudflare R2 vs AWS S3

  • MinIO on VPS: Data must stay on-premises, high-volume internal transfers, complete control, no egress fees within the same VPS
  • Cloudflare R2: Free egress, global CDN, no VPS disk needed — best for public-facing assets
  • AWS S3: Maximum ecosystem compatibility, multi-region replication, managed service SLAs

Step 1: Docker Compose Setup

<code">mkdir -p /opt/minio/{data,config} && cd /opt/minio
nano docker-compose.yml
<code">version: '3.8'

services:
  minio:
    image: minio/minio:latest
    container_name: minio
    restart: always
    command: server /data --console-address ":9001"
    ports:
      - "127.0.0.1:9000:9000"   # S3 API
      - "127.0.0.1:9001:9001"   # Web console
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
      MINIO_DOMAIN: s3.yourdomain.com
      MINIO_SITE_NAME: my-minio
    volumes:
      - ./data:/data
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 10s
      retries: 3
<code">cat > .env << 'EOF'
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=StrongMinioPassword!ChangeThis
EOF
chmod 600 .env

docker compose up -d
docker compose logs -f minio

Step 2: Nginx Reverse Proxy

<code">sudo nano /etc/nginx/sites-available/minio
<code">server {
    listen 80;
    server_name s3.yourdomain.com console.s3.yourdomain.com;
    return 301 https://$host$request_uri;
}

# S3 API endpoint
server {
    listen 443 ssl http2;
    server_name s3.yourdomain.com;

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

    # Allow large file uploads
    client_max_body_size 10000M;
    proxy_read_timeout 300s;

    # Required for MinIO virtual-hosted-style bucket addressing
    # Allows bucket.s3.yourdomain.com to work
    location / {
        proxy_pass http://127.0.0.1:9000;
        proxy_http_version 1.1;
        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_buffering off;
        proxy_request_buffering off;
        chunked_transfer_encoding off;
    }
}

# Web console
server {
    listen 443 ssl http2;
    server_name console.s3.yourdomain.com;

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

    location / {
        proxy_pass http://127.0.0.1:9001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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;
    }
}
<code">sudo ln -s /etc/nginx/sites-available/minio /etc/nginx/sites-enabled/
sudo certbot --nginx -d s3.yourdomain.com -d console.s3.yourdomain.com
sudo systemctl reload nginx

Step 3: MinIO Client (mc) Setup

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

# Configure alias for your server
mc alias set myminio https://s3.yourdomain.com minioadmin StrongMinioPassword!ChangeThis

# Verify connection
mc admin info myminio

Step 4: Create Buckets and Service Accounts

<code"># Create buckets
mc mb myminio/app-uploads      # Application file uploads
mc mb myminio/backups          # Restic/database backups
mc mb myminio/media            # Media assets
mc mb myminio/logs             # Log archival

# Create service accounts (access key pairs — not root credentials)
mc admin user svcacct add myminio app-service

# Create a policy for limited access (only the app-uploads bucket)
cat > app-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket"],
      "Resource": ["arn:aws:s3:::app-uploads", "arn:aws:s3:::app-uploads/*"]
    }
  ]
}
EOF

mc admin policy create myminio app-policy app-policy.json
mc admin user add myminio appuser StrongUserPassword!
mc admin policy attach myminio app-policy --user appuser

Step 5: Use AWS SDK with MinIO

<code">pip install boto3
<code">import boto3
from botocore.client import Config

# Connect to MinIO using boto3 (S3 SDK)
s3 = boto3.client(
    's3',
    endpoint_url='https://s3.yourdomain.com',
    aws_access_key_id='YOUR_ACCESS_KEY',
    aws_secret_access_key='YOUR_SECRET_KEY',
    config=Config(signature_version='s3v4'),
    region_name='us-east-1',  # MinIO ignores this but boto3 requires it
)

# Upload a file
s3.upload_file('local-file.pdf', 'app-uploads', 'documents/file.pdf')

# Generate a presigned URL (for user downloads without exposing credentials)
url = s3.generate_presigned_url(
    'get_object',
    Params={'Bucket': 'app-uploads', 'Key': 'documents/file.pdf'},
    ExpiresIn=3600,  # 1 hour
)
print(url)

# Download a file
s3.download_file('app-uploads', 'documents/file.pdf', '/tmp/downloaded.pdf')

# List objects
objects = s3.list_objects_v2(Bucket='app-uploads', Prefix='documents/')
for obj in objects.get('Contents', []):
    print(obj['Key'], obj['Size'])

Step 6: Bucket Versioning and Lifecycle Policies

<code"># Enable versioning (keep deleted/overwritten files)
mc version enable myminio/app-uploads

# Set lifecycle policy — auto-delete old versions after 30 days
mc ilm rule add myminio/backups \
    --expire-delete-marker \
    --noncurrent-expire-days 30

# Transition old objects to lower storage tier (if using MinIO Tiering)
# Or set expiry for log archival:
mc ilm rule add myminio/logs \
    --expire-days 90   # Delete log objects after 90 days

Step 7: Integrate with Restic for Encrypted Backups

<code"># Use MinIO as Restic backup destination
export RESTIC_REPOSITORY="s3:https://s3.yourdomain.com/backups"
export RESTIC_PASSWORD="YourEncryptionPassword"
export AWS_ACCESS_KEY_ID="YOUR_ACCESS_KEY"
export AWS_SECRET_ACCESS_KEY="YOUR_SECRET_KEY"

restic init
restic backup /var/www /opt --exclude /opt/minio/data
restic snapshots   # List backups

Getting Started

MinIO’s resource usage scales with stored data and request load — at rest, the server itself uses 50–100 MB RAM. A 2 GB Ubuntu VPS at VPS.DO runs MinIO alongside your main application. For large file storage, ensure you have sufficient disk or attach an additional volume. MinIO’s S3-API compatibility means any tool that supports S3 — Restic, rclone, Nextcloud, Appwrite, Listmonk — connects to MinIO without special configuration beyond the endpoint URL.

Conclusion

Self-hosted MinIO provides a private S3-compatible object storage layer — full AWS S3 API, bucket management, versioning, lifecycle policies, and pre-signed URLs — at disk cost. The S3 API compatibility means all existing S3-integrated tools work identically. For teams storing application uploads, database backups, and media assets where data privacy matters and egress costs are a concern, MinIO on a VPS is the right alternative to cloud object storage services.

Fast • Reliable • Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DO’s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!