Pada part 1, umumnya fondasi Docker untuk Laravel sudah terbentuk: aplikasi berjalan, database terhubung, dan service dasar sudah bisa dipakai. Tantangan berikutnya biasanya bukan lagi sekadar jalan, tetapi bagaimana membuat workflow yang rapi, efisien, mudah di-debug, dan realistis untuk development maupun production.
Di artikel ini kita akan fokus pada penyempurnaan tersebut: multi-stage build untuk menekan ukuran image, layer caching Composer agar build lebih cepat, penggunaan .dockerignore, healthcheck, named volume, strategi hot reload untuk development, serta pemisahan queue worker, scheduler, dan service pendukung ke container terpisah. Kita juga akan membahas perbedaan konfigurasi development vs production, termasuk env, debug, opcache, dan asset build dengan Vite.
Mengapa workflow Docker Laravel perlu dirapikan
Setup Docker yang “asal jalan” sering memunculkan masalah yang baru terasa setelah proyek membesar:
- Image terlalu besar, sehingga build dan deploy lambat.
- Cache build tidak efektif, menyebabkan setiap perubahan kecil memaksa Composer install ulang.
- Satu container melakukan terlalu banyak tugas, misalnya PHP-FPM sekaligus queue worker dan scheduler, sehingga sulit diobservasi dan direstart secara terpisah.
- Volume tidak terkelola, mengakibatkan performa development buruk atau file dependency saling menimpa.
- Konfigurasi development dan production tercampur, misalnya debug aktif di production atau Vite dijalankan dengan mode yang tidak tepat.
Prinsip pentingnya adalah: satu image bisa dipakai ulang, tetapi cara menjalankannya bisa berbeda sesuai kebutuhan service. Dengan pendekatan ini, kita tidak perlu membuat image berbeda untuk web, worker, dan scheduler jika basis aplikasinya sama.
Multi-stage build untuk image yang lebih kecil dan bersih
Multi-stage build memisahkan proses build dependency dari image runtime akhir. Ini penting karena tool seperti Composer, Node, atau dependency kompilasi tidak selalu perlu dibawa ke image production.
Contoh Dockerfile multi-stage untuk Laravel
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
--no-dev \
--prefer-dist \
--no-interaction \
--no-progress \
--optimize-autoloader
FROM node:20-alpine AS assets
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY resources ./resources
COPY vite.config.* ./
COPY public ./public
RUN npm run build
FROM php:8.2-fpm-alpine AS app
WORKDIR /var/www/html
RUN apk add --no-cache \
bash \
icu-dev \
libzip-dev \
oniguruma-dev \
unzip \
git \
curl \
fcgi \
linux-headers \
$PHPIZE_DEPS \
&& docker-php-ext-install pdo pdo_mysql mbstring intl zip opcache \
&& apk del $PHPIZE_DEPS
COPY . .
COPY --from=vendor /app/vendor ./vendor
COPY --from=assets /app/public/build ./public/build
RUN chown -R www-data:www-data storage bootstrap/cache \
&& chmod -R ug+rwx storage bootstrap/cache
USER www-data
CMD ["php-fpm"]Kenapa ini efektif?
- Stage
vendorhanya fokus pada dependency PHP. - Stage
assetshanya membangun asset frontend. - Stage
appmenjadi image runtime yang lebih bersih karena tidak perlu membawa seluruh tool build Node dan Composer.
Trade-off-nya: Dockerfile menjadi sedikit lebih kompleks. Namun untuk proyek Laravel yang akan di-deploy berulang kali, manfaatnya biasanya jauh lebih besar dibanding kompleksitas tambahannya.
Kapan dependency development tetap diperlukan?
Untuk development, Anda sering tetap memerlukan composer install dengan package dev, Node runtime, dan Vite dev server. Karena itu, jangan memaksakan image production untuk semua skenario. Lebih baik pisahkan target build atau file Compose untuk development dan production.
Optimasi layer caching Composer dan build context
Salah satu kesalahan paling umum adalah menyalin seluruh source code sebelum menjalankan Composer install. Akibatnya, setiap file yang berubah akan membatalkan cache layer Composer.
Urutan COPY yang benar
COPY composer.json composer.lock ./
RUN composer install --no-dev --prefer-dist --no-interaction --no-progress
COPY . .Dengan urutan ini, Docker hanya menjalankan ulang Composer install jika composer.json atau composer.lock berubah. Jika yang berubah hanya file controller, migration, atau view, layer dependency tetap di-cache.
Gunakan .dockerignore
.dockerignore mencegah file yang tidak relevan ikut dikirim ke Docker daemon saat build. Ini berdampak langsung pada kecepatan build dan ukuran context.
.git
node_modules
vendor
storage/logs
storage/framework/cache
storage/framework/sessions
storage/framework/views
.env
.docker
Dockerfile*
compose*.yml
npm-debug.log
yarn-error.log
.idea
.vscodeCatatan penting:
- Jangan kirim
.envke image. Environment variable sebaiknya diberikan saat runtime, bukan di-bake ke image. - Biasanya jangan kirim
vendordannode_modulesdari host, karena dependency sebaiknya dibangun di dalam proses image agar konsisten.
Kesalahan umum lain adalah lupa mengecualikan folder .git, sehingga context build menjadi besar dan lambat, terutama di repository yang sudah lama.
Named volume dan strategi hot reload untuk development
Untuk development, kebutuhan utamanya adalah perubahan source code langsung terlihat tanpa build image ulang. Cara paling umum adalah bind mount source code dari host ke container.
Contoh docker-compose untuk development
services:
app:
build:
context: .
dockerfile: Dockerfile
working_dir: /var/www/html
volumes:
- ./:/var/www/html
- vendor_data:/var/www/html/vendor
env_file:
- .env.docker
depends_on:
db:
condition: service_healthy
networks:
- appnet
web:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
networks:
- appnet
vite:
image: node:20-alpine
working_dir: /var/www/html
command: sh -c "npm ci && npm run dev -- --host 0.0.0.0"
ports:
- "5173:5173"
volumes:
- ./:/var/www/html
- node_modules_data:/var/www/html/node_modules
networks:
- appnet
volumes:
vendor_data:
node_modules_data:
networks:
appnet:Pola ini penting karena:
- Source code di-mount dari host untuk hot reload.
vendordipisah ke named volume agar tidak tertimpa oleh kondisi host yang belum tentu cocok.node_modulesjuga disimpan di named volume karena package Node yang dibangun di container belum tentu kompatibel jika langsung bercampur dengan environment host.
Kapan bind mount penuh bisa menjadi masalah?
Di beberapa sistem, terutama Docker Desktop pada macOS/Windows, bind mount penuh dapat memperlambat I/O file. Gejalanya antara lain:
- autoload terasa lambat,
- Vite startup lambat,
- test suite dan Artisan command lebih berat dari seharusnya.
Jika ini terjadi, pertimbangkan:
- mount hanya folder yang benar-benar perlu,
- tetap gunakan named volume untuk
vendordannode_modules, - pisahkan service Vite agar beban development frontend tidak mengganggu PHP runtime.
Healthcheck dan ketergantungan antar-service
depends_on saja tidak menjamin service siap menerima koneksi. Misalnya, container MySQL sudah hidup tetapi database belum siap dipakai. Karena itu, healthcheck penting untuk workflow yang stabil.
Contoh healthcheck database dan app
services:
db:
image: mysql:8
environment:
MYSQL_DATABASE: laravel
MYSQL_USER: laravel
MYSQL_PASSWORD: secret
MYSQL_ROOT_PASSWORD: rootsecret
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-prootsecret"]
interval: 10s
timeout: 5s
retries: 10
app:
build: .
healthcheck:
test: ["CMD", "php", "artisan", "about"]
interval: 30s
timeout: 10s
retries: 3Untuk PHP-FPM, healthcheck HTTP tidak selalu relevan jika endpoint web dilayani Nginx di container terpisah. Anda bisa memeriksa proses PHP, menjalankan command Artisan ringan, atau memakai cgi-fcgi untuk probe yang lebih spesifik bila diperlukan.
Catatan: Healthcheck sebaiknya cepat dan ringan. Jangan gunakan command berat yang justru membebani container secara berkala.
Memisahkan web, queue worker, scheduler, dan service pendukung
Di deployment yang lebih rapi, satu container sebaiknya menjalankan satu proses utama. Laravel sering membutuhkan lebih dari sekadar web request handler:
- web/app untuk PHP-FPM,
- queue worker untuk job asynchronous,
- scheduler untuk task terjadwal,
- redis untuk queue/cache,
- database untuk persistence.
Contoh service terpisah di Compose
services:
app:
build: .
command: php-fpm
env_file: .env.docker
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
queue:
build: .
command: php artisan queue:work --sleep=3 --tries=3 --timeout=90
env_file: .env.docker
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
scheduler:
build: .
command: sh -c "while true; do php artisan schedule:run --verbose --no-interaction; sleep 60; done"
env_file: .env.docker
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
redis:
image: redis:alpine
db:
image: mysql:8Kenapa dipisah?
- Restart lebih aman: queue worker crash tidak mematikan web app.
- Observabilitas lebih baik: log tiap proses terpisah.
- Scaling lebih fleksibel: worker bisa ditambah tanpa menambah web container.
Untuk production yang lebih serius, scheduler sering dijalankan sebagai container terpisah atau didelegasikan ke platform orchestration. Pola loop sleep 60 cukup praktis di Compose, tetapi bukan satu-satunya pendekatan.
Perbedaan konfigurasi development vs production
Ini bagian yang paling sering tercampur. Development dan production memiliki tujuan berbeda, sehingga konfigurasinya memang tidak seharusnya identik.
Environment dan debug
- Development:
APP_ENV=local,APP_DEBUG=true. - Production:
APP_ENV=production,APP_DEBUG=false.
Jangan bake nilai sensitif ke image. Gunakan env_file, secret manager, atau environment variable dari platform deploy. Image harus tetap generik; konfigurasi diberikan saat runtime.
Opcache
Untuk production, Opcache sangat penting karena mengurangi overhead parsing PHP berulang. Untuk development, pengaturan terlalu agresif justru membuat perubahan file tidak langsung terbaca.
Contoh pendekatan umum:
- Development: validasi timestamp aktif agar perubahan file langsung terbaca.
- Production: cache lebih agresif, validasi lebih minim, dan preload bisa dipertimbangkan sesuai kebutuhan.
; development
opcache.enable=1
opcache.validate_timestamps=1
opcache.revalidate_freq=0
; production
opcache.enable=1
opcache.validate_timestamps=0
opcache.memory_consumption=192
opcache.max_accelerated_files=20000Pastikan konfigurasi ini dipisahkan, misalnya dengan file INI berbeda atau target image berbeda.
Vite: dev server vs build statis
Di development, Vite umumnya dijalankan sebagai service terpisah dengan hot module replacement. Browser akan mengambil asset dari Vite dev server, bukan dari file hasil build.
Di production, Anda tidak menjalankan npm run dev. Sebaliknya, asset dibangun sekali dengan npm run build, lalu hasilnya disalin ke image runtime, misalnya ke public/build.
Kesalahan yang sering terjadi adalah mencoba menjalankan Vite dev server di production, yang tidak efisien dan menambah attack surface yang tidak perlu.
Perintah build dan run yang efisien
Development
docker compose up -d --build
docker compose exec app php artisan migrate
docker compose exec app php artisan optimize:clearJika hanya source code berubah dan menggunakan bind mount, Anda biasanya tidak perlu rebuild image. Rebuild cukup dilakukan saat Dockerfile, extension PHP, dependency sistem, atau lock file berubah.
Production build
docker build -t myapp:latest .
docker run -d --name myapp-app --env-file .env.production myapp:latestJika ingin lebih cepat dan konsisten, manfaatkan cache build dari CI atau registry cache. Prinsipnya tetap sama: pastikan layer dependency stabil agar cache bisa dipakai ulang.
Tips debugging container, log, dan koneksi antar-service
Melihat log per service
docker compose logs -f app
docker compose logs -f queue
docker compose logs -f webDengan service terpisah, Anda bisa langsung tahu error terjadi di web, worker, atau scheduler.
Masuk ke container untuk inspeksi
docker compose exec app sh
php artisan about
php artisan queue:failed
php -mBeberapa hal yang layak diperiksa saat debugging:
- apakah extension PHP yang dibutuhkan sudah terpasang,
- apakah file permission pada
storagedanbootstrap/cachebenar, - apakah environment variable benar-benar masuk ke container,
- apakah hostname service sesuai dengan nama service Compose, misalnya
DB_HOST=dbatauREDIS_HOST=redis.
Uji koneksi antar-service
docker compose exec app ping db
docker compose exec app ping redisUntuk pengujian yang lebih relevan, gunakan tool sesuai protokol. Misalnya uji koneksi database dari Artisan/Tinker, bukan hanya ping. Ping hanya memastikan resolusi DNS dan jaringan dasar, bukan otentikasi atau kesiapan service.
Masalah umum yang sering terjadi
- Aplikasi tidak bisa konek database: biasanya karena memakai
127.0.0.1ataulocalhostdi dalam container. Gunakan nama service, misalnyadb. - Queue tidak jalan: cek driver queue, koneksi Redis/database, dan pastikan worker benar-benar hidup.
- Perubahan kode tidak terlihat: cek bind mount, opcache development, dan cache Laravel.
- Asset Vite gagal dimuat: pastikan service Vite expose host yang benar, misalnya
0.0.0.0, dan port ter-forward.
Penutup
Workflow Docker untuk Laravel yang rapi bukan soal membuat konfigurasi paling rumit, tetapi memisahkan tanggung jawab dengan jelas dan memanfaatkan fitur Docker secara tepat. Multi-stage build membantu menghasilkan image yang lebih kecil dan bersih. Layer caching Composer mempercepat build. .dockerignore mengurangi beban context. Named volume dan bind mount mendukung development yang nyaman. Sementara itu, pemisahan web, queue worker, dan scheduler membuat sistem lebih mudah dioperasikan dan diskalakan.
Yang paling penting, bedakan dengan tegas kebutuhan development dan production. Development butuh fleksibilitas dan hot reload. Production butuh image yang stabil, ringan, minim tool yang tidak perlu, debug mati, serta opcache dan asset build yang optimal.
Jika fondasi dari part 1 sudah ada, maka langkah-langkah di artikel ini akan membawa setup Laravel Docker Anda dari level “bisa dipakai” menjadi lebih matang untuk kerja harian dan lebih siap untuk deployment yang konsisten.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!