- Published on
Tích Hợp Minio Trong Ứng Dụng Spring Boot
- Authors
- Name
- David Nguyen
Table of Contents
1. Object Storage và MinIO là gì?

Khi làm việc với các hệ thống hiện đại ngày này thì việc lưu trữ các đối tượng dữ liệu phi cấu trúc như videos, images, logs... trở thành điều gần như bắt buộc.
Điều này khó có thể thực hiện được bởi các hệ quản trị cơ sở dữ liệu, đặc biệt là các hệ quản trị cơ sở dữ liệu có cấu trúc. Chính vì vậy Object Storage được ra đời để giải quyết bài toán này và MinIO là một trong những Object Storage phổ biến nhất.
1.1 - Object Storage là gì?
Object storage is a storage architecture for storing unstructured data. This architecture divides data into units (objects) and stores them in a flat data environment. Each object includes the data, metadata, and a unique identifier that applications can use to facilitate viewing and retrieval.
Trích https://cloud.google.com/learn/what-is-object-storage
Hiểu đơn giản, object storage lưu trữ dữ liệu phi cấu trúc (unstructured data), chia dữ liệu thành các đơn vị gọi là object và lưu trữ những unit đó trên môi trường dữ liệu phẳng. Mỗi object bao gồm:
- Data: Nội dung lưu trữ
- Metadata: Các thông tin liên quan đến object (created date, permissions, content type...)
- Unique Identifier: Khoá duy nhất để duyệt qua các object.
1.2 - Cần biết gì về MinIO?
Khi nói về Object Storage có thể các bạn đã nghe tới nhiều Cloud Object Storage như AWS S3, Google Cloud Storage, Azure Blob Storage...

Vậy MinIO có điểm gì khác so với những cái tên kể trên? Để dễ hình dung mình sẽ so sánh MinIO và AWS S3 trên một sô khía cạnh như sau:
Tiêu chí | MinIO | AWS S3 |
---|---|---|
Triển khai | Self-hosted (On-premises, Kubernetes, Cloud) | Managed service trên AWS |
Hiệu suất | Cực nhanh (tối ưu cho Kubernetes & Edge Computing) | Ổn định, phụ thuộc vào AWS hạ tầng |
Chi phí | Miễn phí (Self-host) hoặc có phí hỗ trợ | Pay-as-you-go (AWS tính phí theo dung lượng) |
Khả năng mở rộng | Horizontal scaling (dễ mở rộng với nhiều node) | Auto-scaled bởi AWS |
Bảo mật | Self-managed (IAM, TLS, encryption tùy chỉnh) | AWS IAM, KMS, PrivateLink, VPC Endpoint |
Khả dụng | Phụ thuộc vào hạ tầng tự triển khai | 99.99% SLA trên AWS |
=> Như vậy chúng ta có thể kết luận nên sử dụng MinIO khi cần một private cloud storage, tự quản lý dữ liệu hoặc giảm chi phí.
Để tìm hiểu thêm về MinIO, các bạn có thể tham khảo thêm tại đây:
2. Tích hợp MinIO trong ứng dụng Spring Boot
2.1 - Cài đặt và cấu hình
MinIO hỗ trợ cài đặt trên hầu hết các nền tảng cũng như hệ điều hành hiện nay (K8s, Docker, Linux, macOS, Windows). Để thuận tiện trong bài viết này mình sẽ cùng các bạn cài đặt MinIO thông qua Docker.
Các bạn có thể clone source code của mình tại đây, branch init_project
, cd đến root folder của source code và chạy command:
docker compose up -d
Ở đây, mình đã viết một file docker-compose.yml
để setup chạy MinIO dưới dạng Docker container.
version: '3.8'
services:
minio:
image: quay.io/minio/minio
container_name: minio
restart: unless-stopped
env_file:
- dev.env
volumes:
- minio-data:/data
ports:
- '9000:9000'
- '9001:9001'
command: server /data --console-address ":9001"
volumes:
minio-data:
driver: local
Nếu bạn nào chưa rành về Docker compose có thể chạy lệnh run Docker container trực tiếp như sau:
mkdir -p ~/minio/data
docker run \
-p 9000:9000 \
-p 9001:9001 \
--name minio \
-v ~/minio/data:/data \
-e "MINIO_ROOT_USER=admin" \
-e "MINIO_ROOT_PASSWORD=Admin@123" \
quay.io/minio/minio server /data --console-address ":9001"
Để kiểm tra trạng thái của container sau khi khởi chạy các bạn có thể dùng lệnh:
~/Desktop/code/spring/minio init_project +1 ❯ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a14adce9ba2b quay.io/minio/minio "/usr/bin/docker-ent…" 24 minutes ago Up 24 minutes 0.0.0.0:9000-9001->9000-9001/tcp minio
~/Desktop/code/spring/minio init_project +1 ❯ docker logs -t minio
2025-02-23T02:19:35.793744676Z MinIO Object Storage Server
2025-02-23T02:19:35.793805218Z Copyright: 2015-2025 MinIO, Inc.
2025-02-23T02:19:35.793809009Z License: GNU AGPLv3 - https://www.gnu.org/licenses/agpl-3.0.html
2025-02-23T02:19:35.793810968Z Version: RELEASE.2025-02-18T16-25-55Z (go1.23.6 linux/arm64)
2025-02-23T02:19:35.793813093Z
2025-02-23T02:19:35.793815134Z API: http://172.24.0.2:9000 http://127.0.0.1:9000
2025-02-23T02:19:35.793817218Z WebUI: http://172.24.0.2:9001 http://127.0.0.1:9001
2025-02-23T02:19:35.793819051Z
2025-02-23T02:19:35.793820676Z Docs: https://docs.min.io
Như vậy là chúng ta đã setup MinIO thành công, để truy cập vào UI của MinIO các bạn follow theo đường dẫn: http://localhost:9001

