Studi Kasus: Aplikasi Laravel

Studi Kasus: Aplikasi Laravel

Bitnesia Sep 12, 2026 9 EN

Laravel jadi salah satu framework PHP paling populer, dan containerizing aplikasi Laravel punya karakteristik yang agak berbeda dibanding aplikasi Node.js atau Go, karena PHP secara tradisional butuh dua komponen yang berjalan terpisah: web server yang menerima request HTTP, dan PHP process manager yang mengeksekusi kode PHP-nya. Selain itu, aplikasi Laravel production-ready umumnya tidak cuma butuh satu container untuk melayani HTTP, tapi juga container terpisah untuk queue worker dan scheduler yang berjalan di latar belakang. Bab ini membahas studi kasus penuh: dari menyusun Dockerfile untuk PHP-FPM dan konfigurasi Nginx sebagai reverse proxy, menyambungkan aplikasi ke database MySQL atau PostgreSQL, menjalankan queue worker dan scheduler dalam container terpisah, sampai mengelola environment configuration dan artisan command yang relevan untuk development maupun production.

29.1 Containerizing Laravel dengan PHP-FPM dan Nginx

Berbeda dengan aplikasi Node.js atau Go yang punya built-in HTTP server, PHP-FPM cuma bertugas mengeksekusi skrip PHP dan tidak bisa langsung menerima request HTTP dari browser. Karena itu, containerizing Laravel butuh kombinasi dua image: satu untuk PHP-FPM yang menjalankan kode aplikasi, satu lagi untuk Nginx yang meneruskan request ke PHP-FPM lewat protokol FastCGI.

29.1.1 Struktur Project dan Kebutuhan Container

Studi kasus di bab ini memakai project Laravel dengan struktur folder standar bawaan laravel new, ditambah beberapa file konfigurasi Docker.

myapp/
├── app/
├── bootstrap/
├── config/
├── database/
├── public/
│   └── index.php
├── routes/
├── storage/
├── docker/
│   ├── php/
│   │   └── php.ini
│   ├── nginx/
│   │   └── default.conf
│   └── supervisor/
│       └── supervisord.conf
├── composer.json
├── composer.lock
├── artisan
├── .env.example
└── Dockerfile

Folder public berisi index.php sebagai entry point aplikasi, dan folder inilah yang nanti dijadikan document root oleh Nginx. Folder docker menampung file konfigurasi tambahan yang dibutuhkan image, dipisahkan dari source code aplikasi supaya lebih mudah dikelola.

29.1.2 Dockerfile Multi-Stage untuk PHP-FPM

Instalasi dependency PHP lewat Composer dan proses build aplikasi sebaiknya dipisahkan dari image final, sama seperti pola multi-stage build pada studi kasus containerizing aplikasi lain. Dockerfile berikut memakai image resmi composer untuk instalasi dependency, lalu base image resmi php varian fpm-alpine untuk menjalankan aplikasinya.

FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
    --no-dev \
    --no-scripts \
    --no-autoloader \
    --prefer-dist \
    --no-interaction

COPY . .
RUN composer dump-autoload --optimize --no-dev

FROM php:8.3-fpm-alpine AS production
WORKDIR /var/www/html

RUN apk add --no-cache \
    libpng-dev \
    libzip-dev \
    oniguruma-dev \
    && docker-php-ext-install pdo_mysql mbstring zip gd bcmath

COPY docker/php/php.ini /usr/local/etc/php/conf.d/laravel.ini
COPY --from=vendor /app /var/www/html

RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache

EXPOSE 9000
CMD ["php-fpm"]

