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
// 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:
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)
# 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 / password123Total 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):
# 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 localKrok 3: Upload First File (2 minúty)
# 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.txtTotal time: 5 minút!
Kapitola 2: Docker Compose Setup
Production-Ready Configuration
# 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: bridgeEnvironment file (.env):
# .env
MINIO_ROOT_USER=admin
MINIO_ROOT_PASSWORD=your-secure-password-here
MINIO_DEFAULT_BUCKETS=uploads,images,documentsRun:
# Start
docker-compose up -d
# Check logs
docker-compose logs -f minio
# Stop
docker-compose down
# Stop and remove volumes
docker-compose down -vKapitola 3: Application Integration
Java / Spring Boot
Dependencies (pom.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:
// 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:
// 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:
// 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:
# 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=10MBTest:
# 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.jpgPython Integration
# 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
// 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)
# 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/imagesVia Web Console:
1. Go to Buckets
2. Select bucket → "Manage"
3. "Access Policy" → "public"Custom Policy (JSON):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": ["*"]
},
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::images/*"]
}
]
}Versioning
# 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-filesLifecycle Management
// lifecycle.json
{
"Rules": [
{
"ID": "Delete old files",
"Status": "Enabled",
"Expiration": {
"Days": 30
},
"Filter": {
"Prefix": "temp/"
}
},
{
"ID": "Archive old versions",
"Status": "Enabled",
"NoncurrentVersionExpiration": {
"NoncurrentDays": 90
}
}
]
}# 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-filesEncryption
# 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-filesKapitola 5: Production Setup
Multi-Node Cluster (High Availability)
# 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: bridgenginx.conf:
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
# 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 ✅
# 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 ❌
# 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
@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
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
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:
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 requirementsResources:
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. 💪