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 vendor hanya fokus pada dependency PHP.
  • Stage assets hanya membangun asset frontend.
  • Stage app menjadi 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
.vscode

Catatan penting:

  • Jangan kirim .env ke image. Environment variable sebaiknya diberikan saat runtime, bukan di-bake ke image.
  • Biasanya jangan kirim vendor dan node_modules dari 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.
  • vendor dipisah ke named volume agar tidak tertimpa oleh kondisi host yang belum tentu cocok.
  • node_modules juga 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 vendor dan node_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: 3

Untuk 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:8

Kenapa 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=20000

Pastikan 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:clear

Jika 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:latest

Jika 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 web

Dengan 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 -m

Beberapa hal yang layak diperiksa saat debugging:

  • apakah extension PHP yang dibutuhkan sudah terpasang,
  • apakah file permission pada storage dan bootstrap/cache benar,
  • apakah environment variable benar-benar masuk ke container,
  • apakah hostname service sesuai dengan nama service Compose, misalnya DB_HOST=db atau REDIS_HOST=redis.

Uji koneksi antar-service

docker compose exec app ping db
docker compose exec app ping redis

Untuk 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.1 atau localhost di dalam container. Gunakan nama service, misalnya db.
  • 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.