Obsah článku / Contents
MinIO v Dockeri: S3-compatible storage za 5 minút

MinIO v Dockeri: S3-compatible storage za 5 minút

"Potrebujem S3 storage. Lokálne. Teraz. Bez AWS účtu." 🚀

MinIO je odpoveď.

Prolog: The Problem

javascript
// Týždeň 1: Development
developer = "Potrebujem S3 pre file uploads!"
aws_guy = "OK, vytvorím ti bucket..."
// 3 dni čakania na approval
// 2 dni na credentials
// 1 deň na debugging IAM permissions

// Týždeň 2: Testing
tester = "Nemôžem testovať, nemám S3!"
developer = "Tu máš production credentials..."
tester = "😱 Production?!"

// Týždeň 3: Local Development
junior = "Ako mám testovať upload lokálne?"
senior = "Mock it..."
junior = "Ale production používa S3!"
senior = "Then use MinIO!"

// 5 minút neskôr:
docker run -d minio/minio server /data
// Local S3 ready! ✅

MinIO benefits:

javascript
benefits = {
    "S3 compatible": "✅ Same API as AWS",
    "Local development": "✅ No AWS account needed",
    "Fast setup": "✅ 5 minút",
    "Zero cost": "✅ Free",
    "Production ready": "✅ High performance",
    "Docker friendly": "✅ One command"
}

verdict = "Perfect for dev & prod! 🎯"

Poďme na to!


Kapitola 1: Quick Start (5 minút)

Krok 1: Docker Run (2 minúty)

bash
# Basic MinIO container
docker run -d \
  --name minio \
  -p 9000:9000 \
  -p 9001:9001 \
  -e "MINIO_ROOT_USER=admin" \
  -e "MINIO_ROOT_PASSWORD=password123" \
  minio/minio server /data --console-address ":9001"

# Check logs
docker logs minio

# Output:
# API: http://172.17.0.2:9000
# Console: http://172.17.0.2:9001
# 
# Documentation: https://min.io/docs/minio/linux/index.html
# Status:         1 Online, 0 Offline.

Ports:

  • 9000 - S3 API (for your app)
  • 9001 - Web Console (for humans)

Open Console:

http://localhost:9001
Login: admin / password123

Total time: 2 minúty!

Krok 2: Create Bucket (1 minúta)

Via Web Console:

1. Open http://localhost:9001
2. Login: admin / password123
3. Click "Buckets" → "Create Bucket"
4. Name: "my-files"
5. Click "Create"

Done! ✅

Via CLI (mc - MinIO Client):

bash
# Install mc (MinIO Client)
# Linux/Mac:
wget https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc
sudo mv mc /usr/local/bin/

# Windows:
# Download from https://dl.min.io/client/mc/release/windows-amd64/mc.exe

# Configure alias
mc alias set local http://localhost:9000 admin password123

# Create bucket
mc mb local/my-files

# Output: Bucket created successfully `local/my-files`.

# List buckets
mc ls local

Krok 3: Upload First File (2 minúty)

bash
# Upload file
echo "Hello MinIO!" > test.txt
mc cp test.txt local/my-files/

# List files
mc ls local/my-files/

# Download file
mc cp local/my-files/test.txt downloaded.txt

# Delete file
mc rm local/my-files/test.txt

Total time: 5 minút!


Kapitola 2: Docker Compose Setup

Production-Ready Configuration

yaml
# docker-compose.yml

version: '3.8'

services:
  minio:
    image: minio/minio:latest
    container_name: minio
    restart: unless-stopped
    
    ports:
      - "9000:9000"   # API
      - "9001:9001"   # Console
    
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER:-admin}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-password123}
      
      # Optional: default buckets
      MINIO_DEFAULT_BUCKETS: "uploads,images,documents"
    
    volumes:
      # Persist data
      - minio_data:/data
      
      # Optional: custom config
      - ./config:/root/.minio
    
    command: server /data --console-address ":9001"
    
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 10s
      retries: 3
    
    networks:
      - app_network

