Install MinIO on a VPS: Self-Hosted S3-Compatible Storage
If your cPanel disk quota keeps filling up with backups, media dumps, or app uploads, renting AWS S3 isn't the only option. You can run your own S3-compatible object store on a Getwebup VPS with MinIO — same API, no per-GB egress surprises, and you keep full control of where the data lives. Here's how to set it up properly, and how to avoid the mistakes that turn a weekend project into a 2 AM page.
Why self-host object storage on a VPS
MinIO speaks the S3 API, so anything that already supports S3-compatible storage — WordPress offload plugins, backup tools like Restic or rclone, CI artifact stores, custom apps using the AWS SDK — will work against it without code changes. The appeal for a VPS setup is simple: predictable cost (you're paying for the VPS, not per-request and per-GB-out pricing), data residency you control, and one less dependency on a third-party account being in good standing.
It's not a fit for everything. If you need multi-region redundancy or you're storing terabytes with heavy public read traffic, a managed S3-compatible provider is usually cheaper once you factor in your own uptime and backup responsibility. For internal backups, WordPress media offload, or app file storage on a single VPS, MinIO is a solid, boring-in-a-good-way choice.
Prerequisites
- A VPS running Ubuntu 22.04/24.04 or AlmaLinux 9, with at least 2 GB RAM and separate disk space budgeted for object storage (don't share the root partition with a busy database)
- Root or sudo access over SSH
- A subdomain pointed at the VPS for the API (e.g.
s3.yourdomain.com) and one for the console (e.g.minio-console.yourdomain.com) - Nginx already installed, or willingness to install it as part of this guide
Step 1: Install the MinIO server
Download the official binary rather than relying on distro repos, which lag behind:
wget https://dl.min.io/server/minio/release/linux-amd64/minio
chmod +x minio
sudo mv minio /usr/local/bin/
Create a dedicated system user and a data directory — never run MinIO as root:
sudo useradd -r minio-user -s /sbin/nologin
sudo mkdir -p /mnt/minio-data
sudo chown minio-user:minio-user /mnt/minio-data
If you've attached a separate block volume for storage, mount it at /mnt/minio-data before this step so object data doesn't compete with the OS disk.
Step 2: Set credentials and create the systemd service
Put the root credentials in an environment file, not directly in the unit file, so they don't end up in systemctl status output or process listings:
sudo mkdir -p /etc/minio
sudo tee /etc/default/minio >/dev/null <<'EOF'
MINIO_ROOT_USER=your_admin_user
MINIO_ROOT_PASSWORD=use_a_long_random_string_here
MINIO_VOLUMES="/mnt/minio-data"
MINIO_OPTS="--console-address :9001"
EOF
sudo chmod 600 /etc/default/minio
Now the systemd unit:
sudo tee /etc/systemd/system/minio.service >/dev/null <<'EOF'
[Unit]
Description=MinIO Object Storage
After=network-online.target
Wants=network-online.target
[Service]
User=minio-user
Group=minio-user
EnvironmentFile=/etc/default/minio
ExecStart=/usr/local/bin/minio server $MINIO_VOLUMES $MINIO_OPTS
Restart=always
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now minio
Check it's actually up before moving on:
sudo systemctl status minio
sudo journalctl -u minio -n 50 --no-pager
Step 3: Open the right firewall ports — but not to the world
MinIO uses 9000 for the S3 API and 9001 for the web console. Don't expose either directly to the internet without TLS in front of them — the console in particular is an admin login screen. If you're on UFW:
sudo ufw allow from YOUR.OFFICE.IP.ADDR to any port 9001 proto tcp
sudo ufw allow 9000/tcp
sudo ufw reload
Port 9000 stays open because Nginx will proxy TLS traffic to it locally, but scope 9001 down to trusted IPs (or a VPN) — you generally don't need the console reachable from anywhere.
Step 4: Put Nginx and Let's Encrypt in front of it
Running MinIO behind a reverse proxy gets you HTTPS without touching MinIO's own TLS config, and lets you use normal subdomains instead of a raw IP:port. A minimal server block for the API:
server {
listen 80;
server_name s3.yourdomain.com;
client_max_body_size 0;
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_connect_timeout 300;
proxy_read_timeout 300;
chunked_transfer_encoding off;
}
}
Then issue the certificate and let Certbot handle the redirect to HTTPS:
sudo certbot --nginx -d s3.yourdomain.com
Repeat the same pattern for the console on its own subdomain, pointing proxy_pass at 127.0.0.1:9001 and adding WebSocket headers (proxy_set_header Upgrade $http_upgrade; and proxy_set_header Connection "upgrade";) — the console UI uses a WebSocket connection for live stats and will look broken without them.
Step 5: Create a bucket and scoped access keys
Install the mc client and point it at your new server:
curl https://dl.min.io/client/mc/release/linux-amd64/mc -o mc
chmod +x mc && sudo mv mc /usr/local/bin/
mc alias set myminio https://s3.yourdomain.com your_admin_user your_admin_password
Never hand out the root credentials to an app. Create a bucket and a dedicated user scoped to just that bucket:
mc mb myminio/site-backups
mc admin user add myminio backup-app app_secret_password
mc admin policy attach myminio readwrite --user backup-app
For anything narrower than full read-write, write a custom JSON policy and attach that instead — most integrations (rclone, Restic, WP offload plugins) only need access to one bucket.
Common problems and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Console loads but shows a blank white screen or spins forever | Missing WebSocket headers in the Nginx proxy block | Add Upgrade/Connection headers to the console's server block and reload Nginx |
SignatureDoesNotMatch from the S3 client | Client and server clocks have drifted, or the wrong region/endpoint is set | Sync time with chrony or ntpd; confirm the client's endpoint matches your proxied URL exactly, including the trailing path |
| Uploads over ~1 MB fail through the proxy | Nginx's default client_max_body_size is capping request size | Set client_max_body_size 0; (unlimited) or a sane cap like 500m in the server block |
| Disk fills up faster than expected | Bucket versioning is on and old versions are never pruned | Add a lifecycle rule to expire noncurrent versions: mc ilm add --noncurrent-expire-days 30 myminio/site-backups |
| MinIO won't start after a reboot | The mounted data volume didn't come back before the service started | Add the volume's mount point to /etc/fstab and confirm systemctl status minio.service shows it started after network-online.target, not before the mount |
Prevention: keep it boring
- Rotate the root credentials once setup is done, and never use the root user for application access — scoped users only
- Turn on bucket versioning selectively, not by default, and always pair it with a lifecycle rule
- Back up the MinIO data directory itself (or run erasure-coded multi-drive mode if you have the disks) — a single-VPS MinIO instance is still a single point of failure
- Watch disk usage with your normal VPS monitoring; object storage has a way of growing quietly until a backup job starts failing
- Keep the console restricted to a VPN or allow-listed IPs — it's an admin panel, treat it like one
Is this the right call for you?
| Self-hosted MinIO on your VPS | Managed S3-compatible provider | |
|---|---|---|
| Cost model | Fixed VPS cost, no egress fees | Pay per GB stored + per GB transferred out |
| Setup effort | You own install, TLS, monitoring, backups | Bucket ready in minutes |
| Data control | Stays on infrastructure you manage | Depends on provider's region and policies |
| Redundancy | Only what you build (RAID, multi-node, off-site backup) | Built in, usually multi-AZ |
If you're storing a few hundred GB of backups or WordPress media and want to stop paying transfer fees, self-hosting wins. If you need five-nines durability without babysitting it, a managed provider is worth the markup.
Getting help
If you're running MinIO on a Getwebup VPS and hit something the table above doesn't cover, open a ticket with your journalctl -u minio output and the Nginx error log — that's usually enough for our team to spot it in one pass.
Frequently asked questions
Is MinIO really S3-compatible, or will my existing tools need changes?
MinIO implements the S3 API directly, so tools like rclone, Restic, the AWS CLI, and WordPress S3-offload plugins work against it by just pointing the endpoint URL at your MinIO server and using your generated access/secret keys. You don't need to change how those tools upload or list objects.
Can I run MinIO on the same VPS as my cPanel or WordPress sites?
You can, but give it its own disk or partition and keep an eye on RAM and I/O contention. On a small VPS, heavy backup uploads to MinIO can compete with your web server for resources during peak traffic — a separate VPS or a mounted block volume is safer if the site is busy.
What happens if my single MinIO node's disk fails?
You lose everything on it, the same as any single-disk setup. Single-node MinIO isn't redundant on its own — pair it with off-site backups (rclone sync to another provider, or periodic snapshots of the data volume) or move to a multi-drive erasure-coded deployment once the data matters enough to justify it.
Do I need a separate subdomain for the API and the console?
You don't strictly need two subdomains, but it's cleaner and safer — it lets you lock the console down to trusted IPs via Nginx or a firewall rule while leaving the API endpoint reachable for your apps and backup jobs.
How do I upgrade MinIO without losing data?
Stop the service, replace the binary at /usr/local/bin/minio with the new release, and start it again — MinIO's on-disk format is backward compatible across releases. Your data directory isn't touched by the binary swap, but take a snapshot or backup first as a safety net before any major version jump.