Deploying MinIO for Object Storage with Spring Boot Integration
Windows Environment Setup
MinIO is a lightweight, high-performance object storage system written in Go. It provides an S3-compatible API and is suitable for local deployments or on-premise infrastructure.
Local Installation
- Download the
windows-amd64executable from the official repository. - Place
minio.exeinto a dedicated directory, such asC:\Program Files\MinIO. - Define the environment variable
MINIO_HOMEpointing to this path.
To initialize the service, open an elevated command prompt, navigate to the installation directory, and execute:
cd "C:\Program Files\MinIO"
minio.exe server D:/data-store
The specified directory (D:/data-store) acts as the persistent data volume. Upon successful startup, the console outputs default credentials. Access the web interface at http://127.0.0.1:9000 using minioadmin/minioadmin. Once logged in, navigate to Buckets, click Create Bucket, and adjust the Access Policy under Manage if public read/write access is required.
Terminate the service by closing the terminal window or pressing Ctrl+C.
Spring Boot Configuration
Dependencies
Include the MinIO client library alongside its required HTTP transport dependency. Note that explicit version overrides may be necessary for newer OkHttp versions.
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.9</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
Application Properties
Configure the embedded Tomcat file limits and the storage endpoint credentials in application.yml:
server:
port: 8080
spring:
servlet:
multipart:
enabled: true
max-file-size: 100MB
max-request-size: 250MB
storage:
minio:
endpoint: http://127.0.0.1:9000
access-key: minioadmin
secret-key: minioadmin
Configuration Class
Map the properties to a dedicated configuration component and instantiate the client bean:
import io.minio.MinioClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "storage.minio")
public class MinIOConfig {
private String endpoint;
private String accessKey;
private String secretKey;
public void setEndpoint(String endpoint) { this.endpoint = endpoint; }
public void setAccessKey(String accessKey) { this.accessKey = accessKey; }
public void setSecretKey(String secretKey) { this.secretKey = secretKey; }
@Bean
public MinioClient createClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
Core Storage Adapter
Consolidate all storage operations into a single utility class. The implementation replaces direct RuntimeException throws with custom business exceptions, standardizes builder patterns, and handles stream lengths correctly.
import io.minio.*;
import io.minio.http.Method;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
@RequiredArgsConstructor
public class ObjectStorageService {
private final MinioClient client;
public void ensureBucketExists(String bucketName) {
boolean exists;
try {
exists = client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
} catch (Exception e) {
log.error("Failed to check bucket existence", e);
throw new StorageOperationException(e);
}
if (!exists) {
try {
client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
} catch (Exception e) {
log.error("Failed to create bucket: {}", bucketName, e);
throw new StorageOperationException(e);
}
}
}
public ObjectWriteResponse uploadFromMultipart(String bucketName, MultipartFile sourceFile, String targetKey, String mediaType) {
try (InputStream contentStream = sourceFile.getInputStream()) {
PutObjectArgs putArgs = PutObjectArgs.builder()
.bucket(bucketName)
.object(targetKey)
.contentType(mediaType)
.stream(contentStream, -1, -1)
.build();
return client.putObject(putArgs);
} catch (Exception e) {
throw new StorageOperationException("Upload failed for bucket: " + bucketName, e);
}
}
public ObjectWriteResponse uploadFromFileSystem(String bucketName, String targetKey, String localFilePath) {
try {
UploadObjectArgs uploadArgs = UploadObjectArgs.builder()
.bucket(bucketName)
.object(targetKey)
.filename(localFilePath)
.build();
return client.uploadObject(uploadArgs);
} catch (Exception e) {
throw new StorageOperationException("Local file upload failed", e);
}
}
public ObjectWriteResponse uploadFromRawStream(String bucketName, String targetKey, InputStream dataStream) {
try {
PutObjectArgs args = PutObjectArgs.builder()
.bucket(bucketName)
.object(targetKey)
.stream(dataStream, -1, -1)
.build();
return client.putObject(args);
} catch (Exception e) {
throw new StorageOperationException("Stream upload failed", e);
}
}
public String generatePublicUrl(String bucketName, String targetKey, int validityMinutes) {
try {
GetPresignedObjectUrlArgs urlArgs = GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(targetKey)
.expiry(validityMinutes, ChronoUnit.MINUTES)
.build();
return client.getPresignedObjectUrl(urlArgs);
} catch (Exception e) {
throw new StorageOperationException("URL generation failed", e);
}
}
public String generateSignedUrlExtended(String bucketName, String targetKey, long duration, TimeUnit timeUnit) {
try {
GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(targetKey)
.expiry(duration, timeUnit)
.build();
return client.getPresignedObjectUrl(args);
} catch (Exception e) {
throw new StorageOperationException("Presigned URL creation error", e);
}
}
public void createVirtualDirectory(String bucketName, String dirPath) {
try {
PutObjectArgs args = PutObjectArgs.builder()
.bucket(bucketName)
.object(dirPath.endsWith("/") ? dirPath : dirPath + "/")
.stream(new ByteArrayInputStream(new byte[0]), 0, -1)
.build();
client.putObject(args);
} catch (Exception e) {
throw new StorageOperationException("Directory creation failed", e);
}
}
public boolean isObjectPresent(String bucketName, String targetKey) {
try {
client.statObject(StatObjectArgs.builder().bucket(bucketName).object(targetKey).build());
return true;
} catch (Exception e) {
return false;
}
}
public boolean isVirtualFolderPresent(String bucketName, String folderPrefix) {
try {
Iterable<Result<Item>> objects = client.listObjects(
ListObjectsArgs.builder().bucket(bucketName).prefix(folderPrefix).recursive(false).build()
);
for (Result<Item> result : objects) {
Item item = result.get();
if (item.isDir() && item.objectName().startsWith(folderPrefix)) {
return true;
}
}
} catch (Exception ignored) { /* Intentional */ }
return false;
}
public StatObjectResponse fetchMetadata(String bucketName, String targetKey) {
try {
return client.statObject(StatObjectArgs.builder().bucket(bucketName).object(targetKey).build());
} catch (Exception e) {
throw new StorageOperationException("Metadata retrieval failed", e);
}
}
public void deleteObject(String bucketName, String targetKey) {
try {
client.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(targetKey).build());
} catch (Exception e) {
throw new StorageOperationException("Object deletion failed", e);
}
}
public void deleteBucket(String bucketName) {
try {
client.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
} catch (Exception e) {
throw new StorageOperationException("Bucket removal failed", e);
}
}
public static class StorageOperationException extends RuntimeException {
public StorageOperationException(String message) { super(message); }
public StorageOperationException(String message, Throwable cause) { super(message, cause); }
}
}
REST Endpoint & Client Interface
Provide a simple HTML form to trigger uploads via a Spring MVC controller.
Client View
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>File Uploader</title></head>
<body>
<form action="/api/files/submit" method="post" enctype="multipart/form-data">
<label>Select document:</label>
<input type="file" name="document" required>
<button type="submit">Transmit</button>
</form>
</body>
</html>
Server Controller
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.UUID;
@RestController
@RequestMapping("/api/files")
public class FileUploadController {
private final ObjectStorageService storageService;
@Autowired
public FileUploadController(ObjectStorageService storageService) {
this.storageService = storageService;
}
@PostMapping("/submit")
public ResponseEntity<String> handleUpload(@RequestParam("document") MultipartFile uploadedPart) {
String originalName = uploadedPart.getOriginalFilename();
String extension = originalName != null ? originalName.substring(originalName.lastIndexOf(".")) : "";
String uniqueKey = UUID.randomUUID().toString().replace("-", "") + extension;
storageService.ensureBucketExists("user-uploads");
storageService.uploadFromMultipart("user-uploads", uploadedPart, uniqueKey, "image/jpeg");
String publicLink = storageService.generatePublicUrl("user-uploads", uniqueKey, 7);
return ResponseEntity.ok(publicLink);
}
}
Standard MIME Type Reference
When submitting forms or configuring storage buckets, mapping correct content types ensures proper browser rendering and server validation.
| Category | Content-Type | Description |
|---|---|---|
| Text-based | text/html |
Structured markup documents |
| Text-based | text/plain |
Unformatted character data |
| Text-based | text/xml, application/xml |
Extensible markup structures |
| Images | image/jpeg |
Photographs and complex graphics |
| Images | image/png |
Lossless raster images |
| Images | image/gif |
Animated or indexed-color graphics |
| Documents | application/pdf |
Portable document format |
| Documents | application/json |
Lightweight data interchange |
| Documents | application/octet-stream |
Generic binary streams |
| Forms | application/x-www-form-urlencoded |
Default key-value encoding for HTML forms |