volumes:
  minio_data:
    driver: local

networks:
  app_network:
    driver: bridge

Environment file (.env):

bash
# .env

MINIO_ROOT_USER=admin
MINIO_ROOT_PASSWORD=your-secure-password-here
MINIO_DEFAULT_BUCKETS=uploads,images,documents

Run:

bash
# Start
docker-compose up -d

# Check logs
docker-compose logs -f minio

# Stop
docker-compose down

# Stop and remove volumes
docker-compose down -v

Kapitola 3: Application Integration

Java / Spring Boot

Dependencies (pom.xml):

xml
<dependencies>
    <!-- MinIO SDK -->
    <dependency>
        <groupId>io.minio</groupId>
        <artifactId>minio</artifactId>
        <version>8.5.7</version>
    </dependency>
    
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

Configuration:

java
// MinioConfig.java

package com.example.config;

import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MinioConfig {
    
    @Value("${minio.url:http://localhost:9000}")
    private String minioUrl;
    
    @Value("${minio.access-key:admin}")
    private String accessKey;
    
    @Value("${minio.secret-key:password123}")
    private String secretKey;
    
    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(minioUrl)
                .credentials(accessKey, secretKey)
                .build();
    }
}

Service:

java
// MinioService.java

package com.example.service;

import io.minio.*;
import io.minio.messages.Item;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
public class MinioService {
    
    private final MinioClient minioClient;
    private static final String BUCKET_NAME = "uploads";
    
    /**
     * Upload file to MinIO
     */
    public String uploadFile(MultipartFile file) {
        try {
            // Ensure bucket exists
            ensureBucketExists();
            
            // Generate unique filename
            String filename = System.currentTimeMillis() + "_" + file.getOriginalFilename();
            
            // Upload file
            minioClient.putObject(
                PutObjectArgs.builder()
                    .bucket(BUCKET_NAME)
                    .object(filename)
                    .stream(file.getInputStream(), file.getSize(), -1)
                    .contentType(file.getContentType())
                    .build()
            );
            
            log.info("File uploaded successfully: {}", filename);
            return filename;
            
        } catch (Exception e) {
            log.error("Error uploading file", e);
            throw new RuntimeException("Failed to upload file", e);
        }
    }
    
    /**
     * Download file from MinIO
     */
    public InputStream downloadFile(String filename) {
        try {
            return minioClient.getObject(
                GetObjectArgs.builder()
                    .bucket(BUCKET_NAME)
                    .object(filename)
                    .build()
            );
        } catch (Exception e) {
            log.error("Error downloading file: {}", filename, e);
            throw new RuntimeException("Failed to download file", e);
        }
    }
    
    /**
     * Delete file from MinIO
     */
    public void deleteFile(String filename) {
        try {
            minioClient.removeObject(
                RemoveObjectArgs.builder()
                    .bucket(BUCKET_NAME)
                    .object(filename)
                    .build()
            );
            log.info("File deleted successfully: {}", filename);
        } catch (Exception e) {
            log.error("Error deleting file: {}", filename, e);
            throw new RuntimeException("Failed to delete file", e);
        }
    }
    
    /**
     * List all files in bucket
     */
    public List<String> listFiles() {
        List<String> files = new ArrayList<>();
        try {
            Iterable<Result<Item>> results = minioClient.listObjects(
                ListObjectsArgs.builder()
                    .bucket(BUCKET_NAME)
                    .build()
            );
            
            for (Result<Item> result : results) {
                Item item = result.get();
                files.add(item.objectName());
            }
            
            return files;
        } catch (Exception e) {
            log.error("Error listing files", e);
            throw new RuntimeException("Failed to list files", e);
        }
    }
    
