Bila Deploy Laravel, Permission Bukan Masalah Kecil
Published:
Updated:
Ada satu benda yang agak annoying bila maintain Laravel application dekat VPS.
First deployment okay.
Everything works.
Laravel boleh write log.
Upload file boleh.
Storage boleh.
Cache boleh.
Then aku deploy perubahan baru.
Tiba-tiba:
Cannot append log. Permission denied
Laravel tak boleh write log.
File upload gagal.
Private file tak boleh disimpan.
Kadang-kadang benda yang sama berlaku dekat bootstrap/cache.
Lepas tu aku masuk server.
Check permission.
Betulkan.
Problem hilang.
Sampai next deployment.
Then repeat.
Pada awalnya aku anggap ini sebagai:
"Laravel storage permission problem."
Tapi lama-lama aku rasa framing tu salah.
Sebab kalau benda yang sama boleh berlaku tak kira aku deploy menggunakan Git bare, GitHub Actions, self-hosted runner, Docker atau platform macam Coolify, maksudnya deployment mechanism tu bukan root cause sebenar.
Ada benda yang lebih fundamental.
Sebenarnya ada dua process yang bekerja pada application yang sama
Bila kita cakap pasal Laravel application, kita selalu fikir:
Laravel
Tapi dekat server, Laravel bukan satu process.
Ada process yang deploy application.
Ada process yang run application.
Contohnya deployment mungkin dibuat oleh:
git bare
GitHub Actions
self-hosted runner
Docker
Coolify
SSH
Manakala application pula mungkin berjalan melalui:
Nginx
PHP-FPM
queue worker
scheduler
Dan process-process ni boleh menggunakan user atau group yang berbeza.
Contohnya:
deployment
↓
deploy user
runtime
↓
www-data
Sekarang bayangkan deployment create satu directory baru:
storage/something
Kalau directory tu dimiliki oleh deployment user dengan permission tertentu, kemudian PHP-FPM cuba write dekat situ sebagai www-data, Linux akan check:
User ni boleh write ke sini tak?
Kalau tak boleh:
Permission denied
Laravel cuma menjadi tempat error itu muncul.
Jadi aku mula tukar cara aku tengok deployment
Aku tak lagi fikir:
"Macam mana nak fix Laravel permission?"
Aku lebih suka tanya:
"Siapa yang akan create file ini, dan siapa yang akan gunakan file ini?"
Nampak macam soalan simple.
Tapi soalan ni sebenarnya membawa kita kepada banyak benda lain.
Siapa deploy?
Siapa run PHP?
Siapa run queue?
Siapa create uploaded file?
Siapa create log?
Siapa create cache?
Siapa perlu read?
Siapa perlu write?
Group apa yang mereka share?
Apa permission yang inherited bila file baru dicipta?
Bila mula tanya soalan macam ni, filesystem permission tak lagi nampak macam benda kecil.
Ia dah jadi sebahagian daripada architecture.
chmod 777 memang boleh "solve"
Dan inilah perangkap paling senang.
Bila Laravel tak boleh write:
chmod -R 777 storage
Problem hilang.
Of course.
Sebab kita basically cakap:
"Semua orang boleh buat apa saja."
😂
Tapi bagi aku, itu bukan solution.
Ia cuma menghilangkan constraint yang menyebabkan error.
Kita sebenarnya belum jawab:
Siapa yang sepatutnya boleh write?
Dan itu soalan yang lebih penting.
Aku lebih suka ada ownership model yang jelas
Contohnya, untuk satu Laravel application, aku boleh decide:
deployment user
deploy
runtime user
www-data
Kemudian aku boleh tentukan bahawa writable directory Laravel menggunakan shared group.
Contohnya:
storage/
deploy:www-data
bootstrap/cache/
deploy:www-data
Sekarang deployment process dan PHP-FPM mempunyai relationship yang jelas.
Bukan sekadar:
"chmod sampai tak error"
Tetapi:
deployment
↓
deploy:www-data
↓
shared writable directories
↓
PHP-FPM
Itu lebih predictable.
Di sinilah aku jumpa setgid
Satu detail Linux yang sebenarnya sangat sesuai untuk scenario macam ni ialah setgid pada directory.
Contohnya:
2775
Kebanyakan kita biasa nampak:
775
Tapi 2 dekat depan tu penting.
Ia bermaksud directory tersebut menggunakan setgid.
Salah satu behavior pentingnya ialah file atau directory yang dicipta di bawahnya akan inherit group daripada parent directory.
Contohnya:
storage/
deploy:www-data
2775
Bila sesuatu process create file baru dalam storage, group inheritance boleh kekal sebagai:
www-data
Jadi kita boleh bina satu filesystem model di mana deployment user dan runtime user boleh bekerja pada data yang sama tanpa setiap deployment merosakkan ownership yang diperlukan oleh runtime.
Bagi aku, ini bukan lagi sekadar:
"Oh, chmod 2775."
Ia sebenarnya:
Aku sedang define relationship antara deployment process dengan runtime process melalui filesystem.
Itu yang lebih menarik.
Tapi 2775 bukan bermaksud semua benda perlu writable
Ini pun penting.
Aku tak nak application code aku jadi writable secara membuta tuli.
Aku tak perlu:
app/
config/
routes/
resources/
vendor/
menjadi writable kepada runtime.
Laravel cuma perlukan tempat tertentu untuk write.
Biasanya:
storage/
bootstrap/cache/
Jadi permission model aku patut reflect requirement sebenar application.
Bukan:
everything writable
Tetapi:
code
mostly read-only
runtime directories
writable
Benda ni nampak kecil.
Tapi sebenarnya ini adalah principle of least privilege yang sama kita gunakan dalam security design.
Bagi permission hanya dekat tempat yang memerlukannya.
Lagi aku fikir, lagi aku rasa storage patut dipisahkan daripada deployment
Ini mungkin lebih penting daripada permission itu sendiri.
Application code boleh dibuang.
Kita boleh deploy version baru.
Kita boleh rollback.
Kita boleh rebuild container.
Kita boleh checkout commit lain.
Tetapi uploaded files?
Itu data.
Kalau user upload:
invoice.pdf
profile.jpg
medical-report.pdf
document.pdf
file-file tersebut tak sepatutnya bergantung kepada lifecycle application code.
Sebab itu aku suka architecture yang lebih kurang macam:
/var/www/myapp
├── releases/
│ ├── release-001/
│ ├── release-002/
│ └── release-003/
│
├── current
│
└── shared/
└── storage/
Application release boleh berubah.
Tetapi:
shared/storage
kekal.
Jadi deployment tak perlu kacau persistent data setiap kali code berubah.
Ini juga explain kenapa deployment technology bukan penyelesaian
Kita boleh tukar deployment mechanism.
Contohnya:
Git bare
tak semestinya lebih baik daripada:
GitHub Actions
Dan GitHub Actions pula tak semestinya menghapuskan masalah berbanding:
Docker
Docker pula tak semestinya membuat filesystem permission menjadi irrelevant.
Malah Docker boleh memperkenalkan satu lagi dimension:
UID
GID
container user
host user
volume
bind mount
Kalau host create file dengan UID tertentu tetapi container run sebagai UID lain, kita masih boleh dapat:
Permission denied
Sebab akhirnya semua ni masih berinteraksi dengan satu benda:
Linux filesystem.
Deployment tool cuma menentukan bagaimana kita sampai ke filesystem tersebut.
Sebab itu aku suka fikir dalam bentuk "contract"
Daripada bergantung kepada:
"Setup server ni sekali dan harap tak rosak."
Aku lebih suka ada filesystem contract.
Contohnya:
Application
owner/group → deploy:www-data
Runtime writable directories
storage/
bootstrap/cache/
Directory permission
2775
File permission
664
Persistent storage
separate from release
Sekarang bila aku tukar deployment mechanism, aku cuma perlu pastikan mechanism tersebut memenuhi contract ini.
Tak kisah sama ada:
Git bare
GitHub Actions
Docker
Coolify
self-hosted runner
Implementation boleh berubah.
Contract kekal.
Ini sebenarnya pattern yang lebih besar
Benda ni mengingatkan aku bahawa banyak masalah production sebenarnya bukan masalah technology.
Kita selalu cuba solve berdasarkan tool:
"Laravel error."
"Docker problem."
"Nginx problem."
"Git deployment problem."
"Coolify problem."
Padahal kadang-kadang semua tu cuma symptom.
Masalah sebenar ialah kita tak define interaction antara components dengan cukup jelas.
Dalam kes ni:
Deployment
↓
Application files
↓
Runtime
↓
Persistent data
Siapa own apa?
Siapa boleh read?
Siapa boleh write?
Apa yang persistent?
Apa yang disposable?
Apa yang berlaku bila deployment baru replace files?
Apa yang berlaku bila application create file baru?
Bila soalan-soalan ni dijawab awal, banyak "random production issue" mula jadi predictable.
Jadi sekarang, bila aku setup Laravel dekat VPS...
Aku tak anggap permission sebagai benda yang akan aku fix nanti.
Aku anggap ia sebahagian daripada setup architecture.
Aku nak tahu:
Who deploys?
Who runs the application?
Who writes runtime data?
Which directories are writable?
Which data must survive deployment?
Which users/groups need access?
How does newly-created data inherit ownership?
Dan baru daripada situ aku decide:
ownership
groups
setgid
permissions
directory structure
deployment process
Bukan sebaliknya.
Bukan:
Error
↓
chmod
↓
Error hilang
↓
Done
Tetapi:
Requirements
↓
Ownership model
↓
Filesystem permissions
↓
Deployment architecture
↓
Predictable runtime
Bagi aku, itu beza antara "server setup" dengan "production architecture."
Sebab production yang baik bukan production yang tak pernah ada error.
Production yang baik ialah bila kita boleh explain kenapa system berkelakuan macam itu, dan bila deployment berubah, kita masih tahu apa yang patut berlaku.
Dan untuk sesuatu yang nampak kecil macam Laravel storage permission, sebenarnya ada banyak architecture decision yang duduk di belakangnya.