2.2 - Tích hợp MinIO trong ứng dụng Spring Boot
- Để sử dụng được MinIO trong ứng dụng Spring Boot chúng ta phải thêm dependency vào file
pom.xml
như sau:
<properties>
<minio.version>8.5.7</minio.version>
</properties>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
- Thêm cấu hình trong file
application.yml
spring:
application:
name: Spring Boot and MinIO Integration
profiles:
active: minio
servlet:
multipart:
max-file-size: 5GB # Giới hạn dung lượng tối đa của một tệp tải lên (5GB)
max-request-size: 50GB # Giới hạn tổng dung lượng của tất cả các tệp trong một request (5GB)
minio:
url: http://localhost:9000 # URL của MinIO server
key:
access: admin # Access key để kết nối MinIO
secret: Admin@123 # Secret key để xác thực với MinIO
bucket:
default: demo # Tên bucket mặc định để lưu trữ file
server:
port: 9005
- Cấu hình MinIO Client API cho phép chúng ta thực hiện các thao tác với object. Các bạn có thể đọc kỹ hơn về MinIO Client API cho Java tại đây
package com.davidnguyen.demo.minio.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}")
private String minioUrl;
@Value("${minio.key.access}")
private String accessKey;
@Value("${minio.key.secret}")
private String secretKey;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(minioUrl)
.credentials(accessKey, secretKey)
.build();
}
}
- Bây giờ mình sẽ viết các services để implement một số phương thức như upload, download và get pre-signed file's url.
package com.davidnguyen.demo.minio.service;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.List;
@Service
public interface StorageService {
void upload(List<MultipartFile> files);
InputStream download(String fileName);
String getURL(String fileName);
}
package com.davidnguyen.demo.minio.service;
import io.minio.*;
import io.minio.http.Method;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Service
@RequiredArgsConstructor
@Profile("minio")
public class MinIOStorageService implements StorageService {
private final MinioClient minioClient;
@Value("${minio.bucket.default}")
private String bucketName;
@Override
public void upload(List<MultipartFile> files) {
this.ensureBucketExists(bucketName);
for (MultipartFile file : files) {
try (InputStream inputStream = file.getInputStream()) {
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(file.getOriginalFilename())
.stream(inputStream, file.getSize(), -1)
.contentType(file.getContentType())
.build()
);
} catch (Exception e) {
throw new RuntimeException("Failed to upload file: " + file.getOriginalFilename(), e);
}
}
}
private void ensureBucketExists(String bucketName) {
try {
boolean found = minioClient.bucketExists(
BucketExistsArgs.builder().bucket(bucketName).build()
);
if (!found) {
minioClient.makeBucket(
MakeBucketArgs.builder().bucket(bucketName).build()
);
}
} catch (Exception e) {
throw new RuntimeException("Failed to ensure bucket exists: " + bucketName, e);
}
}
@Override
public InputStream download(String fileName) {
try {
return minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.build()
);
} catch (Exception e) {
throw new RuntimeException("Error downloading file: " + e.getMessage());
}
}
@Override
public String getURL(String fileName) {
try {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.bucket(bucketName)
.object(fileName)
.method(Method.GET)
.expiry(30, TimeUnit.DAYS)
.build()
);
} catch (Exception e) {
throw new RuntimeException("Error generating pre-signed URL: " + e.getMessage());
}
}
}
- Viết controller để expose các APIs và thực hiện testing.
package com.davidnguyen.demo.minio.controller;
import com.davidnguyen.demo.minio.service.MinIOStorageService;
import com.davidnguyen.demo.minio.service.StorageService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
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/v1/storage")
@RequiredArgsConstructor
public class StorageController {
/**
* Upload files to storage.
*/
@PostMapping("/upload")
public ResponseEntity<Void> uploadFile(
@RequestPart("files") List<MultipartFile> files
) {
storageService.upload(files);
return ResponseEntity.ok().build();
}
private final StorageService storageService;
/**
* Download file from storage.
*/
@GetMapping("/download/{fileName}")
public ResponseEntity<byte[]> download(
@PathVariable String fileName
) {
try {
InputStream stream = storageService.download(fileName);
byte[] content = stream.readAllBytes();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
.body(content);
} catch (Exception e) {
return ResponseEntity.internalServerError().body(null);
}
}
/**
* Get pre-signed URL for accessing a file.
*/
@GetMapping("/pre-signed-url/{fileName}")
public ResponseEntity<String> getPreSignedUrl(@PathVariable String fileName) {
return ResponseEntity.ok(storageService.getURL(fileName));
}
}
=> Lưu Ý:
- Mặc định MinIO không hỗ trợ versioning cho các file được upload nên nếu chúng ta upload hai files khác nhau hoặc tương tự nhau nhựng cùng tên file thì mặc định MinIO sẽ override file được upload sau.
- Để tránh điều này, chúng ta có thể generate unique file name cho từng file mỗi lần upload bằng cách append thêm current date hoặc uuid vào file name hoặc sử dụng folders for versioning như sau:
@Override
public void upload(List<MultipartFile> files) {
this.ensureBucketExists(bucketName);
for (MultipartFile file : files) {
String fileName = this.generateFileName(file);
//String fileName = UUID.randomUUID() + "/" + file.getOriginalFilename(); //Folders for versioning
try (InputStream inputStream = file.getInputStream()) {
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.stream(inputStream, file.getSize(), -1)
.contentType(file.getContentType())
.build()
);
} catch (Exception e) {
throw new RuntimeException("Failed to upload file: " + file.getOriginalFilename(), e);
}
}
}
private String generateFileName(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
if (originalFilename == null) {
throw new IllegalArgumentException("File name cannot be null");
}
String extension = "";
int dotIndex = originalFilename.lastIndexOf(".");
if (dotIndex != -1) {
extension = originalFilename.substring(dotIndex);
originalFilename = originalFilename.substring(0, dotIndex);
}
long timestamp = System.currentTimeMillis();
return originalFilename + "_" + timestamp + extension;
}
3. Testing
- API upload files:


- API download file:

- API get file pre-signed URL:

=> Lưu Ý: Các bạn có thể tham khảo toàn bộ source code của mình tại đây
4. Tổng Kết
Vậy là trong bài viết này mình đã cùng các bạn tìm hiểu về MinIO - một Object Storage khá phổ biển hiện nay. Việc tích hợp MinIO vào ứng dụng Spring Boot mang lại một giải pháp lưu trữ đối tượng mạnh mẽ, linh hoạt và tối ưu chi phí.
Với khả năng tương thích cao với Amazon S3 API, MinIO giúp các ứng dụng có thể dễ dàng chuyển đổi giữa môi trường on-premises và cloud, đồng thời tối ưu hiệu suất cho các hệ thống microservices hoặc edge computing.
Hy vọng bài viết này giúp các bạn hiểu thêm về MinIO cũng như làm sao để tích hợp và sử dụng MinIO trong ứng dụng Spring.
Hẹn gặp lại các bạn trong các bài viết tiếp theo. Happy Coding!