Opsi --no-scripts dan --no-autoloader pada tahap composer install sengaja dipakai supaya autoload file baru dibuat setelah seluruh source code aplikasi ikut disalin lewat COPY . ., karena beberapa package Laravel punya script post-install yang butuh keberadaan file aplikasi secara utuh, bukan cuma composer.json. Instruksi docker-php-ext-install dipakai untuk mengaktifkan extension PHP yang dibutuhkan Laravel, seperti pdo_mysql untuk koneksi database MySQL, sesuai mekanisme resmi yang disediakan base image PHP untuk mengaktifkan extension tanpa perlu kompilasi manual dari awal. Khusus extension gd, instalasi lewat docker-php-ext-install saja cuma mengaktifkan dukungan PNG dasar. Kalau aplikasi butuh memproses format JPEG atau WebP, tambahkan docker-php-ext-configure gd --with-jpeg --with-webp sebelum docker-php-ext-install gd supaya library pendukungnya ikut dikompilasi.

Kepemilikan folder storage dan bootstrap/cache diubah ke user www-data karena Laravel menulis file log, cache, dan session ke kedua folder tersebut saat runtime. Kalau kepemilikan folder ini masih milik root sementara proses PHP-FPM berjalan sebagai www-data, aplikasi akan gagal menulis file dan memunculkan error Permission denied begitu ada request masuk.

File docker/php/php.ini yang ikut disalin ke /usr/local/etc/php/conf.d/laravel.ini berisi override untuk beberapa nilai konfigurasi PHP bawaan yang defaultnya terlalu kecil untuk kebutuhan aplikasi web pada umumnya, misalnya batas ukuran upload file dan batas memori tiap request.

upload_max_filesize = 20M
post_max_size = 20M
memory_limit = 256M
max_execution_time = 60

Base image resmi PHP memuat seluruh file .ini di folder conf.d secara otomatis saat proses PHP dijalankan, sehingga file konfigurasi tambahan ini tidak perlu didaftarkan secara manual di tempat lain. Verifikasi nilai yang aktif lewat php -i di dalam container.

docker compose exec app php -i | grep upload_max_filesize

29.1.3 Konfigurasi Nginx sebagai Reverse Proxy

Nginx bertugas menerima request HTTP dari luar, menyajikan static file secara langsung, dan meneruskan request untuk file PHP ke container PHP-FPM lewat FastCGI. Buat file docker/nginx/default.conf berikut.

server {
    listen 80;
    server_name _;
    root /var/www/html/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass app:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

Baris fastcgi_pass app:9000; mengarah ke nama service app pada port 9000, port default tempat PHP-FPM mendengarkan koneksi FastCGI. Nama app ini nanti disesuaikan dengan nama service PHP-FPM yang didefinisikan di compose.yaml, karena Docker Compose menyediakan DNS resolution antar-service berdasarkan nama tersebut. Blok location ~ /\.(?!well-known).* memblokir akses langsung ke file tersembunyi seperti .env lewat HTTP, langkah keamanan dasar yang mencegah Attacker mengakses file konfigurasi sensitif kalau file tersebut tidak sengaja tersimpan di dalam public.

Definisikan service app dan web dalam satu file compose.yaml, dengan keduanya berbagi kode aplikasi lewat named volume.

services:
  app:
    build:
      context: .
      target: production
    volumes:
      - app-storage:/var/www/html/storage
    environment:
      APP_ENV: production

  web:
    image: nginx:1.27-alpine
    ports:
      - "8080:80"
    volumes:
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - app

volumes:
  app-storage:

Perhatikan bahwa service web tidak menyalin source code aplikasi sama sekali, cuma butuh file konfigurasi Nginx-nya. Ini berbeda dengan pendekatan yang menyimpan source code di kedua container lewat bind mount bersama, sesuatu yang lazim dipakai untuk development tapi kurang cocok untuk production karena membuat dua image saling bergantung pada isi folder host yang sama.

29.1.4 Build dan Verifikasi

Build kedua service dan jalankan dalam mode detached.

docker compose up -d --build

Akses aplikasi lewat port yang sudah dipetakan di service web.

curl -I http://localhost:8080

Kalau muncul response 502 Bad Gateway, penyebab paling umum adalah Nginx belum bisa menjangkau PHP-FPM, biasanya karena nama service di fastcgi_pass tidak cocok dengan nama service PHP-FPM di compose.yaml, atau container app belum selesai start saat web mencoba meneruskan request pertama. Periksa log kedua service untuk memastikan penyebabnya.

docker compose logs app
docker compose logs web

Error 500 Internal Server Error yang muncul setelah koneksi FastCGI berhasil biasanya berasal dari sisi Laravel sendiri, misalnya application key belum di-generate atau folder storage belum bisa ditulis. Masuk ke dalam container app untuk memeriksa log Laravel secara langsung.

docker compose exec app tail -n 50 storage/logs/laravel.log

29.2 Setup MySQL/PostgreSQL untuk Laravel

Laravel mendukung banyak database driver lewat abstraksi Eloquent dan query builder, tapi MySQL dan PostgreSQL jadi dua pilihan paling umum dipakai di production. Bagian ini menunjukkan cara menyambungkan container Laravel ke kedua database tersebut lewat Docker Compose.

29.2.1 Service Database di Docker Compose

Tambahkan service db memakai image resmi mysql ke compose.yaml yang sudah dibuat sebelumnya.

services:
  app:
    build:
      context: .
      target: production
    volumes:
      - app-storage:/var/www/html/storage
    environment:
      APP_ENV: production
      DB_CONNECTION: mysql
      DB_HOST: db
      DB_PORT: 3306
      DB_DATABASE: myapp
      DB_USERNAME: myapp
      DB_PASSWORD: secret
    depends_on:
      db:
        condition: service_healthy

  db:
    image: mysql:8.4
    environment:
      MYSQL_DATABASE: myapp
      MYSQL_USER: myapp
      MYSQL_PASSWORD: secret
      MYSQL_ROOT_PASSWORD: rootsecret
    volumes:
      - db-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "myapp", "-psecret"]
      interval: 5s
      timeout: 3s
      retries: 10

  web:
    image: nginx:1.27-alpine
    ports:
      - "8080:80"
    volumes:
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - app

