MinIO is the leading open-source S3-compatible object storage server — the same API as Amazon S3, but running entirely on your Hong Kong VPS. Applications that use the AWS SDK, S3 browser tools, or any S3-compatible client work with MinIO without code changes. You eliminate AWS egress fees, keep data in Hong Kong’s jurisdiction, and give mainland Chinese applications fast access to stored objects via CN2 GIA routing rather than routing through US S3 endpoints.
Use Cases
- User-uploaded files (profile photos, documents, attachments)
- Application media storage (product images, videos)
- Database and VPS backups (replace off-site backup destinations)
- Static website assets served directly from MinIO
- Docker image layers (with MinIO as registry backend)
- Log archival and data lake storage
Step 1: Deploy MinIO
<code">mkdir -p /opt/minio/{data,config}
# Single-node deployment (suitable for most use cases)
docker run -d \
--name minio \
--restart unless-stopped \
-p 127.0.0.1:9000:9000 \ # S3 API endpoint
-p 127.0.0.1:9001:9001 \ # MinIO Console (web UI)
-v /opt/minio/data:/data \
-e MINIO_ROOT_USER=admin \
-e MINIO_ROOT_PASSWORD=StrongMinioPassword! \
-e MINIO_BROWSER_REDIRECT_URL=https://storage-console.yourdomain.com \
quay.io/minio/minio server /data \
--console-address ":9001"
docker logs minio | tail -10Step 2: Nginx with SSL for S3 API and Console
<code">cat > /etc/nginx/sites-available/minio << 'EOF'
# S3 API endpoint — used by applications
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 uploads (adjust for your max file size)
client_max_body_size 5G;
client_body_timeout 300s;
proxy_request_buffering off; # Stream directly to MinIO
ignore_invalid_headers 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 300;
proxy_send_timeout 300;
proxy_read_timeout 300;
proxy_buffering off;
}
}
# Admin console — restrict access
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;
# Restrict to admin IPs
allow YOUR_OFFICE_IP;
deny all;
location / {
proxy_pass http://127.0.0.1:9001;
proxy_set_header Host $http_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
certbot --nginx -d storage.yourdomain.com -d storage-console.yourdomain.com
nginx -t && systemctl reload nginxStep 3: Create Buckets and Access 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 connect to your MinIO instance
mc alias set hkminio https://storage.yourdomain.com admin StrongMinioPassword!
# Create buckets
mc mb hkminio/app-uploads # User uploads
mc mb hkminio/app-media # Product images, videos
mc mb hkminio/backups # Database backups (private)
mc mb hkminio/logs # Log archival (private)
# Make media bucket public (read-only)
mc anonymous set download hkminio/app-media
# Create application user with limited permissions
mc admin user add hkminio app-user StrongAppUserPass!
mc admin policy attach hkminio readwrite --user app-user
# Or create a fine-grained policy:
cat > /tmp/app-policy.json << 'POLICY'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": ["arn:aws:s3:::app-uploads/*", "arn:aws:s3:::app-media/*"]
},
{
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": ["arn:aws:s3:::app-uploads", "arn:aws:s3:::app-media"]
}
]
}
POLICY
mc admin policy create hkminio app-policy /tmp/app-policy.json
mc admin policy attach hkminio app-policy --user app-userStep 4: Use with AWS SDK (Drop-in Replacement)
Node.js
<code"># npm install @aws-sdk/client-s3 @aws-sdk/s3-presigned-post
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
const s3 = new S3Client({
endpoint: "https://storage.yourdomain.com", // MinIO endpoint
region: "us-east-1", // Required by SDK (any value works with MinIO)
credentials: {
accessKeyId: "app-user",
secretAccessKey: "StrongAppUserPass!"
},
forcePathStyle: true, // Required for MinIO — uses path-style URLs
})
// Upload a file
await s3.send(new PutObjectCommand({
Bucket: "app-uploads",
Key: `users/${userId}/avatar.jpg`,
Body: fileBuffer,
ContentType: "image/jpeg",
ACL: "private"
}))
// Generate presigned URL for direct upload (client-side, bypasses your server)
const presignedUrl = await getSignedUrl(s3, new PutObjectCommand({
Bucket: "app-uploads",
Key: `uploads/${Date.now()}-${filename}`,
}), { expiresIn: 3600 }) // URL valid for 1 hour
// Generate presigned URL for download (private objects)
const downloadUrl = await getSignedUrl(s3, new GetObjectCommand({
Bucket: "app-uploads",
Key: `users/${userId}/document.pdf`,
}), { expiresIn: 300 }) // 5 minutesPython (boto3)
<code"># pip install boto3 --break-system-packages
import boto3
from botocore.client import Config
s3 = boto3.client(
's3',
endpoint_url='https://storage.yourdomain.com',
aws_access_key_id='app-user',
aws_secret_access_key='StrongAppUserPass!',
config=Config(signature_version='s3v4'),
region_name='us-east-1'
)
# Upload
s3.upload_file('local_file.pdf', 'app-uploads', 'remote/path/file.pdf')
# Download
s3.download_file('app-uploads', 'remote/path/file.pdf', 'local_copy.pdf')
# List objects
response = s3.list_objects_v2(Bucket='app-uploads', Prefix='users/')
for obj in response.get('Contents', []):
print(obj['Key'], obj['Size'])Step 5: Integrate with Applications
Laravel (Filesystem)
<code"># config/filesystems.php — add MinIO disk
's3' => [
'driver' => 's3',
'key' => env('MINIO_ACCESS_KEY', 'app-user'),
'secret' => env('MINIO_SECRET_KEY', 'StrongAppUserPass!'),
'region' => 'us-east-1',
'bucket' => env('MINIO_BUCKET', 'app-uploads'),
'url' => env('MINIO_URL', 'https://storage.yourdomain.com'),
'endpoint' => env('MINIO_ENDPOINT', 'https://storage.yourdomain.com'),
'use_path_style_endpoint' => true,
],
# In controller:
Storage::disk('s3')->put('users/avatar.jpg', $file);
$url = Storage::disk('s3')->temporaryUrl('users/avatar.jpg', now()->addMinutes(30));Django (django-storages)
<code"># pip install django-storages boto3 # settings.py: DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' AWS_ACCESS_KEY_ID = 'app-user' AWS_SECRET_ACCESS_KEY = 'StrongAppUserPass!' AWS_S3_ENDPOINT_URL = 'https://storage.yourdomain.com' AWS_STORAGE_BUCKET_NAME = 'app-uploads' AWS_S3_REGION_NAME = 'us-east-1' AWS_S3_FILE_OVERWRITE = False AWS_DEFAULT_ACL = 'private'
Step 6: Automated Backup to MinIO
<code"># Backup PostgreSQL to MinIO daily
cat > /opt/backup-to-minio.sh << 'EOF' #!/bin/bash DATE=$(date +%Y%m%d_%H%M%S) # Dump PostgreSQL pg_dump -U postgres myapp_production | \ gzip > /tmp/pg_backup_${DATE}.sql.gz
# Upload to MinIO
/usr/local/bin/mc cp /tmp/pg_backup_${DATE}.sql.gz \
hkminio/backups/postgres/pg_${DATE}.sql.gz
# Keep only last 30 days in MinIO
/usr/local/bin/mc rm --recursive --force \
--older-than 30d hkminio/backups/postgres/
rm /tmp/pg_backup_${DATE}.sql.gz
echo "Backup complete: $DATE"
EOF
chmod +x /opt/backup-to-minio.sh
crontab -l | { cat; echo "0 3 * * * /opt/backup-to-minio.sh"; } | crontab -Cost Comparison: MinIO on VPS vs AWS S3
| Cost Factor | AWS S3 (Asia Pacific) | MinIO on HK VPS |
|---|---|---|
| Storage (100 GB) | ~$2.30/month | Disk cost only (~$0.05/GB) |
| Egress (100 GB to internet) | ~$8.50/month | Included in VPS bandwidth |
| API requests (1M) | ~$0.40 | $0 |
| China latency | 200–400ms | 30–60ms (CN2 GIA) |
| Data jurisdiction | AWS US-East (default) | Hong Kong (your control) |
Conclusion
MinIO on a Hong Kong VPS is a drop-in S3 replacement that eliminates AWS egress fees, keeps data in Hong Kong’s jurisdiction, and serves object requests to mainland Chinese users at 30–60ms rather than 200–400ms from AWS S3. The S3-compatible API means zero application code changes — swap the endpoint URL and credentials. Automated PostgreSQL backups to a private MinIO bucket replace the need for any external backup service.
Host your own S3: Browse Server.HK Hong Kong VPS plans — add extra NVMe storage for large object repositories; MinIO itself uses minimal RAM alongside your application workload.