Skip to Content

Production Guide: Deploy MinIO with Docker Compose + Nginx on Ubuntu

Build a secure S3-compatible object storage stack with TLS, hardened access, validation, and operations runbooks.

Teams often start by storing backups, build artifacts, and media files on a single VM filesystem. It works at first, then breaks under growth: uploads spike, retention policies get messy, and restoring data during an outage becomes painful. This guide shows how to deploy MinIO on Ubuntu using Docker Compose + Nginx with TLS and a production-minded operating model. You will stand up a private S3-compatible object storage endpoint, secure credentials correctly, validate integrity, and prepare the stack for ongoing operations.

Architecture and flow overview

This deployment uses a single Ubuntu host with Docker Engine and Docker Compose plugin. MinIO runs in a container and stores object data on a dedicated host path. Nginx terminates TLS and proxies traffic to the MinIO API and console. The host firewall only exposes HTTP/HTTPS and SSH. Backups and lifecycle policies are configured as part of operational hardening.

  • Client apps/CI: Upload and read objects through HTTPS endpoint.
  • Nginx: Reverse proxy, TLS termination, request buffering policy.
  • MinIO: S3-compatible API + admin console.
  • Host storage: Persistent object data and config backups.

Prerequisites

  • Ubuntu 22.04/24.04 server with public DNS (example: storage.example.com).
  • Docker Engine + Docker Compose plugin installed.
  • A non-root sudo user.
  • Open ports 22, 80, and 443.
  • At least 2 vCPU, 4 GB RAM, and fast disk sized for object growth.

Step-by-step deployment

1) Prepare host and directory layout

Create a dedicated project directory, data path, and least-privilege permissions. Keep MinIO secrets out of shell history by writing them directly to an env file.

sudo mkdir -p /opt/minio/{nginx,certbot,backups}
sudo mkdir -p /srv/minio/data
sudo chown -R $USER:$USER /opt/minio
sudo chown -R 1000:1000 /srv/minio/data
cd /opt/minio

If the copy button does not work in your browser, manually select the command block and copy it.

2) Create the environment file for secrets

Use long random values for root user and password. Do not commit this file to Git. Restrict read permissions.

cat > /opt/minio/.env <<'EOF'
MINIO_ROOT_USER=minio_admin
MINIO_ROOT_PASSWORD=replace-with-32plus-char-random-secret
MINIO_DOMAIN=storage.example.com
MINIO_BROWSER_REDIRECT_URL=https://storage.example.com/console
EOF
chmod 600 /opt/minio/.env

If the copy button does not work in your browser, manually select the command block and copy it.

3) Write Docker Compose stack

This Compose file runs MinIO API on :9000 and console on :9001 internally. Nginx will expose both over 443 with path routing.

services:
  minio:
    image: minio/minio:latest
    container_name: minio
    restart: unless-stopped
    env_file:
      - /opt/minio/.env
    command: server /data --console-address ":9001"
    volumes:
      - /srv/minio/data:/data
    expose:
      - "9000"
      - "9001"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 5s
      retries: 5

  nginx:
    image: nginx:1.27-alpine
    container_name: minio-nginx
    restart: unless-stopped
    depends_on:
      - minio
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /opt/minio/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - /opt/minio/nginx/conf.d:/etc/nginx/conf.d:ro
      - /opt/minio/certbot/www:/var/www/certbot:ro
      - /opt/minio/certbot/conf:/etc/letsencrypt:ro

If the copy button does not work in your browser, manually select the command block and copy it.

4) Configure Nginx for MinIO API and console

We route API traffic from root path and send console traffic via /console/. This keeps one hostname and simplifies firewall rules.

user nginx;
worker_processes auto;

events { worker_connections 1024; }