    /**
     * Get file URL (presigned)
     */
    public String getFileUrl(String filename, int expirySeconds) {
        try {
            return minioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET)
                    .bucket(BUCKET_NAME)
                    .object(filename)
                    .expiry(expirySeconds)
                    .build()
            );
        } catch (Exception e) {
            log.error("Error generating presigned URL", e);
            throw new RuntimeException("Failed to generate URL", e);
        }
    }
    
    /**
     * Ensure bucket exists
     */
    private void ensureBucketExists() {
        try {
            boolean exists = minioClient.bucketExists(
                BucketExistsArgs.builder()
                    .bucket(BUCKET_NAME)
                    .build()
            );
            
            if (!exists) {
                minioClient.makeBucket(
                    MakeBucketArgs.builder()
                        .bucket(BUCKET_NAME)
                        .build()
                );
                log.info("Bucket created: {}", BUCKET_NAME);
            }
        } catch (Exception e) {
            log.error("Error checking bucket", e);
            throw new RuntimeException("Failed to check bucket", e);
        }
    }
}

Controller:

java
// FileController.java

package com.example.controller;

import com.example.service.MinioService;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;
import java.util.List;

@RestController
@RequestMapping("/api/files")
@RequiredArgsConstructor
public class FileController {
    
    private final MinioService minioService;
    
    /**
     * Upload file
     */
    @PostMapping("/upload")
    public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
        String filename = minioService.uploadFile(file);
        return ResponseEntity.ok(filename);
    }
    
    /**
     * Download file
     */
    @GetMapping("/download/{filename}")
    public ResponseEntity<InputStreamResource> downloadFile(@PathVariable String filename) {
        InputStream stream = minioService.downloadFile(filename);
        
        return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
                .body(new InputStreamResource(stream));
    }
    
    /**
     * Delete file
     */
    @DeleteMapping("/{filename}")
    public ResponseEntity<Void> deleteFile(@PathVariable String filename) {
        minioService.deleteFile(filename);
        return ResponseEntity.ok().build();
    }
    
    /**
     * List files
     */
    @GetMapping
    public ResponseEntity<List<String>> listFiles() {
        return ResponseEntity.ok(minioService.listFiles());
    }
    
    /**
     * Get file URL (presigned)
     */
    @GetMapping("/url/{filename}")
    public ResponseEntity<String> getFileUrl(@PathVariable String filename) {
        String url = minioService.getFileUrl(filename, 3600); // 1 hour
        return ResponseEntity.ok(url);
    }
}

application.properties:

properties
# MinIO Configuration
minio.url=http://localhost:9000
minio.access-key=admin
minio.secret-key=password123

# File upload limits
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB

Test:

bash
# Upload file
curl -X POST \
  http://localhost:8080/api/files/upload \
  -F "file=@/path/to/file.jpg"

# Response: 1703001234567_file.jpg

# Download file
curl -X GET \
  http://localhost:8080/api/files/download/1703001234567_file.jpg \
  -o downloaded.jpg

# Get presigned URL
curl -X GET \
  http://localhost:8080/api/files/url/1703001234567_file.jpg

# List files
curl -X GET \
  http://localhost:8080/api/files

# Delete file
curl -X DELETE \
  http://localhost:8080/api/files/1703001234567_file.jpg

Python Integration

python
# requirements.txt
minio==7.2.0
fastapi==0.104.1
uvicorn==0.24.0
python-multipart==0.0.6

# minio_service.py

from minio import Minio
from minio.error import S3Error
from datetime import timedelta
import os
from typing import List