volumes:
  app-storage:
  db-data:

Kalau memilih PostgreSQL sebagai gantinya, ganti image mysql:8.4 menjadi postgres:16-alpine, sesuaikan environment variable-nya menjadi POSTGRES_DB, POSTGRES_USER, dan POSTGRES_PASSWORD, lalu ubah DB_CONNECTION pada service app menjadi pgsql dengan DB_PORT: 5432. Opsi depends_on dengan condition: service_healthy memastikan Laravel baru mencoba konek setelah database benar-benar siap menerima koneksi, bukan cuma setelah container database menyala, karena proses inisialisasi MySQL maupun PostgreSQL saat pertama kali dijalankan butuh waktu tambahan sebelum siap melayani koneksi.

Perlu diperhatikan bahwa password pada opsi -p di perintah mysqladmin untuk health check ini muncul dalam bentuk teks polos kalau dilihat lewat docker inspect atau proses yang berjalan di dalam container. Untuk lingkungan production yang lebih ketat soal keamanan, Sysadmin/DevOps Engineer sebaiknya memakai mekanisme secrets management milik orchestrator yang dipakai, bukan menuliskan password langsung di health check seperti pada contoh ini.

29.2.2 Koneksi Database via .env dan config/database.php

Laravel membaca kredensial database dari environment variable, bukan langsung dari config/database.php. File konfigurasi tersebut cuma berfungsi sebagai jembatan yang memanggil fungsi env() untuk mengambil nilai dari environment.

'mysql' => [
    'driver' => 'mysql',
    'host' => env('DB_HOST', '127.0.0.1'),
    'port' => env('DB_PORT', '3306'),
    'database' => env('DB_DATABASE', 'laravel'),
    'username' => env('DB_USERNAME', 'root'),
    'password' => env('DB_PASSWORD', ''),
],

Nilai DB_HOST memakai nama service db, bukan 127.0.0.1 atau localhost, karena di dalam network Docker Compose, 127.0.0.1 merujuk ke container itu sendiri, bukan ke container database yang terpisah. Kesalahan ini jadi salah satu jebakan paling sering ditemui Developer yang baru pertama kali men-dockerize Laravel, terutama kalau sebelumnya terbiasa menjalankan MySQL langsung di mesin lokal lewat localhost.

