Object storage is the backbone of modern data workflows: backups, media assets, ML training datasets, and application state all rely on scalable, S3-compatible storage. For teams that want full control over their data without locking into a cloud provider's proprietary storage API, MinIO is a leading open-source option. This guide walks through a production-grade MinIO deployment on Ubuntu using Docker Compose, NGINX reverse proxy, Let's Encrypt for automatic SSL, and UFW for firewall hardening.
Why MinIO for Production Workloads
MinIO is designed for high-performance object storage with full S3 compatibility, which means existing tools, SDKs, and backup scripts that work with AWS S3 will work seamlessly with your self-hosted MinIO cluster. Unlike cloud storage, you control retention, access policies, and data locality — critical for regulated industries or cost-sensitive teams ingesting terabytes of log or media data.
In production, a bare MinIO container is not enough. You need TLS termination, firewall rules, persistent storage configuration, and operational runbooks for upgrades and failures. This guide covers all of those, following the same production-first pattern as our other Guides posts.
Architecture Overview
The deployment includes these core components:
- MinIO Server: Runs in a Docker container, stores data to a persistent host volume, exposes the S3 API and web console.
- NGINX: Reverse proxy that terminates TLS (via Let's Encrypt) and routes traffic to MinIO's console and API ports.
- Let's Encrypt: Provides free, auto-renewing SSL certificates for your domain.
- UFW: Ubuntu's Uncomplicated Firewall, configured to only allow traffic on ports 80, 443, and SSH.
All services are defined in a single docker-compose.yml file for reproducibility and easy version control.
Prerequisites
- Ubuntu 22.04 LTS or 24.04 LTS server
- Domain name pointed to your server's public IP (e.g., minio.example.com)
- Docker and Docker Compose installed (we'll cover this below if you haven't already)
- Root or sudo access to the server
- Basic familiarity with terminal commands and YAML syntax
Step 1: Install Docker and Docker Compose
If you don't have Docker installed yet, run these commands to install the latest stable version:
# Update package index
sudo apt update && sudo apt upgrade -y
# Install dependencies
sudo apt install -y ca-certificates curl gnupg lsb-release
# Add Docker GPG key
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Add Docker repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine and Docker Compose plugin
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
Verify Docker is installed correctly:
sudo docker run hello-world
Step 2: Create MinIO Data Directory
Create a persistent directory on the host to store MinIO data, and set permissions so the MinIO container can write to it:
sudo mkdir -p /data/minio
sudo chown -R 1000:1000 /data/minio
sudo chmod -R 755 /data/minio
Step 3: Create docker-compose.yml
Create a directory for your MinIO deployment and add the docker-compose.yml file:
mkdir -p /opt/minio
nano /opt/minio/docker-compose.yml
Paste the following configuration into the file (replace minio.example.com with your actual domain):
version: '3.8'
services:
minio:
image: minio/minio:latest
container_name: minio
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
volumes:
- /data/minio:/data
ports:
- "9000:9000"
- "9001:9001"
networks:
- minio-net
nginx:
image: nginx:alpine
container_name: minio-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/certs:/etc/nginx/certs
- ./nginx/logs:/var/log/nginx
depends_on:
- minio
networks:
- minio-net
networks:
minio-net:
driver: bridge
Step 4: Configure MinIO Environment Variables
Create a .env file in the same directory to store your MinIO root credentials (never commit this to version control):
nano /opt/minio/.env
Add your secure credentials (use a password manager to generate a strong password):
MINIO_ROOT_USER=admin
MINIO_ROOT_PASSWORD=your-strong-password-here
Restrict permissions on the .env file:
chmod 600 /opt/minio/.env
Step 5: Configure NGINX Reverse Proxy
Create the NGINX configuration directory and add a site config for MinIO:
mkdir -p /opt/minio/nginx/conf.d
nano /opt/minio/nginx/conf.d/minio.conf
Paste the following configuration (replace minio.example.com with your domain):
server {
listen 80;
server_name minio.example.com;
# Redirect all HTTP traffic to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name minio.example.com;
# SSL configuration will be added by Certbot
location / {
proxy_pass http://minio:9000;
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;
}
location /console {
proxy_pass http://minio:9001;
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-Forwarded-Prefix /console;
}
}
Step 6: Set Up Let's Encrypt SSL Certificates
Install Certbot and the NGINX plugin to automatically generate and renew SSL certificates:
sudo apt install -y certbot python3-certbot-nginx
# Generate certificates (replace with your domain and email)
sudo certbot --nginx -d minio.example.com -m [email protected] --agree-tos --non-interactive
Certbot will automatically update your NGINX config to use the new SSL certificates and set up auto-renewal via a cron job.
Step 7: Configure UFW Firewall
Enable UFW and only allow traffic on ports 22 (SSH), 80 (HTTP), and 443 (HTTPS):
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Verify UFW rules:
sudo ufw status
Step 8: Start MinIO Services
Navigate to your MinIO directory and start the services with Docker Compose:
cd /opt/minio
sudo docker compose up -d
Check that all containers are running:
sudo docker compose ps
Step 9: Verify Your Deployment
Open your browser and navigate to https://minio.example.com/console (replace with your domain). Log in with the MINIO_ROOT_USER and MINIO_ROOT_PASSWORD you set in the .env file.
Test uploading a file via the web console, then verify it's accessible via the S3 API:
# Install MinIO client
sudo docker pull minio/mc
# Configure MinIO host
sudo docker run --rm -it minio/mc alias set myminio https://minio.example.com admin your-strong-password-here
# Create a test bucket
sudo docker run --rm minio/mc mb myminio/test-bucket
# Upload a test file
echo "Hello MinIO" > test.txt
sudo docker run --rm -v $(pwd):/data minio/mc cp /data/test.txt myminio/test-bucket
Configuration Best Practices
- Secrets Management: Never commit your .env file to version control. Use a secrets manager for production credentials.
- Persistent Storage: Ensure /data/minio is on a dedicated disk with enough capacity for your workload.
- Backups: Regularly back up MinIO data to an offsite location (e.g., another MinIO instance or cloud storage).
- Monitoring: Integrate MinIO with Prometheus (see our VictoriaMetrics guide for metrics setup).
Common Issues & Fixes
- MinIO container won't start: Check permissions on /data/minio. Run
sudo chown -R 1000:1000 /data/minioagain. - SSL certificate errors: Ensure your domain's DNS is correctly pointed to your server. Check Certbot logs at
/var/log/letsencrypt/letsencrypt.log. - NGINX proxy errors: Verify the MinIO container is running and accessible on the internal network. Run
sudo docker exec -it minio ping miniofrom the NGINX container. - Permission denied on .env: Ensure the .env file is owned by root and has 600 permissions.
FAQ
How do I scale MinIO for more storage?
MinIO supports distributed mode, where you can add more nodes to increase storage capacity and throughput. Update your docker-compose.yml to include multiple MinIO containers with different data volumes, and use the MINIO_SERVER_URL environment variable to configure the cluster.
Can I use MinIO with S3-compatible tools?
Yes, MinIO is fully S3-compatible. You can use AWS SDKs, s3cmd, MinIO Client (mc), and most backup tools that support S3 without any changes.
How do I set up bucket policies for public read access?
Use the MinIO web console or the mc client to set bucket policies. For example, to make a bucket publicly readable: mc anonymous set download myminio/public-bucket.
What's the difference between the S3 API port (9000) and console port (9001)?
Port 9000 is for programmatic S3 API access (used by SDKs and tools), while port 9001 is for the web-based management console where you can create buckets, upload files, and manage users.
How do I renew Let's Encrypt certificates?
Certbot sets up automatic renewal by default. You can test renewal with sudo certbot renew --dry-run.
Can I run MinIO behind a different reverse proxy like Traefik?
Yes, we cover Traefik-based deployments in our OpenObserve guide. The core MinIO configuration remains the same; only the proxy config changes.
How do I monitor MinIO health?
MinIO exposes Prometheus metrics at /minio/v2/metrics. You can scrape these with Prometheus and visualize them in Grafana (see our Grafana guide for setup steps).
Internal Resources
- Production Guide: Deploy VictoriaMetrics with Docker Compose + NGINX + systemd + UFW on Ubuntu
- Production Guide: Deploy OpenObserve with Docker Compose + Traefik + ClickHouse on Ubuntu
- Production Guide: Deploy Grafana + Prometheus with Docker Compose + Nginx on Ubuntu
Talk to us
Need help deploying MinIO or designing a production object storage strategy? Our team can help you build a reliable, scalable setup tailored to your workload, including distributed clusters, backup policies, and monitoring integrations.