Hello! Come and sit for a moment. Today I want to discuss something that often becomes a stumbling block for developers who are just transitioning from creating small scale applications to medium scale applications or enterprise.
We will discuss Object Storage, specifically MinIO, and why from now on you have to change the way you think about how applications store files.
Do Not Save All Files on the Server
Imagine this scenario: You've just deployed your first application. There is a profile photo and document upload feature. As the quickest way, you create a
/var/www/uploads folder on the server, then direct your application code to save all the user's upload files there.At first it went smoothly. But a few months later, your app explodes and the traffic skyrockets. What happened?
-
Server Full: Your disk space is running out. The application crashes because it cannot write logs.
-
Scaling Becomes a Nightmare: Due to high traffic, you add one more server (Server B) behind the Load Balancer. However, if user A upload's profile photo and request log in to Server A, the photo will be saved on Server A. When user A refresh the page and happens to be redirected to Server B, the photo is gone! The files are out of sync.
-
Docker Ephemeral: You start using Docker. You redeploy the container, and boom! All user upload files are lost due to the ephemeral (temporary) nature of containers.
-
Hard Backup: Every time you want to backup source code, you have to filter the uploads whose size is hundreds of Gigabytes. Application files and user files are mixed up.
Understand the problem, right? Local filesystem is great for development, but becomes a disaster when the application starts scale. We need a separate place that specifically handles files. That's where we get acquainted with Object Storage.
What is Object Storage?
Let's get the basic concept straight first. If you usually save files on Windows or Mac, you use Filesystem.
A filesystem is like a wardrobe. You have shelves (directories), boxes within the shelves (sub-directories), and clothes within them (files). If you want to look for clothes, you have to know the hierarchy:
Cupboard> Shelf 2> Red Box> Clothes.jpg.Object storage doesn't work like that. Object storage is like a giant flat warehouse.
There are no actual folders or sub-folders. Every item (file) that enters this warehouse is immediately given a unique label, the contents of the item, and notes about the item.
In Object Storage, each file is called a Object, and has three things:
-
Key: The unique name of the identifier (example:
2026/08/baju.jpg). -
Data: The contents of the file itself (binary).
-
Metadata: Additional information about the file (example: uploaded by whom, what size, what format).
These objects are stored in a large container called Bucket.
Plaintext
Bucket: app-uploads
├── object-1 (Key: user-1-profile.jpg)
├── object-2 (Key: invoice-001.pdf)
└── object-3 (Key: backup.zip)
Remember: Object storage is not a database. Databases structure textual and relational data. Object storage stores massive amounts of files/binary with a highly scalable architecture.
What is MinIO?
Now go to the tool. MinIO is one of the Object Storage software that is open-source and High Performance.
Not just a "file store", MinIO is designed to handle any static file: image, PDF, video, document, backup databases, log systems, to Machine Learning (AI) models.
Why do senior engineers really like using MinIO?
-
Self-hosted: You can install MinIO on your own VPS. You don't have to pay a fortune to cloud provider if budget is still limited.
-
S3-Compatible: (This is the most important thing, I discuss it specifically in the next point).
-
Very Light & Fast: Written in Go, runs on Docker very smoothly.
-
Great for Kubernetes: MinIO is a first-class citizen in the Cloud-Native ecosystem.
Understanding the "S3-Compatible" Concept
You must have heard of Amazon S3 (Simple Storage Service). S3 is the industry standard for object storage on AWS.
Well, because S3 is so popular, the API (how apps communicate with S3) is becoming the de factostandard. Think of this S3 API as a "Language".
MinIO is S3-Compatible. This means that MinIO "speaks" exactly the same language as Amazon S3.
Why is this amazing? Because if your application has been programmed using the Amazon S3 SDK/Library (speaking S3 language), your application can be directly connected to MinIO without the need to change the application code significantly. You only need to change the Endpoint URL and credentials.
Plaintext
Application (Using S3 SDK)
|
| Speaking "S3 Language"
v
┌───────────┐
| S3 APIs |
└─┬───────┬─┘
| |
Can go to or to
v v
MinIO Amazon S3
This abstraction keeps us free from vendor lock-in. Start development using MinIO on local, when production if there is budget just switch to AWS S3, or keep scale MinIO on the server itself. The code? Exactly the same!
Important MinIO Components
So that you don't get confused when you see dashboard or the documentation, understand these terms:
-
Server: The machine or container on which MinIO runs.
-
Client: Your application (Backend API) that request to save/retrieve files.
-
Bucket: Primary container. Typically one application is split per bucket (e.g.
bucket-staging,bucket-prod). -
Object: The file you saved (photo, pdf).
-
Object Key: Unique string to identify the object.
-
Metadata: Data about data (example: Content-Type: image/jpeg).
-
Access Key & Secret Key: Username and Password for the API. Never leak!
-
Endpoint: URL where the MinIO server can be accessed (eg:
http://localhost:9000). -
Policy: Rules about who can read/write to a particular bucket.
Important note about Object Key:
Even though you see a structure like this in dashboard MinIO:
Plaintext
users/
123/
profile.jpg
That NOT the
users folder which contains the 123 folder.Object storage is flat. The key is literally a long string:
"users/123/profile.jpg". The slash / is just a UI visualization to make it easier for humans to read.MinIO vs Local Filesystem
When to use what? Take a look at this comparison:
| Aspect | Local Filesystem (/var/www) | MinIO (Object Storage) |
| Deployment | Very easy (just go) | Requires initial setup (Docker/Server) |
| Multi-Server | Difficult (requires NFS/rsync) | High fit, centered |
| S3 API | None | Yes, industry standard |
| Docker/K8s | Prone to data loss (requires complex volume mapping) | Very suitable (Stateless App) |
| Metadata File | Limited (from OS) | Rich (can be custom metadata) |
| Presigned URL | None (native) | Available |
When does local filesystem still make sense?
If your application is just a small internal tool, running on one cheap VPS server, it will never scale to two servers, and it needs setup 5 minutes. Don't over-engineering if you don't need to.
But for serious modern applications? Required Object Storage.
Application Architecture with MinIO
If your application uses MinIO, this is the standard architecture.
Plaintext
User
|
v
Frontend (Web/Mobile)
|
v
Backend API (Laravel/Go/Node.js)
|
+------------------> PostgreSQL (Saving Metadata/References)
|
+------------------> MinIO (Save the Physical File)
Golden Rule: Database only stores information (references) about files. The binary file (binary) goes to MinIO.
Example row in PostgreSQL (
table users):-
id:123 -
name:Budi -
avatar_path:avatars/users/123/profile-xyz.jpg(This is the Object Key!)
MinIO stores its files in key
avatars/users/123/profile-xyz.jpg.File Upload: What Actually Happens?
Many juniors misunderstand how upload works. The flow should be like this:
-
Frontend sends image to Backend API.
-
Backend API validate (size is OK? Is the format really an image?).
-
Backend generates a unique Object Key (eg using UUID).
-
Backend sends files to MinIO using S3 Client.
-
MinIO responds "OK".
-
The backend saves the Object Key to the Database (PostgreSQL).
-
Backend replies to Frontend "Upload Successful".
Why do you have to rename your real name?
Never use the original file name! Imagine there are 3 different users uploading files with the name
profile.jpg.If you use the original name as the Object Key, user 1's file will be overwritten (overwrite) by user 2's file, and again by user 3.
Always generate UUID:
users/123/profile/f47ac10b-58cc.jpg.Download and Access File
To display files to the user, there are three ways:
-
Bucket Public: Files can be accessed by anyone via the MinIO URL. Suitable for web assets (logo, banner).
-
Backend as Proxy: Frontend request to Backend, Backend pulls from MinIO, then Backend gives it to Frontend. (Safe, but burdens Backend server RAM/Bandwidth).
-
Presigned URL (Most Recommended):
What is a Presigned URL?
This is a magic URL generated by your Backend, which grants temporary permission (e.g. 15 minutes) for anyone with that URL to download (or upload) one specific file from MinIO directly, without burdening the Backend.
Plaintext
User request file -> Access rights verification backend ->
Backend generate Presigned URL (without file download) ->
Users download directly from MinIO using this URL.
Ini sangat krusial untuk file private seperti KTP atau invoice.
Integrasi MinIO dengan Laravel
Laravel punya filesystem abstraction yang luar biasa. Kamu tinggal atur
.env:Cuplikan kode
FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=minioadmin
AWS_SECRET_ACCESS_KEY=minioadmin
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=app-storage
AWS_ENDPOINT=http://localhost:9000
AWS_USE_PATH_STYLE_ENDPOINT=true
Catatan:
AWS_USE_PATH_STYLE_ENDPOINT=true wajib untuk MinIO karena secara default MinIO tidak menggunakan subdomain style seperti AWS S3.Di kode Controller-mu, ini sesederhana:
PHP
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
// Generate nama unik
$fileName = Str::uuid() . '.' . $request->file('avatar')->getClientOriginalExtension();
$objectKey = 'users/' . $userId . '/' . $fileName;
// Upload ke MinIO (S3 Disk)
Storage::disk('s3')->put(
$objectKey,
file_get_contents($request->file('avatar'))
);
// Simpan $objectKey ke Database...
Lihat betapa bersihnya? Kode ini tidak peduli di belakangnya MinIO atau AWS S3. Abstraksinya jalan sempurna.
Integrasi MinIO dengan Golang
Kalau kamu pakai Go, kamu bisa pakai official MinIO SDK atau AWS SDK (karena S3 compatible). Ini contoh pakai
minio-go/v7:Go
package main
import (
"context"
"log"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
func main() {
endpoint := "localhost:9000"
accessKeyID := "minioadmin"
secretAccessKey := "minioadmin"
// Init S3 Client
minioClient, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
Secure: false, // true untuk HTTPS (production)
})
if err != nil {
log.Fatalln(err)
}
// Upload Object
bucketName := "app-storage"
objectName := "documents/report-xyz.pdf"
filePath := "/tmp/report-xyz.pdf"
info, err := minioClient.FPutObject(context.Background(), bucketName, objectName, filePath, minio.PutObjectOptions{ContentType: "application/pdf"})
if err != nil {
log.Fatalln(err)
}
log.Printf("Successfully uploaded %s of size %d\n", objectName, info.Size)
}
Sangat terstruktur. Kamu bikin Client, lalu gunakan method
FPutObject atau PutObject untuk melempar file-nya.MinIO dengan Docker
Di development, kita biasanya menjalankan MinIO pakai Docker. Ini contoh
docker-compose.yml yang standar:YAML
version: '3.8'
services:
minio:
image: minio/minio
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
ports:
- "9000:9000" # API Port
- "9001:9001" # Console/Web UI Port
volumes:
- minio_data:/data # INI SANGAT PENTING!
volumes:
minio_data:
Bedah baris penting:
-
port 9000: Ini port API. Aplikasimu (Laravel/Go) nembak ke sini. -
port 9001: Ini Web UI/Console. Kamu bisa buka di browser untuk lihat-lihat bucket secara visual. -
volumes: minio_data:/data: Tanpa ini, setiap kali container MinIO mati atau di-restart, semua file upload kamu HILANG. Persistent volume memastikan file tetap ada secara fisik di host machine.
MinIO dalam Production
MinIO di localhost dan MinIO production adalah dua konteks yang sama sekali berbeda! Di production, kamu tidak bisa asal docker up lalu ditinggal tidur.
Kamu harus memikirkan:
-
HTTPS: API S3 wajib pakai SSL/TLS.
-
Access Management: Jangan pakai akun root (
MINIO_ROOT_USER) di aplikasi. Buat User khusus di MinIO yang policy-nya hanya bisa Read/Write ke bucket tertentu (Least Privilege). -
Persistent Disk: Gunakan block storage yang reliabel, dengan konfigurasi RAID atau ter-backup berkala.
-
High Availability: Di production skala besar, MinIO di-deploy dalam mode Distributed (minimal 4 node) agar kalau 1 server mati, file tetap aman (Disaster Recovery).
MinIO + Nginx
Biar aman dan rapi, MinIO di production biasanya disembunyikan di belakang Reverse Proxy seperti Nginx.
Plaintext
Internet
| (HTTPS)
v
Nginx (Termination SSL)
|
+----> api.example.com (Ke Backend Laravel/Go)
|
+----> storage.example.com (Ke MinIO port 9000)
|
+----> console.storage.example.com (Ke MinIO UI port 9001)
Nginx yang akan memegang SSL Certificate (misal dari Let's Encrypt). Komunikasi dari Nginx ke MinIO di internal network bisa menggunakan HTTP biasa. Ini membuat manajemen domain dan sertifikat jauh lebih mudah.
Security (Keamanan)
Ini area di mana banyak yang kecolongan.
-
Jangan Expose Secret Key: Secret Key ibarat password database. Jangan taruh di Frontend (React/Vue/Mobile App). Komunikasi ke MinIO harus selalu lewat Backend-mu, atau lewat Presigned URL yang di-generate Backend.
-
Jangan Hardcode Credential: Selalu gunakan Environment Variable (
.env). -
Batasi Bucket Policy: Jangan buat semua bucket "Public". Bucket KTP/Invoice harus "Private".
-
Validasi File di Backend: Jangan percaya request upload user. Cek MIME type-nya, batasi ukurannya (misal maksimal 5MB). File berbahaya (seperti
.phpatau.exepalsu) bisa merusak sistem jika tidak difilter.
Kesalahan yang Sering Dilakukan Junior Developer
Sebagai mentor, ini hal-hal yang sering saya temui saat melakukan Code Review:
-
Menyimpan binary file besar langsung di PostgreSQL: (BLOB). Kenapa salah? DB jadi super berat, backup jadi lama, memory habis. Solusi: Simpan file di MinIO, simpan URL/Key di DB.
-
Menggunakan nama file asli sebagai Object Key:
profile_pic.jpg. Dampaknya: File ketimpa sama user lain. Solusi: Pakai UUID. -
Membuat bucket KTP menjadi public: Dampaknya: Data bocor, aplikasi masuk berita (kasus kebocoran data). Solusi: Bucket private + Presigned URL.
-
Menaruh Access Key MinIO Production di Git/Github: Dampaknya: Hacker bot akan scan repo-mu, masuk ke MinIO kamu, hapus semua datamu, dan minta tebusan ransomware. Solusi: Selalu ignore
.env. -
Tidak memikirkan ukuran file: User upload video 1 GB langsung ke RAM backend sebelum di-forward ke MinIO. Dampaknya: Server langsung Out of Memory. Solusi: Kasih batasan upload size di API, atau gunakan Presigned Upload URL agar user langsung upload ke MinIO.
Database Schema untuk File Storage
Walaupun file disimpan di MinIO, database tetap raja untuk manajemen metadata. Biasanya saya membuat satu tabel khusus untuk menyimpan record semua file:
SQL
CREATE TABLE files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
original_name VARCHAR(255) NOT NULL,
object_key TEXT NOT NULL,
bucket VARCHAR(100) NOT NULL,
mime_type VARCHAR(100),
size_bytes BIGINT,
uploaded_by UUID REFERENCES users(id),
created_at TIMESTAMP DEFAULT NOW()
);
Dengan skema ini, hubungan relasionalmu aman. Kalau table
users butuh foto profil, tinggal beri foreign key profile_file_id yang me-refer ke tabel files.Studi Kasus: Mini Bank Application
Mari kita rangkum pemahaman kita dalam kasus nyata. Kamu membuat aplikasi Mini Bank.
Aplikasi ini punya fitur KYC (Verifikasi identitas) yang butuh foto KTP dan foto Selfie, serta fitur download bukti transfer (receipt).
Arsitektur:
Plaintext
┌──────────────┐
│ React App │
└──────┬───────┘
│ (Upload KTP)
v
┌──────────────┐
│ Go API │ (Verifikasi Ukuran/Tipe)
└──────┬───────┘
│
(1) Save Data │ (2) PutObject
v
┌──────────────┐ ┌──────────────┐
│ PostgreSQL │ │ MinIO │
└──────────────┘ └──────────────┘
Alur KYC:
-
User upload KTP (gambar) lewat React.
-
Go API menerima. Generate UUID:
kyc-documents/user-99/ktp-ab12cd.jpg. -
Go API melempar file tersebut ke MinIO ke bucket
bank-private. -
Go API menyimpan
object_keytersebut ke PostgreSQL di tabeluser_kyc. -
Saat tim admin bank mau lihat KTP tersebut lewat internal dashboard, Go API men-generate Presigned URL yang valid selama 30 menit dan mengirimkannya ke dashboard.
Tidak ada satu pun file KTP yang bocor ke publik. Sangat rapi.
MinIO + Kubernetes
Ketika aplikasimu sudah sangat masif dan masuk ke Kubernetes (K8s), MinIO juga bisa ikut masuk.
Di Kubernetes, aplikasi API kita (Laravel/Go) bersifat Stateless (kalau di-restart tidak masalah). Tapi MinIO adalah Stateful workload. File tidak boleh hilang.
Oleh karena itu, di K8s, MinIO membutuhkan:
-
Persistent Volume (PV) & Persistent Volume Claim (PVC): Ini untuk meminta "Hard disk" permanen dari cloud provider (misal AWS EBS atau GCP Persistent Disk) agar menempel ke Pod MinIO.
-
StatefulSet: MinIO di-deploy bukan sebagai Deployment biasa, melainkan StatefulSet agar network identity dan urutan disk-nya terjaga.
Pemahaman dasarnya: K8s mengatur container-nya, tapi storage fisiknya tetap dijaga secara permanen di luar siklus hidup Pod.
Kapan Menggunakan MinIO dan Kapan Tidak?
Gunakan MinIO jika:
-
Kamu membutuhkan abstraksi S3 API untuk fleksibilitas di masa depan.
-
Kamu butuh fitur Presigned URL untuk keamanan file private.
-
Kamu ingin self-host object storage di VPS karena biaya AWS S3 terlalu mahal untuk bandwidth aplikasimu.
-
Aplikasimu di-deploy menggunakan Docker atau Kubernetes (arsitektur stateless).
-
Kamu berurusan dengan banyak aset statis/biner (gambar, PDF, video).
Pertimbangkan alternatif (seperti local filesystem biasa) jika:
-
Aplikasimu hanya blog WordPress sederhana atau web profil perusahaan yang statis.
-
Kamu hanya perlu menyimpan 10-20 gambar kecil.
-
Kamu menghindari operational complexity (tidak mau pusing me-maintain container tambahan).
Mental Model yang Harus Dipahami Junior
Sebagai penutup teknis, saya ingin meluruskan "Mental Model"-mu.
Jangan berpikir MinIO sebagai folder online.
MinIO (dan Object Storage pada umumnya) adalah sebuah Service / Layanan berbasis API.
Kamu tidak "menyalin (copy)" file ke MinIO seperti kamu memindahkan file ke Flashdisk. Kamu berkomunikasi dengan HTTP API (lewat S3 SDK) untuk me-request agar layanan MinIO membuat, mengambil, atau menghapus sebuah Object.
Semuanya API-driven.
Plaintext
Application ---> HTTP (S3 Protocol) ---> Object Storage (MinIO)
|
+--- Bucket (Namespace)
+--- Object (File)
+--- Metadata
+--- Policy (Izin Akses)
Best Practice Checklist
Saat kamu mau mengimplementasikan MinIO di proyekmu berikutnya, cek daftar ini:
-
[ ] Bucket strategy sudah jelas (mana yang public, mana yang private).
-
[ ] Object key menggunakan UUID atau identifier unik, tidak pernah original file name.
-
[ ] Kredensial (Access/Secret Key) tidak di-hardcode di source code, melainkan pakai
.env. -
[ ] HTTPS/SSL diaktifkan di sisi MinIO atau melalui Nginx (Production).
-
[ ] Database men-tracking informasi object key dan bucket.
-
[ ] Akses file private secara ketat menggunakan Presigned URL.
-
[ ] Menggunakan Persistent Volume di Docker/K8s.
-
[ ] Ukuran upload maksimal (file size limit) diaktifkan di Backend/Reverse Proxy.
-
[ ] MIME type dan ektensi file divalidasi dengan ketat di Backend sebelum diteruskan ke MinIO.
-
[ ] Menggunakan akun IAM / User Policy khusus (bukan root user) untuk aplikasi.
Kesimpulan
Nah, sekarang kamu sudah paham gambar besarnya. Object storage seperti MinIO bukan sekadar teknologi keren-kerenan. Ini adalah komponen fundamental dari pemisahan tanggung jawab (Separation of Concerns) dalam arsitektur modern.
-
Application Server (Laravel/Go) khusus menangani komputasi dan business logic.
-
Database (PostgreSQL/MySQL) khusus menangani data terstruktur dan relasional.
-
Object Storage (MinIO/S3) khusus menangani bongkahan file binary (gambar, dokumen).
Dengan memecah beban seperti ini, kamu bisa men-scale ketiga komponen tersebut secara terpisah dan independen seiring berjalannya waktu.
Jadi, jangan simpan file di
/var/www/uploads lagi, ya? Paham kan? Yep! Selamat ngoding!
Sigit Wasis Subekti
Software Engineer & Tech Educator
Software Engineer and Tech Educator sharing insights on web development and software architecture.