SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor

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.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

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 any S3 SDK to your own server, change one endpoint setting, and the client no go know the difference. This guide go build one node for Ubuntu 24.04: binary wey compile from verified source, dedicated system user, systemd unit wey keep root credentials comot from the unit file, and bucket wey restic go back up data enter.

S3 (simple storage service) na HTTP API, e no be filesystem. You PUT object inside bucket under one key, then you GET am back. Partial write and rename no dey. Backup tools like this model because object either arrive complete or e no arrive.

One node dey hold one copy of your data. Na this tradeoff 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 tradeoff make sense.

MinIO community edition status for October 2026

Read this part before you build with am, because things don change recently. For May 2025, MinIO remove the administration features from the web console for the 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 distribute the community edition as source code only, and since April 2026, dem don archive the minio/minio repository with the notice THIS REPOSITORY IS NO LONGER MAINTAINED. The old download server don stop serving the community files too. When dem check am on 10 October 2026, every dl.min.io path wey this guide dey use before to download the server binary, client binary, and their checksum files dey answer 410 Gone. This one na deliberate removal, no be outage. The download page for min.io now offer only AIStor Server, AIStor Client, and AIStor SDK.

The source still dey, and e newer pass the last binary. The final community release na RELEASE.2025-10-15T17-29-55Z, wey dem publish 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 archive the repository, na also the last fix wey the community edition go get.

That one fact dey shape the rest of this guide. Na why MinIO for here dey listen on 127.0.0.1 and e dey reach internet only through proxy wey you control.

Install MinIO binary then verify the download

MinIO na software wey dem write for Go, and vendor README build am with one go install command wey need Go 1.24 or newer. Install Go from the official tarball first. For 10 October 2026, the current stable release for go.dev/dl na go1.27.2, and the hash below na the one wey dem publish for that page for linux-amd64 archive. If you choose newer Go, take 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 that kind thing make people unpack 404 page, then wonder why e no be archive.

cd /tmp
GOVER=go1.27.2
curl -fsSL "https://go.dev/dl/$GOVER.linux-amd64.tar.gz" -o go.tar.gz

Compare the two hashes, and compare only the hashes.

published=ecbadb99091a3f46e31f5f934b068b1864eafa7995211b39eaddf76996045fe5
downloaded=$(sha256sum go.tar.gz | awk '{print $1}')
[ "$published" = "$downloaded" ] && echo "checksum ok"

Make sure 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 separate 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 version

Now 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 anybody tamper with or swap a module, the build stop with security error instead of producing a 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 3

minio --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 the mode dey wrong, while command not found mean say /usr/local/bin no dey for your PATH. If the 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/minio

tee 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.target

No 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/live

is-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, wey be file manager and e no get connection with MinIO.

dl.min.io no dey serve the client again, and dem archive the minio/mc repository since July 2026. So build the 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/mc

Register the server as an 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.txt

mc ls suppose list hello.txt with the size, and mc cat suppose print hello object storage. That round trip na the real proof say the server dey work, because e 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 minioadmin

This command suppose fail. If e succeed, the environment file no reach the process, and your server dey run with the default credentials. Fix am before anything else touch the machine.

mc dey store aliases for ~/.mc/config.json as plain text, so those credentials dey 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.

Share one object with a presigned URL

A presigned URL na normal HTTPS link wey get signature and expiry time attached. Anybody wey get the link fit fetch that one object without account and without client.

mc share download --expire 12h local/backups/hello.txt

The 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 revoke button dey. The signature go remain valid until e expire, so short expiry na the only control wey you get. 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-backup

MinIO 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/backups

The 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 snapshots

restic 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 minio

The 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.

MinIO single-node setup y when you need real S3

Single node for here mean one drive, so parity no dey and no second copy dey inside the deployment. Durability of every object na durability of one VPS disk. MinIO own documentation, wey don dey only as source for the minio/docs repository since hosted community docs dem remove am 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 bucket, but for one VPS e only stop deletion through S3 API. Anybody wey get root for the machine still fit 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. E also good as S3 endpoint for development work and CI artifacts, where losing one bucket mean say you go rebuild, and nothing more. E fit also work for user uploads for small application, as long as 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 fill up. The frozen code na another honest reason. As of 10 October 2026, community edition don enter archive and dem no dey maintain am again. The last release, RELEASE.2025-10-15T17-29-55Z, na the one this guide compile. Nobody go fix anything after am. So if you run am, you accept that situation, or move to MinIO commercial AIStor line or another self-hosted store. self-hosted MinIO alternatives compare those options.

One boundary dey important because people dey ask about am often. Object storage no be database. Every write dey replace the whole object, so live SQL file for S3 bucket go slow and unsafe. Keep the database for local disk and back am up into the bucket instead. running SQLite in production on a VPS explain this split.

Wetin fit go wrong and messages wey you go see

The unit fails right 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 e get wrong path for the unit. Message wey end with permission denied mean say service account no get write permission for data directory, so check say stat -c '%U' /var/lib/minio/data dey print minio-user.

minioadmin:minioadmin still logs in. Environment file no reach the process. Confirm say unit get EnvironmentFile=/etc/default/minio, run sudo systemctl daemon-reload, then restart the service. MinIO dey read its root credentials once e start, so if you edit that file without restart, nothing go change.

Address already in use for startup. 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 no ever see the request. Set client_max_body_size 0 for the server block.

SignatureDoesNotMatch. Either the secret key dey wrong, or something between the client and MinIO rewrite the Host header, and signature dey cover that header.

RequestTimeTooSkewed. Clock for the client or server dey wrong. Every S3 request carry timestamp, and system dey reject am outside 15 minute window. Check timedatectl and confirm say time synchronisation dey active.

Access Denied for bucket wey you know say e dey exist. The key dey limited to another bucket. Print wetin the policy really allow with mc admin policy info local restic-rw, then 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. But e no good enough as your only copy. Single-drive deployment get zero parity, so no second copy dey inside MinIO. If that VPS disk lose data, the objects don disappear. Keep another target for another place, and restore from both at least once so you know say the process dey work.

Why sha256sum -c dey fail for MinIO checksum file?

You go only see that file inside copy wey dem save earlier, because dl.min.io answer 410 Gone for every community binary and checksum file when dem check am on 10 October 2026. The label after the hash inside that file na the release name, like minio.RELEASE.2025-09-07T16-13-09Z, while dem normally call the downloaded file minio. sha256sum -c dey look for file wey get the name written inside the 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 the label no get security meaning.

Where MinIO admin web console go?

MinIO remove the administration features from the community edition console for May 2025, so the web interface now get only object browser. Use the mc client to manage buckets and users, with commands like mc admin user add and mc admin policy attach. Na the intended method for community edition, no be workaround. Na why this guide dey do everything from command line.

How I fit point restic to MinIO as S3 backend?

Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to MinIO access key and the matching secret. 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 get permission for only one bucket no fit create buckets. restic dey encrypt everything with its own repository password before upload, so MinIO dey store ciphertext and e no ever see your files.

I must run MinIO behind nginx?

You need TLS (transport layer security) whenever client no dey for the same machine, because S3 credentials and object data both dey travel inside the request. Proxy for port 443 with certificate from certbot na the simplest way to do this, and e keeps 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 the service account need read access to the renewed private key, and na extra work for the same result.