MinIO on a VPS: Self-Hosted S3 Storage for Backups and App Files
Backups usually fail for a boring reason: nobody decided clearly where they should live. Archives pile up on the same disk as the app, then disappear along with that disk when something goes wrong. Or you pay a hosted object storage bill for a workload so small the invoice feels out of proportion.
MinIO is a straightforward fix. It is an S3-compatible object storage server you can run on your own VPS. Anything that already talks to S3 — backup scripts, a CMS, a web app — can usually point at MinIO with little change. The difference is that your data stays on hardware you control, and the monthly cost is the VPS you already pay for.
This guide takes you from zero to a working backup target: Docker Compose setup, a bucket and a dedicated access key, upload and download tests, then one real backup sent to MinIO. It ends with a security warning you should not skip: the MinIO console must never sit exposed on the public internet without protection.
What you need first
You need a VPS with Docker and Docker Compose installed, plus SSH access as root or a sudo user. A single MinIO node runs fine on 2 GB of RAM with a few GB free for a first setup, but do not treat one node as your only copy of anything important. A backup that exists in exactly one place is not a backup strategy yet. Near the end you will see how to add a second copy.
If you plan to expose the console over HTTPS later, prepare a domain or subdomain — for example minio.yourdomain.com for the console and s3.yourdomain.com for the API. Neither is required for the install itself: you can work through an SSH tunnel first and add a reverse proxy afterwards.
Step 1: Create the folder and the Compose file
Keep MinIO's configuration in its own folder, away from your other projects:
sudo mkdir -p /opt/minio
cd /opt/minioNow create /opt/minio/docker-compose.yml:
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"
- "127.0.0.1:9001:9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
volumes:
- minio_data:/data
volumes:
minio_data:A few choices here are deliberate:
- The command
server /data --console-address ":9001"tells MinIO to store objects in/dataand serve the web console on port 9001. Port 9000 is the S3 API, where apps and CLIs send files. - Ports bind to
127.0.0.1, not to every interface. MinIO is reachable only from inside the VPS until you put a reverse proxy in front of it. That is intentional: the easiest way to get MinIO compromised is to leave its console reachable directly from the internet. - Root credentials do not live in the Compose file. They come from a separate
.envfile.
Create /opt/minio/.env and replace the values with strong credentials of your own:
MINIO_ROOT_USER=your-minio-admin
MINIO_ROOT_PASSWORD=replace-with-a-long-random-passwordA MinIO root password must be at least 8 characters, but for the root account go far beyond that — letters, numbers, and symbols, stored in a password manager. Then lock the file down:
sudo chmod 600 /opt/minio/.envExpected result: /opt/minio holds docker-compose.yml and .env, and .env is not readable by other users on the server.
Step 2: Start MinIO and check it is alive
From the same folder, run:
cd /opt/minio
sudo docker compose up -d
sudo docker compose psHealthy output shows the minio container as Up or running. If it keeps restarting, read the logs before guessing:
sudo docker compose logs --tail 50 minioThe two usual culprits are a root password shorter than 8 characters, or ports 9000/9001 already taken by another service. The log normally names the problem plainly. Fix it, then run sudo docker compose up -d again.
Confirm the API answers from inside the VPS:
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:9000/minio/health/liveExpected result: 200. The S3 API is up and ready for connections.
Step 3: Create a bucket and a dedicated app key
Open the console through an SSH tunnel, not by opening a port to the internet:
ssh -L 9001:127.0.0.1:9001 root@YOUR-VPS-IPWhile the tunnel is open, visit http://127.0.0.1:9001 in your browser and sign in with the root credentials from .env. Create a bucket — the example here is backup-aplikasi. Bucket names use lowercase letters, numbers, dots, and hyphens only; no capitals or spaces.
Do not stop at the root account. Create a separate user for backups — for example backup-writer — under Identity, then Users, and attach a policy limited to the backup bucket. A narrowed version of readWrite, or a custom policy that only writes to backup-aplikasi, is the right shape. Scripts should never need root access to your object store.
Install the MinIO Client, mc, which is far more convenient than the console for testing and scripts:
curl https://dl.min.io/client/mc/release/linux-amd64/mc --create-dirs -o $HOME/minio-binaries/mc
chmod +x $HOME/minio-binaries/mc
export PATH=$PATH:$HOME/minio-binaries/Add that export to ~/.bashrc if you want mc available in every shell. Then add an alias for your server:
mc alias set lokal http://127.0.0.1:9000 MINIO_ROOT_USER MINIO_ROOT_PASSWORDReplace the last two values with the root credentials from .env. Expected output:
Added 'lokal' successfully.Then create the bucket and the dedicated user, and inspect the server:
mc mb lokal/backup-aplikasi
mc admin user add lokal backup-writer LONG-PASSWORD-FOR-BACKUP-WRITER
mc admin info lokalThe mc admin info output confirms the server is reachable, and the bucket now exists alongside its dedicated user.
Step 4: Test upload, download, and delete with mc
Never trust a backup target until a round trip works. Create a small test file, upload it, list it, download it back, compare the contents, then delete the test copy:
echo "MinIO backup test" > /tmp/minio-test.txt
mc cp /tmp/minio-test.txt lokal/backup-aplikasi/
mc ls lokal/backup-aplikasi/
mc cp lokal/backup-aplikasi/minio-test.txt /tmp/minio-test-downloaded.txt
cat /tmp/minio-test-downloaded.txtExpected output of the last command: MinIO backup test. If it matches, the round trip works. Clean up the test files:
mc rm lokal/backup-aplikasi/minio-test.txt
rm /tmp/minio-test.txt /tmp/minio-test-downloaded.txtStep 5: Test with the AWS CLI as a second S3 client
One client can hide a quirk. Proving a second, independent S3 client works against the same bucket is what makes MinIO's compatibility claim real. With the AWS CLI installed, run:
aws --endpoint-url http://127.0.0.1:9000 s3 ls s3://backup-aplikasiThen test a write:
echo "test via aws cli" > /tmp/aws-test.txt
aws --endpoint-url http://127.0.0.1:9000 s3 cp /tmp/aws-test.txt s3://backup-aplikasi/
aws --endpoint-url http://127.0.0.1:9000 s3 ls s3://backup-aplikasi/
rm /tmp/aws-test.txt
aws --endpoint-url http://127.0.0.1:9000 s3 rm s3://backup-aplikasi/aws-test.txtSet AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to the backup user's key pair, and keep the HTTP/HTTPS scheme consistent between the endpoint URL and your reverse proxy setup later.
Step 6: Send a real backup to MinIO
The pattern for a real backup is simple: archive, send it to the bucket, verify it is there, then delete the local archive if you want. A database plus app files example:
mysqldump -u DB_USER -p DB_NAME > /tmp/db-backup.sql
tar -czf /tmp/app-backup.tar.gz /tmp/db-backup.sql /var/www/your-app
mc cp /tmp/app-backup.tar.gz lokal/backup-aplikasi/$(date +%F)/
mc ls lokal/backup-aplikasi/$(date +%F)/
rm /tmp/db-backup.sql /tmp/app-backup.tar.gzSave that pattern as a script — for example /opt/minio/backup-to-minio.sh — make it executable with chmod +x, and schedule it with cron using the full path to mc, for example $HOME/minio-binaries/mc, so the scheduled job does not depend on your interactive shell's PATH.
Test a restore, not just the upload:
mc cp lokal/backup-aplikasi/2026-10-26/app-backup.tar.gz /tmp/restore-test/
tar -tzf /tmp/restore-test/app-backup.tar.gz | headIf the archive lists its files, the restore path works. A backup you have never restored is only a hope.
Security warning: do not expose the console to the public as-is
This is the rule to take seriously. Keep ports 9000 and 9001 bound to 127.0.0.1 until a reverse proxy is in place, and reach the console through an SSH tunnel in the meantime. When you are ready for a domain, use Nginx with HTTPS from Let's Encrypt, with separate subdomains for 9001 and 9000, and add access restrictions where you can.
Do not use a default or short root password. Give each application its own access key, and revoke keys you no longer use through the console or with mc admin user remove. Keep the backup bucket private. And remember that one node on one VPS is not the complete answer: mirror to a second MinIO server with mc mirror, or keep an archive copy off the VPS, so a single server failure does not take your backups with it.
Summary table
| Component | Details |
|---|---|
| Docker image | minio/minio:latest |
| S3 API Port | 9000 |
| Web console Port | 9001 |
| Data directory | /data in the container, volume minio_data |
| Config files | /opt/minio/docker-compose.yml and /opt/minio/.env |
| Root credentials | MINIO_ROOT_USER and MINIO_ROOT_PASSWORD in .env |
| Example bucket | backup-aplikasi |
| Basic test | mc cp, mc ls, download and compare, then mc rm |
| Second S3 test | aws --endpoint-url http://127.0.0.1:9000 s3 ls and s3 cp |
| Safe access | Bind 127.0.0.1, SSH tunnel, then reverse proxy + HTTPS |
Final checklist
- The MinIO container is
Upand/minio/health/livereturns200. - The
backup-aplikasibucket exists. - A dedicated backup user exists, so scripts do not use root.
- Upload, download, and delete of a test file through
mcworked. - The AWS CLI can read and write to the same bucket.
- One real backup was sent and successfully test-restored from a temporary folder.
- The console is not exposed directly to the internet without a reverse proxy and HTTPS.
.envis permission600and credentials are in a password manager.
Which workload will you point at MinIO first — database backups, user uploads, or log archives? Share your choice and your docker system df — no, your MinIO test result — in the comments.
Baca juga:
- Docker Compose untuk Self-Hosting dari Nol — for the Compose foundation used here.
- Docker Volume dan Backup: Jangan Sampai Data Hilang — so the
minio_datavolume survives container deletion. - Backup VPS Otomatis dan Terenkripsi ke Cloud — for a second copy off the VPS.
- Satu VPS Banyak Layanan dengan Nginx Reverse Proxy — when you are ready to expose MinIO through a domain with HTTPS.


Komentar
Posting Komentar