Verifikasi koneksi database berhasil lewat artisan tinker atau perintah db:show yang tersedia sejak Laravel 9.24.

docker compose exec app php artisan db:show

Kalau muncul error SQLSTATE[HY000] [2002] Connection refused, periksa apakah nilai DB_HOST sudah benar mengarah ke nama service database, dan pastikan container database sudah dalam status healthy lewat docker compose ps sebelum aplikasi mencoba konek.

29.2.3 Migration dan Seeder dalam Container

Jalankan migration lewat docker compose exec supaya perintah artisan dieksekusi di dalam container app, memakai environment dan koneksi database yang sama seperti aplikasi yang berjalan.

docker compose exec app php artisan migrate

Untuk seeding data awal, jalankan perintah db:seed secara terpisah atau gabungkan dengan migration lewat opsi --seed.

docker compose exec app php artisan migrate --seed

Perintah migrate tanpa opsi tambahan bersifat aman dijalankan berulang kali karena Laravel mencatat migration mana saja yang sudah dieksekusi di tabel migrations. Berbeda dengan migrate:fresh yang menghapus seluruh tabel sebelum menjalankan ulang semua migration dari awal, perintah ini bersifat destruktif dan sebaiknya cuma dipakai di environment development atau testing, bukan production, karena seluruh data yang sudah ada akan hilang permanen.

29.3 Queue Worker dan Scheduler dalam Container

Aplikasi Laravel yang menjalankan proses berat seperti pengiriman email atau pemrosesan file secara asynchronous memakai queue, dan queue butuh proses worker yang berjalan terus-menerus di latar belakang untuk mengambil dan mengeksekusi job. Proses ini terpisah dari proses PHP-FPM yang menangani request HTTP, sehingga butuh penanganan container tersendiri.

29.3.1 Container Terpisah untuk Queue Worker

Mengikuti prinsip satu proses utama per container yang jadi filosofi dasar Docker, cara paling sederhana menjalankan queue worker adalah lewat service terpisah yang memakai image sama dengan app, tapi dengan command yang berbeda.

services:
  app:
    build:
      context: .
      target: production
    volumes:
      - app-storage:/var/www/html/storage
    environment:
      APP_ENV: production
      DB_CONNECTION: mysql
      DB_HOST: db
    depends_on:
      db:
        condition: service_healthy

  queue:
    build:
      context: .
      target: production
    command: php artisan queue:work --sleep=3 --tries=3 --max-time=3600
    volumes:
      - app-storage:/var/www/html/storage
    environment:
      APP_ENV: production
      DB_CONNECTION: mysql
      DB_HOST: db
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

Opsi restart: unless-stopped menggantikan peran Supervisor dalam menjaga proses queue:work tetap berjalan, karena Docker daemon sendiri akan otomatis me-restart container tersebut kalau prosesnya berhenti karena error yang tidak tertangani. Opsi --max-time=3600 membuat worker berhenti secara graceful setelah berjalan satu jam, sebuah praktik yang direkomendasikan dokumentasi resmi Laravel untuk menghindari memory leak bertahap akibat proses PHP yang berjalan sangat lama, dan restart policy Docker akan langsung menjalankan proses baru begitu worker lama berhenti.

Opsi --tries=3 membatasi percobaan ulang sebuah job yang gagal sebanyak tiga kali sebelum job tersebut dipindahkan ke tabel failed jobs, mencegah job yang selalu gagal diproses berulang-ulang tanpa henti dan menghabiskan resource worker.

29.3.2 Supervisor untuk Multiple Worker Process

Kalau aplikasi butuh beberapa worker berjalan sekaligus dalam satu container, misalnya untuk menangani beberapa queue connection berbeda atau meningkatkan throughput pemrosesan job, Supervisor jadi pilihan yang lazim dipakai karena bisa mengelola banyak proses turunan sekaligus dan otomatis me-restart proses yang mati, sesuai rekomendasi dokumentasi resmi Laravel untuk pengelolaan queue worker di production. Buat file docker/supervisor/supervisord.conf berikut.