class MinioService:
    def __init__(self):
        self.client = Minio(
            endpoint=os.getenv('MINIO_URL', 'localhost:9000'),
            access_key=os.getenv('MINIO_ACCESS_KEY', 'admin'),
            secret_key=os.getenv('MINIO_SECRET_KEY', 'password123'),
            secure=False  # Use True for HTTPS
        )
        self.bucket_name = 'uploads'
        self._ensure_bucket_exists()
    
    def _ensure_bucket_exists(self):
        """Ensure bucket exists"""
        try:
            if not self.client.bucket_exists(self.bucket_name):
                self.client.make_bucket(self.bucket_name)
                print(f"Bucket '{self.bucket_name}' created")
        except S3Error as e:
            print(f"Error checking bucket: {e}")
            raise
    
    def upload_file(self, file_path: str, object_name: str = None):
        """Upload file to MinIO"""
        if object_name is None:
            object_name = os.path.basename(file_path)
        
        try:
            self.client.fput_object(
                bucket_name=self.bucket_name,
                object_name=object_name,
                file_path=file_path
            )
            print(f"File uploaded: {object_name}")
            return object_name
        except S3Error as e:
            print(f"Error uploading file: {e}")
            raise
    
    def upload_bytes(self, data: bytes, object_name: str, content_type: str = 'application/octet-stream'):
        """Upload bytes to MinIO"""
        from io import BytesIO
        
        try:
            self.client.put_object(
                bucket_name=self.bucket_name,
                object_name=object_name,
                data=BytesIO(data),
                length=len(data),
                content_type=content_type
            )
            print(f"Bytes uploaded: {object_name}")
            return object_name
        except S3Error as e:
            print(f"Error uploading bytes: {e}")
            raise
    
    def download_file(self, object_name: str, file_path: str):
        """Download file from MinIO"""
        try:
            self.client.fget_object(
                bucket_name=self.bucket_name,
                object_name=object_name,
                file_path=file_path
            )
            print(f"File downloaded: {file_path}")
        except S3Error as e:
            print(f"Error downloading file: {e}")
            raise
    
    def get_file_stream(self, object_name: str):
        """Get file as stream"""
        try:
            response = self.client.get_object(
                bucket_name=self.bucket_name,
                object_name=object_name
            )
            return response.read()
        except S3Error as e:
            print(f"Error getting file stream: {e}")
            raise
        finally:
            response.close()
            response.release_conn()
    
    def delete_file(self, object_name: str):
        """Delete file from MinIO"""
        try:
            self.client.remove_object(
                bucket_name=self.bucket_name,
                object_name=object_name
            )
            print(f"File deleted: {object_name}")
        except S3Error as e:
            print(f"Error deleting file: {e}")
            raise
    
    def list_files(self, prefix: str = None) -> List[str]:
        """List files in bucket"""
        try:
            objects = self.client.list_objects(
                bucket_name=self.bucket_name,
                prefix=prefix,
                recursive=True
            )
            return [obj.object_name for obj in objects]
        except S3Error as e:
            print(f"Error listing files: {e}")
            raise
    
    def get_presigned_url(self, object_name: str, expires: int = 3600):
        """Get presigned URL (expires in seconds)"""
        try:
            url = self.client.presigned_get_object(
                bucket_name=self.bucket_name,
                object_name=object_name,
                expires=timedelta(seconds=expires)
            )
            return url
        except S3Error as e:
            print(f"Error generating URL: {e}")
            raise

# FastAPI example
from fastapi import FastAPI, UploadFile, File
from fastapi.responses import StreamingResponse

app = FastAPI()
minio_service = MinioService()

@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
    # Read file content
    content = await file.read()
    
    # Upload to MinIO
    filename = f"{int(time.time())}_{file.filename}"
    minio_service.upload_bytes(
        data=content,
        object_name=filename,
        content_type=file.content_type
    )
    
    return {"filename": filename}

@app.get("/download/{filename}")
async def download_file(filename: str):
    data = minio_service.get_file_stream(filename)
    
    return StreamingResponse(
        io.BytesIO(data),
        media_type='application/octet-stream',
        headers={'Content-Disposition': f'attachment; filename="{filename}"'}
    )

@app.delete("/files/{filename}")
async def delete_file(filename: str):
    minio_service.delete_file(filename)
    return {"status": "deleted"}

@app.get("/files")
async def list_files():
    files = minio_service.list_files()
    return {"files": files}

Node.js Integration

