SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-07

How to Run MinIO for S3 on One Ubuntu VPS

Set up MinIO on one Ubuntu 24.04 VPS with verified binary, systemd, mc, presigned URLs, and restic backups, while keeping root credentials out the unit file.

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 go your own server, change one endpoint setting, and the client no go know the difference. This guide build one single node for Ubuntu 24.04: verified binary, dedicated system user, systemd unit wey dey keep root credentials comot from 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 one key, then you GET am back. No partial write and no rename dey happen. Backup tools like this 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, and 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 clearly when this trade-off make sense.

MinIO community edition status for July 2026

Read this part before you build on am, because e 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 now talk say dem dey distribute the community edition as source code only. The older download URLs still work: as of July 2026, dem dey serve server build RELEASE.2025-09-07T16-13-09Z and client build RELEASE.2025-08-13T08-35-41Z, and no newer community build don show. So the binary below na real binary and e dey run, but dem don freeze am. Security fixes wey dem publish after September 2025 no dey inside am.

This one fact shape the rest of this guide. Na why MinIO here dey listen on 127.0.0.1 and e only reach internet through proxy wey you control. If you prefer follow security fixes, build from source. The vendor README give one command, go install github.com/minio/minio@latest, wey need Go toolchain and write the binary to ~/go/bin/minio. Install that binary to /usr/local/bin/minio and every other step here remain the same.

Install MinIO binary and verify the download

Download the pinned release and the checksum wey dem publish. 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 install 404 page, then wonder why e no go execute.

cd /tmp
REL=RELEASE.2025-09-07T16-13-09Z
curl -fsSL "https://dl.min.io/server/minio/release/linux-amd64/archive/minio.$REL" -o minio
curl -fsSL "https://dl.min.io/server/minio/release/linux-amd64/archive/minio.$REL.sha256sum" -o minio.sha256sum

Compare the two hashes, and compare only the hashes.

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

No use sha256sum -c minio.sha256sum here. The label wey dem write after the hash inside that file na minio.RELEASE.2025-09-07T16-13-09Z, and we save the download as minio, so -c dey look for file wey no exist. E report No such file or directory and then WARNING: 1 listed file could not be read. This one fit look like corrupted download, but e no be corrupted download. The label na only name. The hash na the part wey provide the guarantee.

Make you understand wetin this check prove. The binary and the hash come from the same vendor through the same connection, so if dem match, e prove say the download complete and no damage or alteration happen during transfer. E no prove say the vendor trustworthy. That one na different problem, and no sha256sum command fit solve am.

sudo install -o root -g root -m 755 minio /usr/local/bin/minio
minio --version

minio --version print minio version RELEASE.2025-09-07T16-13-09Z followed by some build lines. A Permission denied here mean say the mode wrong, and command not found mean say /usr/local/bin no dey for your PATH.

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

MinIO client na mc. No install am with apt install mc. That package na Midnight Commander, wey be file manager and e no get anything to do with MinIO.

cd /tmp
curl -fsSL https://dl.min.io/client/mc/release/linux-amd64/mc -o mc
curl -fsSL https://dl.min.io/client/mc/release/linux-amd64/mc.sha256sum -o mc.sha256sum
[ "$(awk '{print $1}' mc.sha256sum)" = "$(sha256sum mc | awk '{print $1}')" ] && echo "checksum ok"
sudo install -o root -g root -m 755 mc /usr/local/bin/mc

Register 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.txt

mc ls suppose list hello.txt together with the size, and mc cat suppose print hello object storage. This 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 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 this before anything else touch the machine.

mc dey store aliases for ~/.mc/config.json as plain text, so whoever run the command go get those credentials for their home directory. 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.

When single-node MinIO dey enough, and when you need real S3

Single node for here mean one drive with zero parity. MinIO own documentation describe this layout as suitable for testing and small workloads wey no get availability requirement. No second copy dey inside the deployment, so durability of every object na the durability of one VPS disk. Features wey assume distributed erasure-coded backend, including bucket replication and object locking, belong to multi-drive deployments. 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, where losing one bucket mean say you go rebuild am and nothing more. E still reasonable 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, or when you no wan be the person wey dem page at 03:00 because disk don full. The frozen build na the other honest reason. As of July 2026, the pre-compiled community binary come from September 2025 and no dey receive fixes. So if you run am, you accept that limitation, or you build from source and keep up with the project by yourself.

One boundary dey worth stating because e dey come up often. Object storage no be database. Every write replaces the whole object, so live SQL file for S3 bucket slow and unsafe. Keep the database for local disk and back am up into the bucket instead: running SQLite for production on VPS describe that 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 dey good enough for real backups?

E good enough as restic target wey dey run for another machine, separate 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. If that VPS disk lose data, the objects don disappear. Keep another target for another location, and restore from both at least once so you know say the process dey work.

Why sha256sum -c dey fail for MinIO checksum file?

Na because the label after the hash inside that file dey name the release, minio.RELEASE.2025-09-07T16-13-09Z, while your downloaded file normally dey called 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 any security meaning.

Where MinIO admin web console go?

MinIO remove the administration features from the community edition console for May 2025, and leave object browser for the web interface. Now you dey manage buckets and users with the mc client, using commands like mc admin user add and mc admin policy attach. Na the supported 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 dey 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) anytime 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 dey hold public.crt and private.key. But then the service account need read access to the renewed private key, and that one na extra work for the same result.

#minio#s3#object-storage#self-hosted#vps