[supervisord]
nodaemon=true

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=4
user=www-data
stdout_logfile=/var/www/html/storage/logs/worker.log
stopwaitsecs=3600

Direktif numprocs=4 membuat Supervisor menjalankan empat proses queue:work sekaligus, masing-masing dipantau dan di-restart otomatis lewat autorestart=true kalau salah satunya berhenti tanpa sengaja. Direktif stopwaitsecs=3600 memberi waktu bagi worker untuk menyelesaikan job yang sedang diproses sebelum benar-benar dihentikan paksa, penting terutama untuk job yang butuh waktu pemrosesan cukup lama.

Buat image terpisah khusus untuk container worker yang memakai Supervisor sebagai proses utamanya.

FROM php:8.3-fpm-alpine AS worker
WORKDIR /var/www/html

RUN apk add --no-cache supervisor \
    libpng-dev libzip-dev oniguruma-dev \
    && docker-php-ext-install pdo_mysql mbstring zip gd bcmath

COPY --from=vendor /app /var/www/html
COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache

CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

Verifikasi seluruh worker berjalan lewat supervisorctl di dalam container.

docker compose exec queue supervisorctl status

Status RUNNING pada keempat proses laravel-worker menandakan Supervisor berhasil menjalankan semuanya. Kalau salah satu proses berulang kali masuk status FATAL, biasanya penyebabnya ada di konfigurasi command yang salah path atau environment variable koneksi database yang belum terbaca oleh container tersebut.

29.3.3 Scheduler dengan Cron atau schedule:work

Selain queue, Laravel juga menyediakan mekanisme task scheduling untuk menjalankan perintah tertentu secara berkala, misalnya membersihkan data lama atau mengirim laporan harian. Secara tradisional, scheduler ini butuh satu baris cron di level sistem operasi yang memanggil php artisan schedule:run setiap menit, dan Laravel sendiri yang menentukan tugas mana saja yang perlu dieksekusi pada menit tersebut berdasarkan definisi di kode aplikasi.

* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1

Menjalankan cron daemon di dalam container menambah kompleksitas tersendiri, karena butuh Supervisor atau init system serupa untuk mengelola proses cron sekaligus PHP-FPM dalam satu container, sesuatu yang bertentangan dengan filosofi satu proses utama per container. Alternatif yang lebih sesuai untuk lingkungan container adalah perintah schedule:work, yang menurut dokumentasi resmi Laravel berjalan di foreground dan otomatis memanggil scheduler setiap menit sampai proses tersebut dihentikan, tanpa perlu cron sama sekali.

services:
  scheduler:
    build:
      context: .
      target: production
    command: php artisan schedule:work
    volumes:
      - app-storage:/var/www/html/storage
    environment:
      APP_ENV: production
      DB_CONNECTION: mysql
      DB_HOST: db
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

Pendekatan ini membuat container scheduler punya satu proses utama yang jelas, sejalan dengan container queue yang sudah dibahas sebelumnya, dan restart policy Docker tetap berperan menjaga proses tersebut tetap hidup kalau terjadi crash. Verifikasi scheduler berjalan lewat log container-nya.

docker compose logs -f scheduler

Di lapangan, jebakan yang sering ditemui Sysadmin/DevOps Engineer adalah menjalankan lebih dari satu container scheduler secara bersamaan, misalnya saat aplikasi di-scale ke beberapa replica. Task yang didefinisikan tanpa withoutOverlapping() atau onOneServer() berpotensi dieksekusi berkali-kali dalam waktu bersamaan oleh tiap replica scheduler, sehingga sebaiknya container scheduler tetap dijaga berjumlah satu instance saja, terpisah dari service app yang boleh di-scale sebanyak apa pun.

29.4 Environment Configuration dan Artisan Commands

Konfigurasi Laravel yang nyaman dipakai saat development lewat file .env biasa perlu disesuaikan lagi sebelum aplikasi benar-benar siap dijalankan di production, terutama soal bagaimana environment variable disuntikkan ke container dan langkah caching yang perlu dijalankan setiap kali ada perubahan konfigurasi.

