How to run MinIO for S3 storage on your VPS
Run MinIO on Ubuntu 24.04 with a verified source build, systemd, mc, presigned URLs, and restic backups, plus the October 2026 Community Edition catch.
Wetin self-hosted object storage with MinIO dey give you
MinIO na self-hosted object storage wey dey speak Amazon S3 API. Point restic or an S3 SDK to your own server, change one endpoint setting, and for normal object reads, writes, listings and presigned links, na everything be that. But compatible no mean say dem identical. For example, MinIO no implement Amazon object ACLs, and the maintainers don talk say dem no get plan to implement am. So, application wey depend on ACL behaviour need more than new endpoint. This guide go build one node for Ubuntu 24.04: binary wey dem compile from verified source, dedicated system user, systemd unit wey keep root credentials out of the unit file, and bucket wey restic go back up data into.
S3 (simple storage service) na HTTP API, no be filesystem. You PUT object inside bucket under a key and GET am back. For standard bucket, partial write and rename no dey. Amazon S3 Express One Zone directory buckets na exception, because dem get their own append and rename calls. But na one storage class be that, and nothing for this guide depend on am. Backup tools like the standard model because object either arrive complete or e no arrive.
One node dey hold one copy of your data. Na the trade-off wey you dey make. You get S3 endpoint wey you control for the price of VPS. You also inherit every work wey cloud provider dey do before, from replacing failed disk to patching server software. The section near the end go explain plainly when this trade-off make sense.
MinIO community edition wey dey for October 2026
Read this part before you build on am, because e change recently. For May 2025, MinIO remove the administration features from the web console for community edition. Wetin remain for browser na object browser, so you go manage buckets and access keys with the mc command line client instead.
Later for 2025, MinIO stop publishing pre-compiled community binaries. The project README talk say dem dey distribute community edition as source code only, and since April 2026, dem don archive the minio/minio repository under the notice THIS REPOSITORY IS NO LONGER MAINTAINED. The old download server don stop serving the community files too. When dem check am on 20 October 2026, every dl.min.io path wey this guide use before to download from — the server binary, the client binary and their checksum files — dey answer 410 Gone. This one na deliberate removal, no be outage. The only mc binary wey still dey that server na AIStor Client under /aistor/mc/release/, and e belong to the commercial line. The download page for min.io now list AIStor Server, AIStor Client, AIStor SDK and some AIStor add-ons, plus one free AIStor tier wey e describe as meant for developers, researchers and small organisations wey dey run standalone deployments. No community build dey there.
The source code still dey, and e newer than the last binary. The final community release na RELEASE.2025-10-15T17-29-55Z, published on 16 October 2025 as security release. E fix privilege escalation through session policy bypass for service accounts and STS, and the release notes advise all users to upgrade immediately. The last pre-compiled binary na the September 2025 build, so e no ever receive that fix. This guide compile the October release, and because dem don archive the repository, na also the last fix community edition go get.
That one fact shape the rest of this guide. Na why MinIO for here dey listen on 127.0.0.1 and e only reach internet through proxy wey you control.
Install MinIO binary then verify the download
MinIO na Go-based software, and vendor README dey build am with one go install command wey need Go 1.24 or newer. Install Go from the official tarball first. On 20 October 2026, the current stable release for go.dev/dl na go1.27.3. Dem release am on 19 October 2026 with security fix for crypto/x509 package, and the hash below na the one wey dem publish for that page for linux-amd64 archive. If you choose newer Go, collect the hash from that same page. The -f flag make curl fail when HTTP error happen, instead of saving the error page with the name wey you request. Na so people dey unpack 404 page, then wonder why e no be archive.
cd /tmp
GOVER=go1.27.3
curl -fsSL "https://go.dev/dl/$GOVER.linux-amd64.tar.gz" -o go.tar.gzCompare the two hashes, and compare only the hashes.
published=65efbc80fdc1fa2cd2631cad5b481777a68e8725f34e89f50fc6ac714e379b40
downloaded=$(sha256sum go.tar.gz | awk '{print $1}')
[ "$published" = "$downloaded" ] && echo "checksum ok"Make you understand wetin this check prove. If the hashes match, e prove say the download complete and nobody damage or change am while e dey transit. E no prove say you fit trust the vendor. That one na different problem, and no sha256sum command fit solve am.
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go.tar.gz
export PATH=$PATH:/usr/local/go/bin
go versionNow build the final community release as your normal user. go install check every module wey e download, including MinIO source, against Go public checksum database for sum.golang.org. So if person tamper with or swap any module, the build go stop with security error instead of producing binary.
go install github.com/minio/minio@RELEASE.2025-10-15T17-29-55Z
sudo install -o root -g root -m 755 ~/go/bin/minio /usr/local/bin/minio
minio --version
go version -m /usr/local/bin/minio | head -n 3minio --version print DEVELOPMENT.GOGET where release name dey before, because go install no apply the build flags wey vendor use to stamp its own binaries. go version -m read the module information wey Go embed inside every binary, and its mod github.com/minio/minio line show the commit wey you build. If Permission denied show here, e mean say the mode wrong, while command not found mean say /usr/local/bin no dey for your PATH. If VPS no get enough memory for the compile, run the same go install for another Linux machine with the same architecture, then copy the binary go there.
Create system user and data directory
MinIO dey accept uploads from network, so e no suppose run as root. Give am account wey no get home directory and no login shell.
sudo groupadd -r minio-user
sudo useradd -M -r -g minio-user -s /usr/sbin/nologin minio-user
sudo mkdir -p /var/lib/minio/data
sudo chown -R minio-user:minio-user /var/lib/minio
sudo chmod 750 /var/lib/minio-r dey create system account with UID below 1000, so e no go enter the range wey people dey use. -M dey skip home directory, because account wey no dey ever log in no get anything to keep there. Check the result with id minio-user, and with stat -c '%U %a' /var/lib/minio, wey suppose print minio-user 750.
That user must get write permission for the data directory, no be only read permission. When MinIO start for the first time, e dey create .minio.sys directory inside the volume to keep its own configuration. So, if root own the directory, MinIO go exit during startup with message wey end for permission denied. This same rule apply to every service wey you run this way, and service users with least privilege for VPS explain am well.
Put the root credentials for environment file
Root credentials fit open every bucket, so dem no belong inside the unit file, because everybody fit read am. Create the file with the correct mode first, then write inside am. This way, the password no dey inside readable file even for one moment.
sudo install -o root -g root -m 600 /dev/null /etc/default/minio
printf 'MINIO_ROOT_USER=minio-root\nMINIO_ROOT_PASSWORD=%s\nMINIO_VOLUMES="/var/lib/minio/data"\nMINIO_OPTS="--address 127.0.0.1:9000 --console-address 127.0.0.1:9001"\n' "$(openssl rand -base64 24)" | sudo tee /etc/default/minio > /dev/null
sudo sed -n 's/^MINIO_ROOT_PASSWORD=//p' /etc/default/miniotee dey truncate existing file instead of creating am again, so mode remain 600 and owner remain root. Na deliberate arrangement be this. systemd dey read EnvironmentFile as root before e drop privilege go User=, so service account no ever need read its own credentials. After service don dey run, prove am with sudo -u minio-user cat /etc/default/minio. That command must print Permission denied.
You need know two MinIO behaviours before you start am. If MINIO_ROOT_USER and MINIO_ROOT_PASSWORD no dey for its environment, MinIO no go refuse to start. E go start with the documented default credentials minioadmin:minioadmin. Na the first pair wey any scanner dey try, and the service go look completely healthy while e dey use dem. But MinIO go reject password wey get less than 8 characters. E go exit during startup with error say credentials no valid, because access key need at least 3 characters and secret key need at least 8.
MINIO_VOLUMES na the data path, while MINIO_OPTS dey hold the flags. When you bind to 127.0.0.1, nothing outside this VPS fit reach the S3 API yet. Na the correct default be this. Later, you go open am deliberately through proxy wey dey hold certificate.
Write the systemd unit
Create /etc/systemd/system/minio.service:
[Unit]
Description=MinIO object storage
Documentation=https://github.com/minio/minio
Wants=network-online.target
After=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
NoNewPrivileges=true
[Install]
WantedBy=multi-user.targetNo get leading - for EnvironmentFile, and na deliberate decision, no be typo. If dash dey there, systemd go ignore missing file and start MinIO anyway. So if person delete file or misspell path, e fit silently hand you server wey dey run with minioadmin:minioadmin. If dash no dey, missing file go make unit fail before MinIO ever run, and journalctl -u minio go show Failed to load environment files: No such file or directory. Unit wey refuse to start dey easier to notice than server wey quietly accept default password.
$MINIO_VOLUMES and $MINIO_OPTS no get quotes on purpose, because systemd dey split unquoted variables on whitespace into separate arguments. Na so the four words for MINIO_OPTS take become four arguments for minio server. LimitNOFILE=65536 dey increase file descriptor limit, because every open connection and every open data file dey use one descriptor, and default value of 1024 go finish when load high.
sudo systemctl daemon-reload
sudo systemctl enable --now minio
systemctl is-active minio
curl -fsS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:9000/minio/health/liveis-active suppose print active, and health endpoint suppose answer 200. journalctl -u minio -n 20 --no-pager dey show the API address wey server dey listen on. If unit dey restart again and again, systemd go give up and log Start request repeated too quickly. This mean say MinIO dey exit every time e try start. The reason dey for the lines above that message, so read upward.
For more isolation, add ProtectSystem=full and ProtectHome=true to the [Service] section. Both need mount namespaces from the host kernel. For container virtualisation wey dey share host kernel, like OpenVZ or LXC, dem fit fail, and unit go then report status=226/NAMESPACE. Remove those two lines and e go start. The unit itself na ordinary one, and systemd services and timers for VPS cover the remaining directives.
Install mc and prove a round trip
The MinIO client na mc. No install am with apt install mc. That package na Midnight Commander, a file manager wey no get anything to do with MinIO.
dl.min.io still dey serve one mc binary, but e no be the community one. The old community path, https://dl.min.io/client/mc/release/linux-amd64/mc, dey answer 410 Gone. Wetin MinIO dey offer there now na AIStor Client for https://dl.min.io/aistor/mc/release/linux-amd64/mc. Its AIStor CLI reference point to am and describe am as client wey get exclusive functionality for licensed AIStor Server deployments. That reference no talk anything about using am against community server, so this guide go use the community client. Dem don archive minio/mc repository since July 2026. So build its last release, RELEASE.2025-08-13T08-35-41Z, with the same Go toolchain. The checksum database cover this download too.
go install github.com/minio/mc@RELEASE.2025-08-13T08-35-41Z
sudo install -o root -g root -m 755 ~/go/bin/mc /usr/local/bin/mcRegister the server as alias, then move one object through am.
MINIO_PASS=$(sudo sed -n 's/^MINIO_ROOT_PASSWORD=//p' /etc/default/minio)
mc alias set local http://127.0.0.1:9000 minio-root "$MINIO_PASS"
mc mb local/backups
echo "hello object storage" > /tmp/hello.txt
mc cp /tmp/hello.txt local/backups/hello.txt
mc ls local/backups
mc cat local/backups/hello.txtmc ls suppose list hello.txt with its size, and mc cat suppose print hello object storage. That round trip na the real proof say the server dey work, because e dey make the same signed S3 requests wey every other client go make. mc admin info local go print the server status if you want another confirmation.
Run one more check now, while the box still empty.
mc alias set defaultcheck http://127.0.0.1:9000 minioadmin minioadminThis command must fail. If e succeed, the environment file no reach the process, and your server dey run with the default credentials. Fix that before anything else touch the machine.
mc dey store aliases for ~/.mc/config.json as plain text, so those credentials dey sit inside the home directory of whoever run the command. If you run mc under sudo, e go put the root credentials for /root/.mc/config.json. Keep the root alias for one administrator account, and give every application its own key.
Hand out one object with a presigned URL
A presigned URL na normal HTTPS link wey get signature and expiry attached. Anybody wey hold the link fit fetch that one object without account and without client.
mc share download --expire 12h local/backups/hello.txtThe output get X-Amz-Signature and X-Amz-Expires for the query string. Two things about am dey surprise people. The link dey build from the endpoint for the alias wey you use, so alias for 127.0.0.1 go produce link wey na only this machine fit open: make another alias for your public hostname for links wey you plan send. And no per-link revoke button dey. The signature go remain valid until e expire or until dem remove or change the key wey sign am. For link wey root credentials sign, that mean you need rotate the credentials, so short expiry na the practical control. Seven days na the maximum wey the S3 signature format allow.
Give restic im own key and bucket
The root credentials fit read and delete every bucket, so backup job no suppose hold dem. Create one bucket, one policy wey dey limited to that bucket, and one user wey no get any other access.
mc mb local/restic
cat > /tmp/restic-rw.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:ListBucket", "s3:GetBucketLocation"],
"Resource": ["arn:aws:s3:::restic"]
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": ["arn:aws:s3:::restic/*"]
}
]
}
EOF
RESTIC_KEY=$(openssl rand -base64 24)
mc admin policy create local restic-rw /tmp/restic-rw.json
mc admin user add local restic-backup "$RESTIC_KEY"
mc admin policy attach local restic-rw --user restic-backupMinIO get built-in readwrite policy wey for make this one command shorter, but e dey give full access to every bucket for the server. The policy above mention the bucket two times on purpose: once as arn:aws:s3:::restic so listing the bucket go work, and once as arn:aws:s3:::restic/* for the objects inside am. For S3, bucket and the objects inside am na separate resources, so policy wey mention only one of dem go fail in a way wey look like say client spoil.
Test the limit before you trust am.
mc alias set resticuser http://127.0.0.1:9000 restic-backup "$RESTIC_KEY"
mc ls resticuser/restic
mc ls resticuser/backupsThe first ls go succeed and the second one go fail with Access Denied. Policy wey you never test na just guess.
Now point restic to the bucket. restic dey read S3 credentials from the standard AWS environment variables, so no restic-specific credential file dey involved.
sudo apt install -y restic
export AWS_ACCESS_KEY_ID=restic-backup
export AWS_SECRET_ACCESS_KEY="$RESTIC_KEY"
restic -r s3:http://127.0.0.1:9000/restic init
restic -r s3:http://127.0.0.1:9000/restic backup /etc
restic -r s3:http://127.0.0.1:9000/restic snapshotsrestic init dey ask for repository password. That password dey encrypt the repository, so MinIO go only store ciphertext, and if you lose the password, you lose the backup. A run wey systemd timer start no get terminal to type inside, so set RESTIC_PASSWORD_FILE to a mode 600 file for scheduled backups.
One placement rule important pass every command above. If restic repository dey for the same VPS as the data wey e dey protect, e go save you from bad rm and nothing else. The MinIO node suppose dey for another machine, ideally for another region. restic backups for VPS dey explain scheduling and retention on top of this.
Terminate TLS with nginx
MinIO dey for localhost, so na nginx be the public surface. Issue the certificate first, as e dey described for Let's Encrypt certificates with certbot and nginx, then use this server block.
server {
listen 443 ssl;
server_name s3.example.com;
ignore_invalid_headers off;
client_max_body_size 0;
proxy_buffering off;
proxy_request_buffering off;
location / {
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_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding off;
proxy_pass http://127.0.0.1:9000;
}
}Some of those lines dey carry important work. client_max_body_size 0 removes the default 1 MB body limit. Without am, nginx go reject any upload wey pass that size with 413 Request Entity Too Large before MinIO even see the request. proxy_request_buffering off streams the upload direct through, because the default behaviour dey save the whole request for temporary file first. Large object then go need disk space twice. proxy_set_header Host $http_host na the subtle one: S3 signature dey cover the Host header. So, if proxy rewrite am, every request go fail with SignatureDoesNotMatch, even though access log dey show say normal request arrive.
Tell MinIO the public name too, so the links wey e generate go point to the proxy instead of localhost.
echo 'MINIO_SERVER_URL=https://s3.example.com' | sudo tee -a /etc/default/minio
sudo systemctl restart minioThe firewall fit remain small. Allow SSH and HTTPS. Leave ports 9000 and 9001 without any rule, because address wey bind to 127.0.0.1 no fit reach from another machine, no matter wetin firewall talk. ufw firewall basics on a VPS get the commands.
When single-node MinIO dey enough, and when you want real S3
Single node for here mean one drive. So parity no dey, and no second copy dey inside the deployment. The durability of every object na the durability of one VPS disk. MinIO own documentation, wey don dey only as source for the minio/docs repository since dem take down the hosted community docs for October 2025, describe standalone deployment as something for early development and evaluation. E also talk say MinIO no recommend am for production. You fit switch on object locking for a bucket, but for one VPS e only stop deletion through the S3 API. Anybody wey get root for the machine fit still remove the files for disk. So no promise anybody immutable retention policy for this setup.
E good as restic target for second VPS wey dey another region, and as S3 endpoint for development work and CI artifacts. For this kind use, if bucket loss happen, na rebuild you go need, nothing more. E also fit work for user uploads for small application, as long as na you own the recovery plan and you don actually test restore.
Choose managed S3 when contract or regulator ask for object lock or multi-region durability. Choose am too when you no wan be the person wey dem page at 03:00 because disk don full. The frozen code na another honest reason. As of 20 October 2026, the community edition don enter archive and dem no dey maintain am again. Its last release, RELEASE.2025-10-15T17-29-55Z, na the one wey this guide compile. Dem no go fix anything after am. So if you run am, you accept that condition. Or you fit move to MinIO commercial AIStor line or another self-hosted store. self-hosted MinIO alternatives compare dem.
One boundary dey important because people dey ask about am often. Object storage no be database. For standard bucket, every write dey replace the whole object. Even the append call wey Amazon add for S3 Express One Zone only add data for the end. So live SQL file for S3 bucket slow and unsafe. Keep the database for local disk, then back am up into the bucket. running SQLite in production on a VPS explain this split.
Failure modes and the messages wey you go see
The unit fail immediately after systemctl enable --now. Read journalctl -u minio -n 30 --no-pager. Failed to load environment files: No such file or directory mean say /etc/default/minio no dey there, or dem spell its path wrong inside the unit. Message wey end with permission denied mean say service account no get write permission for the data directory, so check say stat -c '%U' /var/lib/minio/data dey print minio-user.
minioadmin:minioadmin still dey log in. The environment file never reach the process. Confirm say the unit get EnvironmentFile=/etc/default/minio, run sudo systemctl daemon-reload, then restart the service. MinIO dey read its root credentials once when e start, so if you edit that file without restart, nothing go change.
Address already in use when e dey start. Another process dey hold port 9000. Find am with sudo ss -ltnp | grep :9000 before you change MinIO port.
Uploads wey pass 1 MB fail through the proxy. nginx answer with 413 Request Entity Too Large and MinIO never see the request. Set client_max_body_size 0 inside the server block.
SignatureDoesNotMatch. Either the secret key no correct, or something between the client and MinIO change the Host header, and the signature cover that header.
RequestTimeTooSkewed. The clock for the client or server no correct. Every signed S3 request carry timestamp, and MinIO, like Amazon S3, dey reject header-signed request when its timestamp pass 15 minutes away from its own clock. Dem check presigned links against their expiry instead. Check timedatectl and confirm say time synchronisation dey active.
Access Denied for bucket wey you know say e dey exist. The key get scope for another bucket. Print wetin the policy really allow with mc admin policy info local restic-rw and compare the bucket name for the resource lines.
FAQ
Single-node MinIO good enough for real backups?
E good enough as restic target wey dey run for another machine from the data wey e dey protect. E no good enough as your only copy. Single-drive deployment get zero parity, so no second copy dey inside MinIO, and if VPS disk lose data, the objects don go. Keep second target for another place, and restore from both at least once so you know say the process dey work.
Why sha256sum -c dey fail on MinIO checksum file?
Na only for copy wey you save earlier you go meet that file, because dl.min.io answer 410 Gone for every community binary and checksum file when dem check am on 20 October 2026. The label after the hash inside that file na the release name, like minio.RELEASE.2025-09-07T16-13-09Z, while the downloaded file normally get name minio. sha256sum -c dey look for file wey get the name written inside checksum file, e no find am, then e report No such file or directory and WARNING: 1 listed file could not be read. The download dey okay. Compare the hash strings directly and ignore the label, because e no get security meaning.
Where MinIO admin web console go?
MinIO remove the administration features from community edition console for May 2025, leaving object browser for the web interface. Now, dem dey manage buckets and users with mc client, using commands like mc admin user add and mc admin policy attach. Na this be the intended method for community edition, no be workaround. Na why this guide dey do everything from command line.
How I go point restic to MinIO as S3 backend?
Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to MinIO access key and the secret wey belong to am, then use repository string for the form s3:https://s3.example.com/restic, where the last path element na the bucket name. Create the bucket first with mc mb, because key wey scope to one bucket no get permission to create buckets. restic encrypt everything with its own repository password before upload, so MinIO dey store ciphertext and e never see your files.
I must run MinIO behind nginx?
You need TLS (transport layer security) whenever client no dey the same machine, because without am every object go cross the network as plain text, together with access key ID and request signature. The secret key itself never leave the client, because S3 signing only use am locally to compute that signature, but anybody for the network path still fit read your data and replay captured request while its timestamp still valid. Proxy for port 443 with certificate from certbot na the simplest way to get TLS, and e keep certificate renewal away from MinIO. MinIO fit also terminate TLS by itself if you point --certs-dir to directory wey hold public.crt and private.key, but then service account need read access to the renewed private key. Na extra work for the same result.