Swap Alibaba Cloud OSS for MinIO and Save Thousands a Year on Object Storage
Background
File upload requirements come up constantly in outsourcing projects — user avatars, contract PDFs, Excel exports. Initially, Alibaba Cloud OSS was the easy choice, until one project where users uploaded several thousand contract scans and the monthly OSS bill hit over 200 yuan.
200 yuan isn't much on its own, but it adds up. Across three client projects, that's several thousand yuan a year. Meanwhile, the server already had 200 GB of idle disk space, so everything was migrated to a self-hosted MinIO setup.
What is MinIO
MinIO is open-source object storage compatible with the Amazon S3 API. Deploy it on your server — a single Docker container. The code that previously called S3 now calls MinIO the same way — the API is identical.
Docker Deployment
services:
minio:
image: minio/minio:latest
container_name: minio
ports:
- "9000:9000" # API port
- "9001:9001" # Console port
volumes:
- ./minio/data:/data
- ./minio/config:/root/.minio
environment:
- MINIO_ROOT_USER=admin
- MINIO_ROOT_PASSWORD=your_password_here
command: server /data --console-address ":9001"
networks:
- aipdt-cloud
restart: unless-stopped
After starting, log into the console at http://your-ip:9001 and create buckets. At least two are recommended: public for publicly accessible files (like avatars), and private for confidential files (contracts, ID documents).
Spring Boot Integration
Only one dependency:
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.7</version>
</dependency>
Configuration:
minio:
endpoint: http://minio:9000 # Use the service name directly inside containers
access-key: admin
secret-key: your_password_here
bucket-public: public
bucket-private: private
Configuration class:
@Configuration
public class MinioConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.access-key}")
private String accessKey;
@Value("${minio.secret-key}")
private String secretKey;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
Utility Class
Encapsulate common operations in a utility class so business code can handle them in a single line:
@Slf4j
@Component
public class MinioUtil {
@Autowired
private MinioClient minioClient;
// Upload file
public String upload(String bucket, InputStream inputStream,
String fileName, long size) {
try {
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucket)
.object(fileName)
.stream(inputStream, size, -1)
.contentType(MediaType.APPLICATION_OCTET_STREAM_VALUE)
.build());
return fileName;
} catch (Exception e) {
log.error("Upload failed", e);
throw new RuntimeException("File upload failed");
}
}
// Get file stream
public InputStream download(String bucket, String fileName) {
try {
return minioClient.getObject(GetObjectArgs.builder()
.bucket(bucket)
.object(fileName)
.build());
} catch (Exception e) {
log.error("Download failed", e);
throw new RuntimeException("File download failed");
}
}
// Generate temporary access URL (valid for 7 days)
public String presignedUrl(String bucket, String fileName) {
try {
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.bucket(bucket)
.object(fileName)
.expiry(7, TimeUnit.DAYS)
.method(Method.GET)
.build());
} catch (Exception e) {
log.error("Failed to generate URL", e);
return null;
}
}
// Delete file
public void delete(String bucket, String fileName) {
try {
minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(bucket)
.object(fileName)
.build());
} catch (Exception e) {
log.error("Delete failed", e);
}
}
}
Business Code Examples
Upload avatar:
@PostMapping("/avatar")
public Result<String> uploadAvatar(@RequestParam MultipartFile file) {
String fileName = "avatar/" + userId + ".jpg";
minioUtil.upload(bucketPublic, file.getInputStream(),
fileName, file.getSize());
String url = minioUtil.presignedUrl(bucketPublic, fileName);
return Result.ok(url);
}
Export Excel to MinIO:
// Generate Excel
ByteArrayOutputStream out = new ByteArrayOutputStream();
excelWriter.write(out);
// Upload to MinIO
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
String fileName = "excel/" + UUID.randomUUID() + ".xlsx";
minioUtil.upload(bucketPrivate, in, fileName, out.size());
// Return download link
String downloadUrl = minioUtil.presignedUrl(bucketPrivate, fileName);
Access Policy
Public files: Avatars, cover images, etc. Set the bucket to public read:
mc policy set download myminio/public
Private files: Contracts, ID cards, etc. Keep the bucket private and control access via presigned URLs. The links carry an expiration time and signature, making them secure and controllable.
Pitfalls Encountered
1. Can't connect using localhost inside a container
When both MinIO and Spring Boot run in Docker, endpoint cannot use localhost. Use the container name http://minio:9000 or the host machine IP.
2. OOM when uploading large files
putObject reads the entire file into memory by default. Uploading files tens of MB in size will blow up memory. Switch to multipart upload:
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucket)
.object(fileName)
.stream(inputStream, -1, 5 * 1024 * 1024) // 5 MB part size
.build());
The third parameter -1 indicates unknown total size, and the fourth parameter 5M is the part size. This suits scenarios where the frontend uploads directly.
3. Domain issue with presigned URLs
MinIO generates links that default to http://internal-ip:9000/bucket/file. If the frontend needs to access them, the link must contain an external domain. Configure an nginx proxy:
location /minio/ {
proxy_pass http://minio:9000/;
}
Or specify the environment variable MINIO_SERVER_URL=https://your-domain when starting MinIO.
When MinIO Is Not Suitable
- CDN acceleration needed: OSS comes with CDN built in. MinIO requires adding an nginx reverse proxy and CDN configuration yourself.
- High availability is a hard requirement: A single machine going down means unavailability. MinIO does support cluster mode, but you need multiple servers.
- High compliance requirements: For example, Level 3 information security protection in China requires audit logs and cross-region backup for object storage. Self-hosting makes this troublesome.
If none of the above three conditions apply, MinIO is sufficient.
Summary
- MinIO replaces OSS, one-click Docker deployment, S3 API compatible.
- Two buckets — public and private — with public-read and presigned URL access modes.
- Use multipart upload for large files to avoid OOM.
- For small to medium projects without CDN or high-availability requirements, MinIO is fully adequate.
- Saves several thousand yuan per year in OSS fees while fully utilizing a single server's resources.
I am doing independent development in a "one person + AI" model. Search the WeChat mini-program "面狮狮" to experience AI-simulated interviews. More practical experience: https://gitee.com/yao113088/jiguang-dev
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
MinIO seems to be no longer maintained