javascript
// package.json
{
  "dependencies": {
    "minio": "^7.1.3",
    "express": "^4.18.2",
    "multer": "^1.4.5-lts.1"
  }
}

// minio-service.js

const Minio = require('minio');
const fs = require('fs');

class MinioService {
  constructor() {
    this.client = new Minio.Client({
      endPoint: process.env.MINIO_ENDPOINT || 'localhost',
      port: parseInt(process.env.MINIO_PORT || '9000'),
      useSSL: process.env.MINIO_USE_SSL === 'true',
      accessKey: process.env.MINIO_ACCESS_KEY || 'admin',
      secretKey: process.env.MINIO_SECRET_KEY || 'password123'
    });
    
    this.bucketName = 'uploads';
    this._ensureBucketExists();
  }
  
  async _ensureBucketExists() {
    try {
      const exists = await this.client.bucketExists(this.bucketName);
      if (!exists) {
        await this.client.makeBucket(this.bucketName);
        console.log(`Bucket '${this.bucketName}' created`);
      }
    } catch (err) {
      console.error('Error checking bucket:', err);
      throw err;
    }
  }
  
  async uploadFile(filePath, objectName) {
    try {
      const metaData = {
        'Content-Type': 'application/octet-stream'
      };
      
      await this.client.fPutObject(
        this.bucketName,
        objectName,
        filePath,
        metaData
      );
      
      console.log(`File uploaded: ${objectName}`);
      return objectName;
    } catch (err) {
      console.error('Error uploading file:', err);
      throw err;
    }
  }
  
  async uploadBuffer(buffer, objectName, contentType = 'application/octet-stream') {
    try {
      await this.client.putObject(
        this.bucketName,
        objectName,
        buffer,
        buffer.length,
        { 'Content-Type': contentType }
      );
      
      console.log(`Buffer uploaded: ${objectName}`);
      return objectName;
    } catch (err) {
      console.error('Error uploading buffer:', err);
      throw err;
    }
  }
  
  async downloadFile(objectName, filePath) {
    try {
      await this.client.fGetObject(this.bucketName, objectName, filePath);
      console.log(`File downloaded: ${filePath}`);
    } catch (err) {
      console.error('Error downloading file:', err);
      throw err;
    }
  }
  
  async getFileStream(objectName) {
    try {
      return await this.client.getObject(this.bucketName, objectName);
    } catch (err) {
      console.error('Error getting file stream:', err);
      throw err;
    }
  }
  
  async deleteFile(objectName) {
    try {
      await this.client.removeObject(this.bucketName, objectName);
      console.log(`File deleted: ${objectName}`);
    } catch (err) {
      console.error('Error deleting file:', err);
      throw err;
    }
  }
  
  async listFiles(prefix = '') {
    return new Promise((resolve, reject) => {
      const files = [];
      const stream = this.client.listObjects(this.bucketName, prefix, true);
      
      stream.on('data', obj => files.push(obj.name));
      stream.on('error', reject);
      stream.on('end', () => resolve(files));
    });
  }
  
  async getPresignedUrl(objectName, expires = 3600) {
    try {
      return await this.client.presignedGetObject(
        this.bucketName,
        objectName,
        expires
      );
    } catch (err) {
      console.error('Error generating presigned URL:', err);
      throw err;
    }
  }
}

module.exports = MinioService;

// Express app example
const express = require('express');
const multer = require('multer');
const MinioService = require('./minio-service');

const app = express();
const upload = multer({ storage: multer.memoryStorage() });
const minioService = new MinioService();