29.4.1 Mengelola .env di Berbagai Environment

File .env sebaiknya tidak ikut disalin ke dalam image lewat instruksi COPY di Dockerfile, karena isinya sering berbeda antar environment dan berpotensi berisi secret seperti password database atau API key. Tambahkan .env ke file .dockerignore supaya tidak tersalin sama sekali ke dalam build context.

.env
.git
node_modules
vendor
tests

Docker Compose menyediakan opsi env_file untuk memuat environment variable dari file eksternal ke dalam container tanpa perlu menyalinnya ke dalam image.

services:
  app:
    build:
      context: .
      target: production
    env_file:
      - .env.production
    volumes:
      - app-storage:/var/www/html/storage

Pola ini memisahkan secara tegas antara image yang bersifat immutable dan bisa dipakai di environment mana pun, dengan konfigurasi yang berubah-ubah tergantung environment tempat container dijalankan, sesuai prinsip build once, run anywhere yang jadi salah satu keunggulan utama containerization. Untuk kredensial yang sangat sensitif seperti DB_PASSWORD atau APP_KEY di production, pertimbangkan memakai mekanisme secrets management milik orchestrator yang dipakai, alih-alih menaruhnya polos di file .env.production yang tersimpan di server.

29.4.2 Menjalankan Artisan Command dalam Container

Semua perintah artisan yang biasa dijalankan langsung di terminal lokal tetap bisa dipakai di dalam container lewat docker compose exec, selama container-nya sedang berjalan.

docker compose exec app php artisan --version
docker compose exec app php artisan make:controller PostController
docker compose exec app php artisan storage:link

Perintah storage:link membuat symbolic link dari public/storage ke storage/app/public, langkah yang wajib dijalankan setidaknya sekali supaya file yang diunggah lewat filesystem disk public bisa diakses lewat URL publik. Untuk perintah yang perlu dijalankan sekali saja saat deployment, seperti migrate atau storage:link, pertimbangkan menjalankannya lewat tahap terpisah di pipeline deployment, bukan otomatis setiap kali container start, karena menjalankan migrate otomatis di banyak replica sekaligus berisiko menimbulkan race condition pada perubahan skema database.

29.4.3 Caching Config, Route, dan View untuk Production

Laravel secara default membaca file konfigurasi dan mendaftarkan route setiap kali ada request masuk, proses yang cukup ringan untuk development tapi menambah overhead yang bisa dihindari di production. Jalankan perintah optimize untuk menggabungkan seluruh proses caching sekaligus.

docker compose exec app php artisan optimize

Perintah ini menjalankan config:cache, route:cache, view:cache, dan event:cache sekaligus, sesuai dokumentasi resmi Laravel. Hasil cache ini bersifat statis dan cuma valid sampai ada perubahan pada file konfigurasi, route, atau environment variable, sehingga wajib dijalankan ulang setiap kali ada deployment baru, bukan cuma sekali saat image pertama kali dibuat.

Satu jebakan yang sering ditemui Developer adalah menjalankan config:cache lalu lupa bahwa perintah ini "membekukan" nilai env() ke dalam satu file cache. Setelah config:cache dijalankan, pemanggilan fungsi env() secara langsung di luar file config/*.php, misalnya di dalam route atau controller, tidak lagi mengembalikan nilai environment variable yang sebenarnya, karena Laravel sudah membaca dari file cache, bukan dari environment lagi. Pastikan seluruh akses environment variable dilakukan lewat file konfigurasi di folder config, bukan lewat pemanggilan env() langsung di kode aplikasi, supaya perilaku aplikasi tetap konsisten baik sebelum maupun sesudah caching dijalankan.

Kalau ada perubahan konfigurasi tapi aplikasi masih menampilkan perilaku lama setelah deployment, hapus cache lama lebih dulu sebelum membuat cache baru.

docker compose exec app php artisan optimize:clear
docker compose exec app php artisan optimize

Verifikasi bahwa cache sudah aktif dengan memeriksa keberadaan file bootstrap/cache/config.php di dalam container.

docker compose exec app ls -la bootstrap/cache/