Object storage is the foundation of modern data pipelines. Whether you are serving ML model artifacts, hosting video archives, or building a private S3-compatible backend for internal applications, you need a store that is fast, predictable, and easy to operate. MinIO delivers exactly that: a high-performance object storage server with full Amazon S3 API compatibility, erasure coding, and identity management, packaged as a single static binary.
This guide deploys MinIO on Ubuntu as a native systemd service behind NGINX. We avoid containers intentionally. A systemd-managed binary gives you transparent process supervision, native log forwarding through journald, and straightforward upgrades without image pulls or layer caching concerns. NGINX terminates TLS and enforces sane request limits, while MinIO listens only on localhost. The result is a production-ready object storage endpoint you can hand to a platform team or integrate into CI pipelines with confidence.
Architecture and flow overview
The design is minimal but robust. Public clients and applications connect to https://storage.example.com. NGINX handles TLS negotiation, keeps connection counts bounded, and proxies signed requests to MinIO running on 127.0.0.1:9000. MinIO stores objects on local filesystem under /opt/minio/data. A separate console port on 127.0.0.1:9001 provides the web admin interface, also proxied by NGINX at a restricted path or subdomain depending on your security posture.
systemd manages the MinIO process lifecycle: start after network is online, restart on failure, and apply resource limits through cgroup settings. Logs stream to the journal, so you can debug with standard journalctl commands instead of learning container-specific log drivers. UFW allows only 22, 80, and 443. Because MinIO binds to loopback, an accidental firewall misconfiguration does not expose the storage API directly to the internet.
- Edge: NGINX with TLS 1.3, rate limiting, and request buffering
- App: MinIO server bound to localhost with erasure coding enabled for single-node resilience
- State: Flat files under
/opt/minio/datawith structured backups - Ops: systemd unit with restart policy and environment-file secrets
- Safety: Dedicated service account, strict directory permissions, and off-server backup sync
Prerequisites
- Ubuntu 22.04 or 24.04 LTS server with root or sudo access
- A DNS record such as
storage.example.compointing to the server IP - At least 2 vCPU, 4 GB RAM, and sufficient disk for object data plus 20 percent headroom
- Certbot or another source for a valid TLS certificate
- A backup destination reachable via rsync, sftp, or S3 replication to a second MinIO cluster
- Replace all example domains, IPs, and credentials with your own values before executing commands
Step-by-step deployment
1) Install dependencies and create the minio user
Running MinIO under its own user limits filesystem exposure and simplifies permission auditing. Create the user, directories, and install the baseline packages first.
sudo apt update
sudo apt install -y nginx curl wget ufw certbot python3-certbot-nginx
sudo useradd --system --create-home --home-dir /opt/minio --shell /usr/sbin/nologin minio
sudo install -d -o minio -g minio /opt/minio/{bin,data,backups}
2) Download and verify MinIO
MinIO publishes stable binaries. Download the server binary to /opt/minio/bin, verify its checksum if your workflow requires it, and set executable permissions.
wget https://dl.min.io/server/minio/release/linux-amd64/minio -O /opt/minio/bin/minio
chmod +x /opt/minio/bin/minio
chown -R minio:minio /opt/minio
/opt/minio/bin/minio --version
3) Create the environment file for secrets
MinIO requires root credentials on first start. Store them in an environment file with restricted read permissions. Never commit this file to version control.
sudo tee /opt/minio/minio.env > /dev/null << 'EOF'
MINIO_VOLUMES="/opt/minio/data"
MINIO_OPTS="--console-address :9001 --address 127.0.0.1:9000"
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=REPLACE_WITH_32CHAR_RANDOM_STRING
EOF
sudo chmod 640 /opt/minio/minio.env
sudo chown root:minio /opt/minio/minio.env
Generate a strong password with openssl rand -base64 24 and replace the placeholder. The file is readable by root and the minio group, but not by ordinary users.
4) Write the systemd service unit
The unit enforces startup order, restart behavior, and hardening options. It loads secrets from the environment file and runs the binary as the minio user.
sudo tee /etc/systemd/system/minio.service > /dev/null << 'EOF'
[Unit]
Description=MinIO Object Storage Server
Documentation=https://min.io/docs/minio/linux/index.html
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=minio
Group=minio
EnvironmentFile=/opt/minio/minio.env
ExecStart=/opt/minio/bin/minio server $MINIO_VOLUMES $MINIO_OPTS
Restart=on-failure
RestartSec=5
StartLimitInterval=60s
StartLimitBurst=3
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/minio/data
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
5) Configure NGINX as the TLS reverse proxy
NGINX handles public traffic and forwards to MinIO. Create a server block for the API and another location for the console, or serve both from the same domain with path-based routing. The example below uses the same domain with a /console prefix for the web UI.
sudo tee /etc/nginx/sites-available/minio > /dev/null << 'EOF'
upstream minio_api {
server 127.0.0.1:9000;
keepalive 32;
}
upstream minio_console {
server 127.0.0.1:9001;
keepalive 32;
}
server {
listen 80;
listen [::]:80;
server_name storage.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name storage.example.com;
ssl_certificate /etc/letsencrypt/live/storage.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/storage.example.com/privkey.pem;
client_max_body_size 0;
proxy_buffering off;
proxy_request_buffering off;
location / {
proxy_pass http://minio_api;
proxy_set_header Host $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;
}
location /console/ {
proxy_pass http://minio_console/;
proxy_set_header Host $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_set_header X-Scheme $scheme;
proxy_connect_timeout 300;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
EOF
sudo ln -s /etc/nginx/sites-available/minio /etc/nginx/sites-enabled/minio
sudo nginx -t
Obtain a certificate with Certbot before reloading NGINX. If DNS is not yet pointed, use DNS validation or temporarily use a self-signed cert for initial testing.
6) Open firewall ports and start services
Allow SSH, HTTP, and HTTPS. Everything else drops by default.
sudo ufw default deny incoming
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo systemctl enable --now minio
sudo systemctl reload nginx
7) Create a bucket and access policy
Log in to the console at https://storage.example.com/console using the root credentials from the environment file. Create a bucket, then create a dedicated access key for applications. Do not distribute root credentials to clients.
# Example: create a bucket with the MinIO client (mc)
mc alias set local https://storage.example.com minioadmin REPLACE_WITH_PASSWORD
mc mb local/backups
mc anonymous set download local/backups
Marking a bucket as download enables public read access. Remove that line for private data. Always scope access keys to the minimum required actions and prefix paths.
Configuration and secrets handling
Secrets live in /opt/minio/minio.env with mode 640. Rotate credentials by updating the file, running systemctl restart minio, and updating downstream clients during a maintenance window. For higher assurance, consider a secrets manager that writes the environment file through an agent, or use systemd credentials when running on newer distributions that support LoadCredential=.
Backups should include both the object data and the IAM configuration. MinIO stores identity data inside its backend, so a filesystem snapshot of /opt/minio/data is sufficient for full recovery. Test restores quarterly. If your availability requirements demand zero downtime during hardware failures, expand to a distributed MinIO deployment across four or more nodes with erasure coding set to tolerate at least one drive or node loss.
Verification
systemctl status minioshows active and no recent restartsjournalctl -u minio --since todayshows clean startup lines and no permission errorscurl -I https://storage.example.com/minio/health/livereturns HTTP 200- NGINX error log shows no upstream connect failures
- Console login succeeds with the root user and the configured password
- Application access key can list, put, and get objects through the S3 API
- Backup script completes and produces a non-empty tar archive of
/opt/minio/data - TLS certificate is valid and covers the exact domain in use
Common issues and fixes
MinIO fails to start with a permission denied error
The minio user must own /opt/minio/data and the service unit must list that path in ReadWritePaths. Run chown -R minio:minio /opt/minio/data and reload systemd.
Large uploads fail halfway through
NGINX defaults can buffer or timeout large streams. Set client_max_body_size 0, disable request buffering, and increase proxy_connect_timeout and proxy_send_timeout to 300 seconds or more.
Console shows a blank page after login
The console relies on websocket upgrades and correct forwarded headers. Verify that NGINX passes Upgrade and Connection headers for the console location and that the X-Scheme header matches the actual client scheme.
Certificate renewal breaks the proxy
Certbot may alter the NGINX server block during renewal. Use certbot --nginx in a way that preserves your upstream definitions, or switch to DNS validation and reload NGINX manually after certificate replacement.
FAQ
Is a single-node MinIO deployment production ready?
Yes, for many teams. A single node with regular backups, monitoring, and tested restore procedures is production ready if your availability target allows brief maintenance windows. If you need continuous availability during node failures, move to a distributed erasure-coded cluster.
Can I use MinIO instead of Amazon S3 for existing applications?
Absolutely. MinIO implements the S3 API. Most SDKs and tools work with only endpoint and credentials changed. Verify multipart upload, versioning, and lifecycle behavior if your application relies on advanced features.
How do I upgrade MinIO safely?
Stop the service, back up the data directory, replace the binary with the new release, and start the service. Run the verification checklist before declaring the upgrade complete. For critical environments, rehearse on a cloned instance first.
What monitoring should I configure?
Collect disk usage, API latency, error rate, and backup age. MinIO exposes a Prometheus metrics endpoint at /minio/v2/metrics/cluster. Scrape that endpoint and alert on growth trends so you can expand storage before exhaustion.
Can I run MinIO and NGINX on separate machines?
Yes. In that case, bind MinIO to a private network interface and point NGINX to that IP. Restrict firewall rules so only the proxy tier can reach the MinIO API and console ports.
How do I migrate data from another S3 provider into MinIO?
Use the MinIO client mc mirror or a tool like rclone. Mirror buckets incrementally, validate checksums on a sample of objects, and switch application endpoints only after a final delta sync completes successfully.
Internal links
- Production Guide: Deploy PocketBase with systemd + NGINX + SQLite + UFW on Ubuntu
- How to Deploy Grafana in Production with Docker Compose + systemd
- Production Guide: Deploy SearXNG with Docker Compose + Caddy + Redis on Ubuntu
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.
Header image: Original SysBrix generated header, no watermark.