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-dataIf 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/minioNow 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 minioCheck it's actually up before moving on:
sudo systemctl status minio
sudo journalctl -u minio -n 50 --no-pagerStep 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 reloadPort 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.comRepeat 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_passwordNever 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-appFor 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.