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.