Introduction

MinIO is a high-performance S3-compatible object storage system — ideal for backups, artifacts, data lakes, and any workload needing an S3 API. Ansible automates the full deployment: single-node or distributed cluster, TLS encryption, bucket policies, lifecycle rules, site replication, and Prometheus monitoring.

Single-Node Deployment

---
- name: Deploy MinIO single node
  hosts: minio_servers
  become: true
  vars:
    minio_root_user: minioadmin
    minio_root_password: "{{ vault_minio_root_password }}"
    minio_data_dir: /data/minio
    minio_console_port: 9001
    minio_api_port: 9000
  tasks:
    - name: Create MinIO user
      ansible.builtin.user:
        name: minio-user
        system: true
        shell: /usr/sbin/nologin

    - name: Create data directory
      ansible.builtin.file:
        path: "{{ minio_data_dir }}"
        state: directory
        owner: minio-user
        group: minio-user
        mode: '0750'

    - name: Download MinIO
      ansible.builtin.get_url:
        url: https://dl.min.io/server/minio/release/linux-amd64/minio
        dest: /usr/local/bin/minio
        mode: '0755'

    - name: Deploy MinIO environment file
      ansible.builtin.template:
        src: minio.env.j2
        dest: /etc/default/minio
        mode: '0600'
      notify: restart minio

    - name: Create systemd service
      ansible.builtin.copy:
        dest: /etc/systemd/system/minio.service
        content: |
          [Unit]
          Description=MinIO Object Storage
          After=network-online.target
          [Service]
          User=minio-user
          Group=minio-user
          EnvironmentFile=/etc/default/minio
          ExecStart=/usr/local/bin/minio server $MINIO_OPTS
          Restart=always
          RestartSec=5
          LimitNOFILE=65536
          [Install]
          WantedBy=multi-user.target
        mode: '0644'
      notify:
        - daemon reload
        - restart minio

    - name: Allow MinIO through firewall
      ansible.posix.firewalld:
        port: "{{ item }}/tcp"
        permanent: true
        state: enabled
        immediate: true
      loop:
        - "{{ minio_api_port }}"
        - "{{ minio_console_port }}"

    - name: Start MinIO
      ansible.builtin.service:
        name: minio
        state: started
        enabled: true

  handlers:
    - name: daemon reload
      ansible.builtin.systemd:
        daemon_reload: true
    - name: restart minio
      ansible.builtin.service:
        name: minio
        state: restarted

Environment Template

# templates/minio.env.j2
MINIO_ROOT_USER={{ minio_root_user }}
MINIO_ROOT_PASSWORD={{ minio_root_password }}
MINIO_OPTS="{{ minio_data_dir }} --console-address :{{ minio_console_port }} --address :{{ minio_api_port }}"
{% if minio_tls_cert is defined %}
MINIO_CERTS_DIR=/etc/minio/certs
{% endif %}
{% if minio_prometheus_auth_type is defined %}
MINIO_PROMETHEUS_AUTH_TYPE={{ minio_prometheus_auth_type }}
{% endif %}

Distributed Cluster

# For 4-node distributed MinIO cluster
# group_vars/minio_cluster/main.yml
minio_nodes:
  - minio1.example.com
  - minio2.example.com
  - minio3.example.com
  - minio4.example.com
minio_data_dirs:
  - /data/disk1
  - /data/disk2
# templates/minio-cluster.env.j2
MINIO_ROOT_USER={{ minio_root_user }}
MINIO_ROOT_PASSWORD={{ minio_root_password }}
MINIO_OPTS="{% for node in minio_nodes %}{% for disk in minio_data_dirs %}http://{{ node }}:{{ minio_api_port }}{{ disk }} {% endfor %}{% endfor %} --console-address :{{ minio_console_port }}"
MINIO_SERVER_URL=https://minio.example.com:{{ minio_api_port }}

Install MinIO Client (mc)

- name: Install MinIO client
  ansible.builtin.get_url:
    url: https://dl.min.io/client/mc/release/linux-amd64/mc
    dest: /usr/local/bin/mc
    mode: '0755'

- name: Configure mc alias
  ansible.builtin.command: >
    mc alias set myminio http://localhost:{{ minio_api_port }}
    {{ minio_root_user }} {{ minio_root_password }}
  changed_when: true
  no_log: true

Manage Buckets

- name: Create buckets
  ansible.builtin.command: "mc mb myminio/{{ item }}"
  loop:
    - backups
    - artifacts
    - logs
    - data-lake
  register: bucket_result
  changed_when: "'Bucket created' in bucket_result.stdout"
  failed_when: false

- name: Set bucket versioning
  ansible.builtin.command: "mc version enable myminio/{{ item }}"
  loop: [backups, data-lake]
  changed_when: true

- name: Set lifecycle policy (expire old versions after 30 days)
  ansible.builtin.copy:
    dest: /tmp/lifecycle-backups.json
    content: |
      {
        "Rules": [{
          "ID": "expire-old-versions",
          "Status": "Enabled",
          "NoncurrentVersionExpiration": {
            "NoncurrentDays": 30
          }
        }]
      }
    mode: '0644'

- name: Apply lifecycle policy
  ansible.builtin.command: mc ilm import myminio/backups < /tmp/lifecycle-backups.json
  changed_when: true

TLS Configuration

- name: Create certs directory
  ansible.builtin.file:
    path: /etc/minio/certs
    state: directory
    owner: minio-user
    mode: '0700'

- name: Copy TLS certificate
  ansible.builtin.copy:
    src: "{{ minio_tls_cert }}"
    dest: /etc/minio/certs/public.crt
    owner: minio-user
    mode: '0644'

- name: Copy TLS private key
  ansible.builtin.copy:
    src: "{{ minio_tls_key }}"
    dest: /etc/minio/certs/private.key
    owner: minio-user
    mode: '0600'
  notify: restart minio

Nginx Reverse Proxy

- name: Deploy MinIO Nginx proxy
  ansible.builtin.template:
    src: minio-nginx.conf.j2
    dest: /etc/nginx/conf.d/minio.conf
    mode: '0644'
  notify: reload nginx
# templates/minio-nginx.conf.j2
upstream minio_api {
{% for node in minio_nodes | default([inventory_hostname]) %}
    server {{ node }}:{{ minio_api_port }};
{% endfor %}
}

server {
    listen 443 ssl;
    server_name minio.example.com;

    ssl_certificate /etc/ssl/certs/minio.crt;
    ssl_certificate_key /etc/ssl/private/minio.key;

    client_max_body_size 0;
    proxy_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;
    }
}

Health Check

- name: Check MinIO health
  ansible.builtin.uri:
    url: "http://localhost:{{ minio_api_port }}/minio/health/live"
    status_code: 200

- name: Check cluster health
  ansible.builtin.uri:
    url: "http://localhost:{{ minio_api_port }}/minio/health/cluster"
    status_code: 200
  when: minio_nodes is defined

Troubleshooting

Disk Space

- name: Check disk usage on MinIO data dirs
  ansible.builtin.command: "df -h {{ minio_data_dir }}"
  register: disk_usage
  changed_when: false

Connection Issues

- name: Test MinIO API
  ansible.builtin.uri:
    url: "http://localhost:{{ minio_api_port }}/minio/health/live"
    timeout: 5
  register: health
  ignore_errors: true

Conclusion

MinIO gives you an S3-compatible API anywhere — on-premises, edge, or cloud. Ansible deploys single-node instances for dev and distributed clusters for production, manages TLS, creates buckets with versioning and lifecycle policies, and monitors health. Object storage as code means reproducible, version-controlled storage infrastructure.