MinIO is the leading open-source S3-compatible object store — compatible with every AWS SDK, CLI, and tool that speaks S3, but running entirely on your own infrastructure. Deployed on a Hong Kong VPS, MinIO gives your applications S3-compatible file storage with CN2 GIA routing for fast uploads from mainland China, no per-GB AWS transfer fees, and complete data sovereignty for files that must not leave Asian infrastructure.
Why Self-Host Object Storage?
- AWS S3 egress costs — S3 charges $0.09/GB for data transfer out to the internet; at high volumes this becomes significant. MinIO on a VPS with unmetered bandwidth eliminates egress fees
- China accessibility — AWS S3 endpoints are slow to unreliable from mainland China; MinIO on a Hong Kong VPS with CN2 GIA delivers fast, consistent upload and download speeds to Chinese users
- Data residency — files stay in Hong Kong on hardware you control
- S3 API compatibility — existing code using AWS SDK requires only endpoint URL and credentials changes
- Cost predictability — flat VPS cost versus variable per-request and per-GB AWS pricing
VPS Storage Planning
| Use Case | Expected Storage | VPS Disk | Notes |
|---|---|---|---|
| App file uploads (images, docs) | 50–500 GB | 200 GB+ NVMe | Start small, expand |
| Media platform (video) | 1–10 TB | Additional block vol. | Attach extra storage |
| Database backups | 100–500 GB | 200 GB NVMe | Compressed + rotated |
| Software distribution | 100 GB–1 TB | 500 GB+ NVMe | High read, low write |
Step 1: Deploy MinIO with Docker
apt update && apt upgrade -y
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
mkdir -p /opt/minio /mnt/minio-data
cat > /opt/minio/docker-compose.yml << 'EOF'
version: '3.8'
services:
minio:
image: minio/minio:latest
container_name: minio
restart: unless-stopped
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: minioadmin
MINIO_ROOT_PASSWORD: CHANGE_THIS_STRONG_PASSWORD_32CHARS
MINIO_SITE_NAME: hk-vps-storage
# Optional: set public URL for pre-signed URLs
MINIO_SERVER_URL: https://storage.yourdomain.com
MINIO_BROWSER_REDIRECT_URL: https://storage-console.yourdomain.com
volumes:
- /mnt/minio-data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 10s
retries: 3
EOF
cd /opt/minio && docker compose up -d
docker compose logs minioStep 2: Configure Nginx for S3 API and Console
<code">apt install -y nginx certbot python3-certbot-nginx
cat > /etc/nginx/sites-available/minio << 'EOF'
# S3 API endpoint
server {
listen 443 ssl http2;
server_name storage.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/storage.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/storage.yourdomain.com/privkey.pem;
# Allow large file uploads
client_max_body_size 10G;
client_body_timeout 300s;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
ignore_invalid_headers off;
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;
}
}
# Web console
server {
listen 443 ssl http2;
server_name storage-console.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/storage-console.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/storage-console.yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:9001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
EOF
ln -s /etc/nginx/sites-available/minio /etc/nginx/sites-enabled/
certbot --nginx -d storage.yourdomain.com -d storage-console.yourdomain.com
nginx -t && systemctl reload nginxStep 3: Create Buckets and Policies
<code"># Install MinIO client wget https://dl.min.io/client/mc/release/linux-amd64/mc -O /usr/local/bin/mc chmod +x /usr/local/bin/mc # Configure mc to point to your MinIO instance mc alias set myhk https://storage.yourdomain.com minioadmin CHANGE_THIS_STRONG_PASSWORD_32CHARS # Create buckets mc mb myhk/app-uploads # Private (user uploads) mc mb myhk/public-assets # Public (images, CSS, JS) mc mb myhk/database-backups # Private (DB dumps) mc mb myhk/media # Private (media files) # Make public-assets bucket publicly readable mc anonymous set download myhk/public-assets # Verify mc ls myhk
Create Application Service Account
<code">cat > /tmp/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"]
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": ["arn:aws:s3:::public-assets/*"]
},
{
"Effect": "Allow",
"Action": ["s3:PutObject"],
"Resource": ["arn:aws:s3:::database-backups/*"]
}
]
}
EOF
mc admin policy create myhk app-policy /tmp/app-policy.json
mc admin user add myhk appuser StrongUserPassword!
mc admin policy attach myhk app-policy --user appuserStep 4: Use MinIO in Your Application (Drop-in S3 Replacement)
Node.js with AWS SDK v3
<code">npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const s3 = new S3Client({
endpoint: 'https://storage.yourdomain.com',
region: 'us-east-1', // MinIO ignores this but SDK requires it
credentials: {
accessKeyId: 'appuser',
secretAccessKey: 'StrongUserPassword!'
},
forcePathStyle: true // Required for MinIO
});
// Upload a file
async function uploadFile(buffer, filename, contentType) {
await s3.send(new PutObjectCommand({
Bucket: 'app-uploads',
Key: filename,
Body: buffer,
ContentType: contentType
}));
return `https://storage.yourdomain.com/app-uploads/${filename}`;
}
// Generate pre-signed download URL (expires in 1 hour)
async function getDownloadUrl(filename) {
const command = new GetObjectCommand({ Bucket: 'app-uploads', Key: filename });
return await getSignedUrl(s3, command, { expiresIn: 3600 });
}Python with boto3
<code">pip install boto3
import boto3
s3 = boto3.client(
's3',
endpoint_url='https://storage.yourdomain.com',
aws_access_key_id='appuser',
aws_secret_access_key='StrongUserPassword!',
region_name='us-east-1'
)
# Upload
s3.upload_file('/tmp/report.pdf', 'app-uploads', 'reports/report-2026.pdf')
# Download
s3.download_file('app-uploads', 'reports/report-2026.pdf', '/tmp/downloaded.pdf')
# Pre-signed URL
url = s3.generate_presigned_url(
'get_object',
Params={'Bucket': 'app-uploads', 'Key': 'reports/report-2026.pdf'},
ExpiresIn=3600
)Migrate from AWS S3 (Code Change Only)
<code"># If you use boto3 or AWS SDK, only the endpoint changes: # Before: no endpoint_url (uses AWS S3) # After: endpoint_url='https://storage.yourdomain.com', forcePathStyle=true # All bucket names, key patterns, and API calls remain identical
Step 5: Automated Database Backups to MinIO
<code">cat > /opt/backup-to-minio.sh << 'EOF' #!/bin/bash DATE=$(date +%Y%m%d_%H%M%S) # Dump PostgreSQL sudo -u postgres pg_dumpall | gzip > /tmp/pgdump_$DATE.sql.gz
# Upload to MinIO
mc cp /tmp/pgdump_$DATE.sql.gz myhk/database-backups/postgres/
# Cleanup local
rm /tmp/pgdump_$DATE.sql.gz
# Keep only 30 days of backups in MinIO
mc rm --recursive --force --older-than 30d myhk/database-backups/
echo "Backup complete: $DATE"
EOF
chmod +x /opt/backup-to-minio.sh
crontab -l | { cat; echo "0 3 * * * /opt/backup-to-minio.sh"; } | crontab -Step 6: MinIO Lifecycle Policies (Automatic Expiry)
<code"># Auto-delete temporary upload files after 7 days
cat > /tmp/lifecycle.json << 'EOF'
{
"Rules": [{
"ID": "delete-temp-uploads",
"Status": "Enabled",
"Filter": {"Prefix": "temp/"},
"Expiration": {"Days": 7}
}]
}
EOF
mc ilm import myhk/app-uploads < /tmp/lifecycle.json
mc ilm ls myhk/app-uploadsPerformance: MinIO on HK VPS vs AWS S3 for Asia Users
| Operation | AWS S3 (us-east-1) from Shanghai | MinIO HK VPS (CN2 GIA) from Shanghai |
|---|---|---|
| Upload latency (first byte) | 250–400ms | 30–55ms |
| 10 MB file upload | 4–12s | 0.5–2s |
| Pre-signed URL generation | 200–350ms API call | 20–40ms (local MinIO) |
| Cost (100 GB egress/month) | ~$9/month | $0 (included bandwidth) |
Conclusion
MinIO on a Hong Kong VPS is a compelling replacement for AWS S3 when your primary users are in Asia-Pacific — especially mainland China where AWS S3 latency and reliability are significant pain points. The S3-compatible API means zero application code changes beyond swapping the endpoint URL, and CN2 GIA routing delivers upload and download speeds that match or exceed what Chinese users experience with any US-hosted storage service.
Self-host your storage: Browse Server.HK Hong Kong VPS plans — choose a plan with NVMe storage matching your expected file volume, or attach additional block storage volumes as your library grows.