http {
  include /etc/nginx/mime.types;
  default_type application/octet-stream;
  sendfile on;
  keepalive_timeout 65;
  include /etc/nginx/conf.d/*.conf;
}

If the copy button does not work in your browser, manually select the command block and copy it.

server {
  listen 80;
  server_name storage.example.com;

  location /.well-known/acme-challenge/ {
    root /var/www/certbot;
  }

  location / {
    return 301 https://$host$request_uri;
  }
}

server {
  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 5G;
  proxy_read_timeout 600s;
  proxy_send_timeout 600s;

  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 https;
  }

  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 https;
  }
}

If the copy button does not work in your browser, manually select the command block and copy it.

5) Issue TLS certificate and launch services

Use Certbot in a short-lived container. Then bring up MinIO + Nginx and verify service health.

mkdir -p /opt/minio/nginx/conf.d
# Save nginx.conf and minio.conf first

docker compose -f /opt/minio/docker-compose.yml up -d minio nginx

docker run --rm \
  -v /opt/minio/certbot/conf:/etc/letsencrypt \
  -v /opt/minio/certbot/www:/var/www/certbot \
  certbot/certbot certonly --webroot \
  -w /var/www/certbot \
  -d storage.example.com \
  --email [email protected] --agree-tos --no-eff-email

docker compose -f /opt/minio/docker-compose.yml restart nginx

docker compose -f /opt/minio/docker-compose.yml ps

If the copy button does not work in your browser, manually select the command block and copy it.

6) Create bucket, service account, and baseline policy

Use MinIO Client (mc) for repeatable administration. Prefer service accounts per application instead of sharing root credentials.

docker run --rm --network host \
  -v /opt/minio:/work minio/mc:latest sh -c '
mc alias set local https://storage.example.com minio_admin "replace-with-32plus-char-random-secret" --api s3v4
mc mb -p local/app-backups
mc anonymous set none local/app-backups
mc admin user svcacct add --access-key app-backups-ak --secret-key replace-with-another-strong-secret local minio_admin
'

If the copy button does not work in your browser, manually select the command block and copy it.

7) Enable lifecycle management and object lock strategy

For compliance-minded workloads, combine retention and versioning. Lifecycle rules keep storage costs predictable and reduce stale object buildup.

cat > /opt/minio/lifecycle.json <<'EOF'
{
  "Rules": [
    {
      "ID": "expire-temp-after-30d",
      "Status": "Enabled",
      "Filter": {"Prefix": "tmp/"},
      "Expiration": {"Days": 30}
    }
  ]
}
EOF

docker run --rm --network host -v /opt/minio:/work minio/mc:latest sh -c '
mc alias set local https://storage.example.com minio_admin "replace-with-32plus-char-random-secret" --api s3v4
mc ilm import local/app-backups < /work/lifecycle.json
'

If the copy button does not work in your browser, manually select the command block and copy it.

Configuration and secret-handling best practices

  • Never embed secrets in Compose YAML. Use env files with strict permissions and rotate values on schedule.
  • Separate root/admin from app credentials. Use one service account per workload and scope policies to exact bucket/path.
  • Protect backups. Store MinIO config exports and bucket policy snapshots in an off-host location.
  • Patch cadence. Track MinIO and Nginx releases monthly; perform staged rollouts in non-production first.
  • Audit and alerts. Forward container logs to your SIEM and alert on repeated auth failures.

Verification checklist

  • HTTPS endpoint returns valid certificate and modern TLS ciphers.
  • Console loads at https://storage.example.com/console/.
  • Health check responds from /minio/health/live.
  • Bucket create/list/upload/download works with service account.
  • Lifecycle rule appears in bucket policy output.
curl -I https://storage.example.com/minio/health/live

AWS_ACCESS_KEY_ID=app-backups-ak \
AWS_SECRET_ACCESS_KEY=replace-with-another-strong-secret \
aws --endpoint-url https://storage.example.com s3 ls s3://app-backups

If the copy button does not work in your browser, manually select the command block and copy it.

Common issues and fixes

Console redirect loop

Cause: wrong MINIO_BROWSER_REDIRECT_URL or missing trailing /console. Fix the env var and restart MinIO.

413 Request Entity Too Large

Cause: Nginx upload limit too low. Increase client_max_body_size and reload Nginx.

Slow multipart uploads

Cause: disk I/O saturation or small VM class. Move data path to faster volume and monitor iowait.

TLS renewal failures

Cause: ACME challenge path not reachable on port 80. Validate DNS, firewall, and the /.well-known/acme-challenge/ mapping.

Alternative deployment options

If your traffic profile or compliance requirements grow beyond a single node, consider MinIO distributed mode across multiple disks/nodes, or run MinIO behind Kubernetes ingress with persistent volumes and external secret management. For simple internal workloads, Caddy can replace Nginx with more automated TLS management. Keep the same principles: explicit identity boundaries, encrypted transport, and tested restore workflows.

FAQ

Can I run MinIO in single-node mode for production?

Yes, for small to mid-sized workloads with clear RPO/RTO expectations. Pair it with tested backups and fast restore drills. For high availability, move to distributed mode.

How often should I rotate service account keys?

At least every 60–90 days, or immediately after personnel changes, incident suspicion, or CI secret exposure. Use dual-key cutover to avoid downtime.

Do I need both API and console exposed publicly?

No. Many teams keep console restricted by VPN/IP allowlist and expose only API to trusted applications. That reduces attack surface significantly.

What is the safest way to back up MinIO data?

Use versioned bucket replication or scheduled object sync to another isolated target, plus config export snapshots. Validate restore by actually recovering sample datasets.

How do I monitor MinIO health in production?

Track container health, disk latency, free space, failed auth attempts, request latency, and 5xx rates. Alert on trend changes, not only hard failures.

Can I use this endpoint with standard AWS SDKs?

Yes. MinIO is S3-compatible. Set the custom endpoint URL and test signed requests early, especially for multipart uploads and region-related defaults.

Related guides on SysBrix

Talk to us

Need help deploying a production-ready object storage platform, designing secure S3-compatible access patterns, or creating tested backup and disaster-recovery runbooks? We can help with architecture, hardening, migration, and operational readiness.

Contact Us

Production Guide: Deploy OpenProject with Docker Compose + Caddy + PostgreSQL on Ubuntu
A production-ready OpenProject deployment with HTTPS, hardened config, backup automation, verification, and practical troubleshooting.