Sooner or later almost every project needs a place to dump files that is not the local disk. User uploads, application backups, build artifacts, container image layers, machine learning datasets: all of these are a poor fit for a regular filesystem once you have more than one server. The industry answer to this problem is object storage, and the de facto standard API is Amazon S3.
But you do not have to send your data to AWS to get an S3 API. MinIO is an open-source, S3-compatible object storage server that you can run on your own hardware or VPS. It speaks the exact same protocol as AWS S3, which means any tool or SDK that already works with S3 will work with MinIO without code changes. In this tutorial you will install MinIO on Ubuntu, run it as a proper background service, create buckets and scoped access keys, and put it behind TLS so it is safe to use in production.
This guide is for developers, sysadmins, and DevOps engineers who want a private storage layer they fully control. You only need basic Linux command-line skills to follow along.
What Object Storage Actually Is
If you are used to files and folders, object storage works a little differently and it helps to understand why before you start.
A traditional filesystem stores files in a tree of directories. You navigate paths, you can append to a file, you can lock part of it. Object storage throws most of that away. Instead it stores objects inside flat containers called buckets. Each object has a key (which often looks like a path, such as photos/2026/cat.jpg), the actual data, and some metadata. You do not edit an object in place: you upload a whole new version to replace it. You talk to it over HTTP, not over a mounted disk.
This sounds limiting, but it is exactly what makes object storage scale. Because objects are immutable blobs accessed over HTTP, the system can spread them across many disks and machines, replicate them, and serve them to thousands of clients without the locking headaches of a shared filesystem. It is the right tool for storing large numbers of files that are written once and read many times.
A few terms you will see throughout this guide:
- Bucket: a top-level container for objects, similar to a top-level folder. Bucket names are globally unique within your server.
- Object: a single stored file plus its metadata.
- Access key and secret key: the username and password pair that an application uses to authenticate, just like AWS credentials.
- Policy: a JSON document that says which actions an identity is allowed to perform on which buckets.
MinIO implements all of these the same way AWS S3 does, which is why your existing S3 tooling will not know the difference.
Prerequisites
Before you start, make sure you have the following:
- An Ubuntu 22.04 or 24.04 server with
sudoaccess. - At least 2 GB of RAM. MinIO is light, but object storage benefits from headroom.
- A dedicated disk or directory with plenty of free space for your data.
- A domain name pointing at the server if you want HTTPS, for example
storage.example.com. - Basic familiarity with the Linux command line.
This tutorial uses a single server with a single data directory, which is perfect for development, internal tooling, and small production workloads. MinIO can also run in a distributed multi-node mode for high availability, which we will point to at the end.
Step 1: Create a Dedicated User and Data Directory
Running storage services as root is asking for trouble. Create a dedicated system user that MinIO will run as, with no login shell:
sudo useradd -r -s /sbin/nologin minio-user
The -r flag creates a system account and -s /sbin/nologin prevents anyone from logging in as it. Next, create the directory where MinIO will keep its data:
sudo mkdir -p /mnt/minio-data
sudo chown minio-user:minio-user /mnt/minio-data
In a real deployment you would point this at a dedicated disk or volume mounted at /mnt/minio-data. Keeping data on its own volume means you can grow storage independently and you will not fill up the root partition by accident.
Step 2: Install the MinIO Server Binary
MinIO ships as a single static binary, so installation is just a download. Grab the latest server binary from the official source:
wget https://dl.min.io/server/minio/release/linux-amd64/minio
Make it executable and move it into a directory on your PATH:
chmod +x minio
sudo mv minio /usr/local/bin/minio
Confirm it runs:
minio --version
You should see output similar to this:
minio version RELEASE.2026-05-xx...
Runtime: go1.23 linux/amd64
The exact version string will differ depending on when you download it, but as long as it prints a version, the binary is good.
Step 3: Configure MinIO with an Environment File
MinIO reads its core configuration from environment variables. The cleanest way to manage them is a single environment file that the service will load. Create it:
sudo nano /etc/default/minio
Add the following content:
# Directory where MinIO stores data
MINIO_VOLUMES="/mnt/minio-data"
# Address and port for the S3 API
MINIO_OPTS="--address :9000 --console-address :9001"
# Root credentials (CHANGE THESE)
MINIO_ROOT_USER="minioadmin"
MINIO_ROOT_PASSWORD="ChangeMe-SuperSecret-Passw0rd"
A few important notes here. MINIO_VOLUMES tells MinIO where to store objects. --address :9000 is the S3 API endpoint that applications connect to, while --console-address :9001 serves the web admin UI on a separate port.
The root user and password are the master credentials. The defaults minioadmin/minioadmin are infamous and are scanned for constantly on the public internet, so set a long random password here. The password must be at least 8 characters; MinIO will refuse to start with a weak or missing one.
Lock the file down so only root can read the credentials:
sudo chmod 640 /etc/default/minio
sudo chown root:minio-user /etc/default/minio
Step 4: Run MinIO as a systemd Service
You want MinIO to start on boot and restart if it crashes, so we will manage it with systemd rather than launching it by hand. Create a service unit:
sudo nano /etc/systemd/system/minio.service
Paste in the following:
[Unit]
Description=MinIO Object Storage
Documentation=https://min.io/docs
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
The important pieces: EnvironmentFile pulls in the settings you just created, Restart=always keeps the service alive, and LimitNOFILE=65536 raises the open file limit because object storage juggles a lot of file handles. If you want a deeper look at writing and managing units like this, see Manage Background Services and Timers with systemd on Ubuntu.
Reload systemd, enable the service so it starts on boot, and start it now:
sudo systemctl daemon-reload
sudo systemctl enable minio
sudo systemctl start minio
Check that it is running:
sudo systemctl status minio
You should see active (running). If you see a failure, jump to the troubleshooting section before continuing.
Step 5: Open the Firewall and Visit the Console
If you run UFW, allow the two MinIO ports:
sudo ufw allow 9000/tcp
sudo ufw allow 9001/tcp
Now open the web console in your browser at http://your-server-ip:9001. Log in with the root user and password from your environment file. You will land on a dashboard where you can browse buckets, watch metrics, and manage users.
The console is great for poking around, but for anything repeatable you will want the command line client, which we set up next.
Step 6: Install and Configure the mc Client
MinIO ships a powerful command line client called mc (short for “MinIO Client”). It behaves a bit like ls, cp, and rm but works against object storage. Download it:
wget https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc
sudo mv mc /usr/local/bin/mc
Now register your server as an alias called local so you do not have to type the endpoint every time:
mc alias set local http://127.0.0.1:9000 minioadmin "ChangeMe-SuperSecret-Passw0rd"
Use the same password you set in the environment file. Verify the connection:
mc admin info local
This prints the server status, drive health, and how much space is used. If it reports the server is online, you are connected.
Step 7: Create a Bucket and Upload an Object
Create your first bucket:
mc mb local/backups
mb stands for “make bucket”. List your buckets to confirm:
mc ls local
Now upload a file. Let’s create a test file and put it in the bucket:
echo "hello object storage" > test.txt
mc cp test.txt local/backups/
List the contents of the bucket:
mc ls local/backups
You should see test.txt listed with its size and timestamp. Congratulations, you are running your own S3. You can copy entire directories with mc cp --recursive, mirror folders with mc mirror, and remove objects with mc rm.
Step 8: Create a Scoped Access Key for Applications
You should never hand your root credentials to an application. Instead, create a dedicated user with a policy that grants access to only the buckets it needs. This is the principle of least privilege: if those credentials leak, the blast radius is one bucket, not your entire server.
First, write a policy file that allows full access to just the backups bucket:
nano backups-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:*"],
"Resource": [
"arn:aws:s3:::backups",
"arn:aws:s3:::backups/*"
]
}
]
}
The two resource entries matter: the first grants actions on the bucket itself (like listing it), and the second grants actions on the objects inside it. Register the policy with MinIO:
mc admin policy create local backups-only backups-policy.json
Create a user for your application and attach the policy:
mc admin user add local backup-app "app-secret-key-change-me"
mc admin policy attach local backups-only --user backup-app
Now backup-app can read and write the backups bucket and nothing else. Test it by adding a second alias that authenticates as this user:
mc alias set backupapp http://127.0.0.1:9000 backup-app "app-secret-key-change-me"
mc ls backupapp/backups
It works for backups but will be denied if it tries to touch any other bucket.
Step 9: Put MinIO Behind HTTPS
So far MinIO is talking plain HTTP, which means credentials and data travel unencrypted. For anything beyond local testing you must add TLS. The easiest path is a reverse proxy that handles certificates for you, such as Caddy, which I covered in Install Caddy Web Server with Automatic HTTPS on Ubuntu.
Assuming Caddy is installed and your domain storage.example.com points at the server, edit the /etc/caddy/Caddyfile:
storage.example.com {
reverse_proxy 127.0.0.1:9000
}
console.example.com {
reverse_proxy 127.0.0.1:9001
}
Reload Caddy:
sudo systemctl reload caddy
Caddy will fetch a Let’s Encrypt certificate automatically and proxy HTTPS traffic to MinIO. There is one extra detail for the console: because it now sits behind a different public URL, tell MinIO where the browser should be redirected by adding this line to /etc/default/minio and restarting the service:
MINIO_BROWSER_REDIRECT_URL="https://console.example.com"
Once TLS is in place, update your mc alias to use the HTTPS endpoint and close the raw ports 9000 and 9001 on the firewall so the only way in is through the proxy:
mc alias set local https://storage.example.com minioadmin "ChangeMe-SuperSecret-Passw0rd"
sudo ufw delete allow 9000/tcp
sudo ufw delete allow 9001/tcp
Common Mistakes and Troubleshooting
The service fails to start with a permission error. Run sudo journalctl -u minio -e to read the logs. The usual culprit is that /mnt/minio-data is not owned by minio-user. Fix it with sudo chown -R minio-user:minio-user /mnt/minio-data.
MinIO refuses to start and complains about the password. The root password must be at least 8 characters. If MINIO_ROOT_PASSWORD is missing or too short, MinIO exits immediately. Check /etc/default/minio.
mc returns “Access Denied” with the correct keys. Double-check that you copied the secret key exactly, including no trailing spaces. For scoped users, confirm the policy is attached with mc admin policy entities local --user backup-app.
The console loads but shows broken styling or login loops behind the proxy. This is the redirect URL problem. Make sure MINIO_BROWSER_REDIRECT_URL matches the exact public HTTPS address of the console and that you restarted MinIO after setting it.
You changed the root password but mc still works with the old one. The mc alias caches credentials locally in ~/.mc/config.json. Re-run mc alias set to update it.
Best Practices
A few habits will keep your storage server healthy and safe in production:
- Never use the default credentials. Set a long, random root password and rotate it if you suspect exposure. Treat the root user like a database superuser: use it for administration, not for applications.
- One access key per application. Scoped users with narrow policies mean a leaked key is a contained incident, not a catastrophe.
- Always run behind TLS. Object storage carries credentials in every request. Plain HTTP exposes them to anyone on the network path.
- Enable bucket versioning for important data. Run
mc version enable local/backupsso an overwrite or accidental delete can be recovered. - Monitor disk usage. Object storage fills up quietly. Watch free space on the data volume and alert before it hits 100 percent, because a full disk will make MinIO read-only.
- Use it as a backup target. MinIO is an excellent destination for encrypted backups. You can point Restic straight at it with the S3 backend; see Automated Encrypted Backups with Restic on Ubuntu for the full workflow.
Conclusion
You now have a working, S3-compatible object storage server running on your own Ubuntu box. Along the way you created a dedicated service user, installed the MinIO binary, managed it cleanly with systemd, created buckets and uploaded objects with the mc client, locked down access with scoped users and policies, and wrapped the whole thing in HTTPS for production use.
From here there are two natural directions to grow. The first is reliability: MinIO supports a distributed mode across multiple nodes and drives that uses erasure coding to survive disk and machine failures, which is the way to run it for serious production storage. The second is integration: point your applications, container registries, and backup tools at your new endpoint using their standard S3 settings. Because MinIO speaks the S3 API faithfully, almost anything that works with AWS will work with your server, except now the data lives on infrastructure you control.