// Upload file
app.post('/upload', upload.single('file'), async (req, res) => {
  try {
    const filename = `${Date.now()}_${req.file.originalname}`;
    
    await minioService.uploadBuffer(
      req.file.buffer,
      filename,
      req.file.mimetype
    );
    
    res.json({ filename });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Download file
app.get('/download/:filename', async (req, res) => {
  try {
    const stream = await minioService.getFileStream(req.params.filename);
    
    res.setHeader('Content-Disposition', `attachment; filename="${req.params.filename}"`);
    stream.pipe(res);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Delete file
app.delete('/files/:filename', async (req, res) => {
  try {
    await minioService.deleteFile(req.params.filename);
    res.json({ status: 'deleted' });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// List files
app.get('/files', async (req, res) => {
  try {
    const files = await minioService.listFiles();
    res.json({ files });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Kapitola 4: Advanced Features

Bucket Policies (Public Access)

bash
# Make bucket public (via mc)
mc anonymous set download local/images

# Check policy
mc anonymous get local/images

# Remove public access
mc anonymous set none local/images

Via Web Console:

1. Go to Buckets
2. Select bucket → "Manage"
3. "Access Policy" → "public"

Custom Policy (JSON):

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": ["*"]
      },
      "Action": ["s3:GetObject"],
      "Resource": ["arn:aws:s3:::images/*"]
    }
  ]
}

Versioning

bash
# Enable versioning
mc version enable local/my-files

# Check version status
mc version info local/my-files

# List object versions
mc ls --versions local/my-files/

# Suspend versioning
mc version suspend local/my-files

Lifecycle Management

json
// lifecycle.json
{
  "Rules": [
    {
      "ID": "Delete old files",
      "Status": "Enabled",
      "Expiration": {
        "Days": 30
      },
      "Filter": {
        "Prefix": "temp/"
      }
    },
    {
      "ID": "Archive old versions",
      "Status": "Enabled",
      "NoncurrentVersionExpiration": {
        "NoncurrentDays": 90
      }
    }
  ]
}
bash
# Set lifecycle policy
mc ilm import local/my-files < lifecycle.json

# View lifecycle policy
mc ilm export local/my-files

# Remove lifecycle policy
mc ilm remove local/my-files

Encryption

bash
# Server-side encryption (SSE-S3)
mc encrypt set sse-s3 local/secure-files

# Check encryption
mc encrypt info local/secure-files

# Remove encryption
mc encrypt clear local/secure-files

Kapitola 5: Production Setup

Multi-Node Cluster (High Availability)

yaml
# docker-compose-cluster.yml

version: '3.8'

services:
  minio1:
    image: minio/minio:latest
    container_name: minio1
    hostname: minio1
    volumes:
      - data1-1:/data1
      - data1-2:/data2
    environment:
      MINIO_ROOT_USER: admin
      MINIO_ROOT_PASSWORD: password123
    command: server http://minio{1...4}/data{1...2} --console-address ":9001"
    networks:
      - minio_cluster
  
  minio2:
    image: minio/minio:latest
    container_name: minio2
    hostname: minio2
    volumes:
      - data2-1:/data1
      - data2-2:/data2
    environment:
      MINIO_ROOT_USER: admin
      MINIO_ROOT_PASSWORD: password123
    command: server http://minio{1...4}/data{1...2} --console-address ":9001"
    networks:
      - minio_cluster
  
  minio3:
    image: minio/minio:latest
    container_name: minio3
    hostname: minio3
    volumes:
      - data3-1:/data1
      - data3-2:/data2
    environment:
      MINIO_ROOT_USER: admin
      MINIO_ROOT_PASSWORD: password123
    command: server http://minio{1...4}/data{1...2} --console-address ":9001"
    networks:
      - minio_cluster
  
  minio4:
    image: minio/minio:latest
    container_name: minio4
    hostname: minio4
    volumes:
      - data4-1:/data1
      - data4-2:/data2
    environment:
      MINIO_ROOT_USER: admin
      MINIO_ROOT_PASSWORD: password123
    command: server http://minio{1...4}/data{1...2} --console-address ":9001"
    networks:
      - minio_cluster
  
  # Nginx load balancer
  nginx:
    image: nginx:alpine
    container_name: minio-lb
    ports:
      - "9000:9000"
      - "9001:9001"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - minio1
      - minio2
      - minio3
      - minio4
    networks:
      - minio_cluster

volumes:
  data1-1:
  data1-2:
  data2-1:
  data2-2:
  data3-1:
  data3-2:
  data4-1:
  data4-2:

networks:
  minio_cluster:
    driver: bridge

nginx.conf:

nginx
events {
    worker_connections 1024;
}

http {
    upstream minio_api {
        server minio1:9000;
        server minio2:9000;
        server minio3:9000;
        server minio4:9000;
    }
    
    upstream minio_console {
        server minio1:9001;
        server minio2:9001;
        server minio3:9001;
        server minio4:9001;
    }
    
    server {
        listen 9000;
        
        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;
            
            client_max_body_size 100M;
        }
    }
    
    server {
        listen 9001;
        
        location / {
            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;
        }
    }
}

SSL/TLS Configuration

bash
# Generate certificates
mkdir -p certs
openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 \
  -keyout certs/private.key \
  -out certs/public.crt

# docker-compose.yml with SSL
services:
  minio:
    image: minio/minio:latest
    volumes:
      - minio_data:/data
      - ./certs:/root/.minio/certs  # Mount certificates
    environment:
      MINIO_ROOT_USER: admin
      MINIO_ROOT_PASSWORD: password123
    command: server /data --console-address ":9001"
    ports:
      - "9000:9000"
      - "9001:9001"

Kapitola 6: Best Practices

DO's ✅

bash
# 1. Persist data
volumes:
  - minio_data:/data  # ✅ Data survives restart

# 2. Use strong passwords
MINIO_ROOT_PASSWORD=super-secure-password-here  # ✅

# 3. Use buckets for organization
uploads/
  ├── images/
  ├── documents/
  └── videos/

# 4. Set lifecycle policies
# Delete temp files after 7 days ✅

# 5. Enable versioning for important data
mc version enable local/important-files  # ✅

# 6. Use presigned URLs for temporary access
url = getPresignedUrl(filename, expires=3600)  # ✅

# 7. Monitor storage usage
mc admin info local  # ✅

DON'Ts ❌

bash
# 1. Don't use default credentials in production
MINIO_ROOT_PASSWORD=password123  # ❌ Change it!

# 2. Don't make everything public
mc anonymous set public local/bucket  # ❌ Be selective

# 3. Don't store credentials in code
accessKey = "admin"  # ❌ Use environment variables!

# 4. Don't ignore errors
try:
    upload_file()
except:
    pass  # ❌ Handle errors properly!

# 5. Don't forget to close streams
stream = getObject()
# ... do something ...
# ❌ Forgot to close! Memory leak!

# Instead:
try:
    stream = getObject()
    # ... do something ...
finally:
    stream.close()  # ✅

Kapitola 7: Real-World Use Cases

Use Case 1: Image Upload Service

java
@Service
public class ImageService {
    
    @Autowired
    private MinioService minioService;
    
    private static final Set<String> ALLOWED_TYPES = Set.of(
        "image/jpeg", "image/png", "image/gif", "image/webp"
    );
    
    private static final long MAX_SIZE = 5 * 1024 * 1024; // 5MB
    
    public String uploadImage(MultipartFile file) {
        // Validate
        if (!ALLOWED_TYPES.contains(file.getContentType())) {
            throw new IllegalArgumentException("Invalid image type");
        }
        
        if (file.getSize() > MAX_SIZE) {
            throw new IllegalArgumentException("Image too large");
        }
        
        // Generate thumbnail
        BufferedImage thumbnail = createThumbnail(file);
        
        // Upload original
        String originalFilename = minioService.uploadFile(file);
        
        // Upload thumbnail
        String thumbnailFilename = "thumb_" + originalFilename;
        minioService.uploadImage(thumbnail, thumbnailFilename);
        
        return originalFilename;
    }
    
    private BufferedImage createThumbnail(MultipartFile file) {
        // Use Thumbnailator or similar
        return Thumbnails.of(file.getInputStream())
            .size(200, 200)
            .asBufferedImage();
    }
}

Use Case 2: Document Management

python
class DocumentService:
    def __init__(self, minio_service):
        self.minio = minio_service
    
    def upload_document(self, file, metadata):
        """Upload document with metadata"""
        # Generate ID
        doc_id = str(uuid.uuid4())
        filename = f"docs/{doc_id}/{file.filename}"
        
        # Upload file
        self.minio.upload_bytes(
            data=file.read(),
            object_name=filename,
            content_type=file.content_type
        )
        
        # Store metadata
        metadata_file = f"docs/{doc_id}/metadata.json"
        self.minio.upload_bytes(
            data=json.dumps(metadata).encode(),
            object_name=metadata_file,
            content_type='application/json'
        )
        
        return doc_id
    
    def get_document(self, doc_id):
        """Get document with metadata"""
        # Get metadata
        metadata_file = f"docs/{doc_id}/metadata.json"
        metadata_bytes = self.minio.get_file_stream(metadata_file)
        metadata = json.loads(metadata_bytes)
        
        # Get file list
        files = self.minio.list_files(prefix=f"docs/{doc_id}/")
        documents = [f for f in files if not f.endswith('metadata.json')]
        
        return {
            'metadata': metadata,
            'files': documents
        }

Use Case 3: Backup Service

javascript
class BackupService {
  constructor(minioService) {
    this.minio = minioService;
  }
  
  async backupDatabase() {
    // Create backup
    const timestamp = new Date().toISOString();
    const backupFile = `/tmp/backup_${timestamp}.sql`;
    
    await exec(`pg_dump mydb > ${backupFile}`);
    
    // Upload to MinIO
    const objectName = `backups/db_${timestamp}.sql`;
    await this.minio.uploadFile(backupFile, objectName);
    
    // Clean up local file
    fs.unlinkSync(backupFile);
    
    // Keep only last 30 days
    await this.cleanOldBackups(30);
    
    return objectName;
  }
  
  async cleanOldBackups(days) {
    const files = await this.minio.listFiles('backups/');
    const cutoff = new Date();
    cutoff.setDate(cutoff.getDate() - days);
    
    for (const file of files) {
      const match = file.match(/backup_(.+)\.sql/);
      if (match) {
        const date = new Date(match[1]);
        if (date < cutoff) {
          await this.minio.deleteFile(file);
          console.log(`Deleted old backup: ${file}`);
        }
      }
    }
  }
}

Záver

MinIO Setup:

bash
time_to_s3 = {
    "AWS Account": "3 days (approval + setup)",
    "MinIO": "5 minút",
    
    "verdict": "MinIO wins! 🏆"
}

Key Benefits:

✅ S3-compatible API (same code for AWS & MinIO)
✅ Local development (no AWS account)
✅ Fast setup (one Docker command)
✅ Zero cost (open source)
✅ Production ready (high performance)
✅ Easy integration (Java, Python, Node.js)

When to use:

Development:
✅ Local S3 for testing
✅ CI/CD pipelines
✅ Integration tests

Production:
✅ On-premise storage
✅ Private cloud
✅ Cost-effective alternative to S3
✅ Data sovereignty requirements

Resources:

Docs: https://min.io/docs/minio/
Docker: https://hub.docker.com/r/minio/minio
SDKs: https://min.io/docs/minio/linux/developers/minio-drivers.html
mc Client: https://min.io/docs/minio/linux/reference/minio-mc.html

Článok napísal developer ktorý potreboval S3 lokálne. MinIO zachránil projekt. 5 minút vs 3 dni. 🚀

P.S.: S3-compatible znamená že kód funguje rovnako na MinIO aj AWS. Win-win! 💯

P.P.S.: Žiadny IAM hell, žiadne approval processy, žiadne čakanie. Just works. ✨

P.P.P.S.: Production? MinIO clusters handle milióny requestov. Trust me. 💪

Všetky články