Di artikel sebelumnya, kita sudah sukses merakit stack lokal Laravel menggunakan Docker Compose. Semuanya jalan mulus di mesin lokal: kode ter-mount via volume, ekstensi PHP lengkap, database terhubung, dan fitur live reload berjalan tanpa kendala.
Tapi, begitu kita bicara soal production, situasinya berubah 180 derajat.
Kalau image development kemarin langsung kamu dorong ke production registry (seperti Docker Hub, AWS ECR, atau GitHub Packages), kamu bakal menghadapi tiga masalah besar:
-
Ukuran image membengkak drastis (bloated): Sering kali tembus di atas 1 GB hingga 1.5 GB karena membawa compiler C++, ekstensi development, dev-dependencies Composer (PHPUnit, Faker), hingga folder
node_modulesraksasa dari Vite/Tailwind. -
Deploy lambat dan boros bandwidth: Setiap proses CI/CD harus mengunduh dan mengekstrak image berukuran gigabyte ke server produksi atau worker Kubernetes.
-
Attack surface melebar: Membiarkan utility seperti
npm,git, build tools, dan source code non-kompilasi berada di dalam container production memberikan celah ekstra bagi penyerang saat sistem dieksploitasi.
Solusi standar industri untuk masalah ini adalah Docker Multi-Stage Build.
Dengan teknik ini, kita memisahkan proses kompilasi (compile assets dan instalasi vendor) dari runtime final. Hasil akhirnya? Image production Laravel yang langsing (bisa di bawah 120 MB), bersih dari sampah build, dan jauh lebih aman.
Memahami Konsep Multi-Stage Build
Bayangkan kamu sedang membangun rumah modular.
Kamu butuh gergaji mesin, semen, truk molen, dan tukang las selama proses konstruksi. Tapi begitu rumahnya selesai dan siap ditinggali, apakah semua mesin molen dan tumpukan semen sisa itu perlu ditaruh di ruang tamu? Jelas tidak. Kamu hanya butuh perabotan jadi dan bangunan yang sudah bersih.
Di Docker konvensional tanpa multi-stage, semua "alat tukang" itu ikut terkunci di image akhir.
Dengan Multi-Stage Build, satu file
Dockerfile dapat memiliki beberapa instruksi FROM. Setiap instruksi FROM mewakili stage (tahapan) terpisah:[ Stage 1: Frontend Builder (Node.js) ]
Compile Tailwind/Vite ──► Ekstrak: /public/build
[ Stage 2: Composer Builder (PHP CLI) ]
composer install --no-dev ──► Ekstrak: /vendor
│ (Hanya salin hasil akhir)
▼
[ Stage 3: Production Runtime (PHP-FPM Minimal) ]
Hanya berisi: PHP Runtime + Ekstensi + Source Code + /public/build + /vendor
(Tanpa Node.js, tanpa compiler OS, tanpa git)
Container production kamu tidak perlu tahu apa itu
npm atau bagaimana Vite bekerja. Ia hanya butuh file output .css dan .js yang sudah di-minify.Merancang Dockerfile.prod Multi-Stage
Mari kita racik
Dockerfile.prod yang membagi alur kerja ke dalam 3 tahapan terisolasi. Buat file ini di folder docker/php/Dockerfile.prod atau langsung di root proyek kamu.Dockerfile
# =========================================================================
# STAGE 1: FRONTEND BUILDER (Node.js Stage)
# =========================================================================
# Tahap ini khusus meng-compile asset Vite, Tailwind CSS, atau JavaScript.
FROM node:20-alpine AS frontend-builder
WORKDIR /app
# Salin package definitions terlebih dahulu untuk memaksimalkan Docker layer caching
COPY package.json package-lock.json ./
# Install dependencies frontend secara bersih
RUN npm ci
# Salin source code yang dibutuhkan untuk proses build frontend
COPY resources/ ./resources/
COPY vite.config.js tailwind.config.js* postcss.config.js* ./
COPY public/ ./public/
# Jalankan proses kompilasi asset ke public/build
RUN npm run build
# =========================================================================
# STAGE 2: BACKEND DEPENDENCIES BUILDER (Composer Stage)
# =========================================================================
# Tahap ini khusus mengunduh paket vendor PHP tanpa dev-dependencies.
FROM composer:2 AS vendor-builder
WORKDIR /app
# Salin file manifesto Composer
COPY composer.json composer.lock ./
# Unduh paket PHP dengan flags optimasi production:
# - --no-dev: lewati paket testing (PHPUnit, Mockery, Faker, dsb)
# - --no-scripts: cegah artisan scripts jalan sebelum source code lengkap
# - --no-autoloader: tunda pembuatan autoloader sampai seluruh source code ada
# - --ignore-platform-reqs: hindari kegagalan build jika ekstensi sistem belum terpasang di stage ini
RUN composer install \
--no-dev \
--no-interaction \
--no-plugins \
--no-scripts \
--prefer-dist \
--optimize-autoloader
# =========================================================================
# STAGE 3: PRODUCTION RUNTIME (PHP-FPM Minimalist)
# =========================================================================
# Image final yang benar-benar akan dijalankan di server production.
FROM php:8.3-fpm-alpine AS production
# Label metadata untuk image tracking
LABEL maintainer="Engineering Team"
LABEL environment="production"
# Install ekstensi sistem level OS seminimal mungkin yang mutlak dibutuhkan runtime
# Menggunakan Alpine package manager (apk) agar ukuran image sangat kecil
RUN apk add --no-cache \
libpng \
libjpeg-turbo \
freetype \
libzip \
oniguruma \
icu-libs
# Install build dependencies sementara untuk kompilasi modul PHP, lalu langsung hapus build-deps nya
RUN apk add --no-cache --virtual .build-deps \
$PHPIZE_DEPS \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
libzip-dev \
oniguruma-dev \
icu-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
pdo_mysql \
mbstring \
zip \
bcmath \
intl \
opcache \
gd \
&& apk del .build-deps
# Konfigurasi PHP Production & Opcache
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
# Terapkan tuning Opcache agresif untuk performa production
RUN { \
echo 'opcache.memory_consumption=128'; \
echo 'opcache.interned_strings_buffer=8'; \
echo 'opcache.max_accelerated_files=10000'; \
echo 'opcache.revalidate_freq=0'; \
echo 'opcache.validate_timestamps=0'; \
echo 'opcache.enable_cli=1'; \
echo 'opcache.fast_shutdown=1'; \
} > /usr/local/etc/php/conf.d/opcache-recommended.ini
# Set direktori kerja
WORKDIR /var/www/html
# 1. Salin seluruh source code aplikasi Laravel
COPY . /var/www/html
# 2. Salin vendor PHP dari STAGE 2 (Composer Stage)
COPY --from=vendor-builder /app/vendor /var/www/html/vendor
# 3. Salin artefak frontend dari STAGE 1 (Node Stage)
COPY --from=frontend-builder /app/public/build /var/www/html/public/build
# 4. Ambil Composer binary sebentar untuk menghasilkan production classmap final
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
RUN composer dump-autoload --optimize --classmap-authoritative --no-dev \
&& rm /usr/bin/composer
# Setup permission: serahkan kepemilikan file ke user standar www-data
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache
# Jalankan container menggunakan user non-root demi keamanan
USER www-data
EXPOSE 9000
CMD ["php-fpm"]
Bedah Anatomi dan Rahasia Optimasi
Setiap baris di atas dirancang dengan alasan arsitektural spesifik. Mari kita bedah detail pentingnya:
1. Perbedaan Drastis Debian vs Alpine
Di environment lokal pada artikel sebelumnya, kita menggunakan base image Debian (
php:8.3-fpm). Debian sangat ramah pemula karena tool debugging-nya lengkap.Namun untuk production, kita beralih ke Alpine Linux (
php:8.3-fpm-alpine). Base image Alpine berukuran kurang dari 10 MB, dibandingkan Debian yang bisa mencapai 100+ MB hanya untuk sistem operasi dasar.2. Trik Virtual Build Dependencies (apk add --virtual)
Perhatikan blok ini di stage final:
Dockerfile
RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS ... \
&& docker-php-ext-install ... \
&& apk del .build-deps
Untuk meng-compile ekstensi C (seperti GD atau intl), PHP butuh compiler (
gcc, make, autoconf). Tapi compiler itu tidak dipakai lagi begitu ekstensinya selesai di-compile.Dengan menandai paket-paket tersebut ke dalam grup virtual
.build-deps lalu menghapusnya (apk del) dalam satu instruksi RUN yang sama, cache compiler tidak akan tertinggal di layer Docker mana pun.3. Ekstrak Hasil, Tinggalkan Mesinnya (COPY --from=...)
Ini adalah jantung dari multi-stage build:
-
Node.js, package npm, dan cache
~/.npmtertinggal seluruhnya di containerfrontend-builder. Yang kita bawa ke image akhir hanyalah folder statis/public/build. -
Tool Composer dan package development (
fakerphp,mockery) tertinggal di containervendor-builder.
4. Opcache Lock (validate_timestamps=0)
Pada server production, file kode aplikasi tidak akan berubah selama container berjalan (karena tidak ada volume mounting dari laptop).
Dengan menyetel
opcache.validate_timestamps=0, PHP tidak akan membuang siklus I/O CPU untuk memeriksa apakah ada file .php yang diedit. Kode langsung dibaca 100% dari shared memory, membuat eksekusi routing dan rendering controller jauh lebih kencang.Cara Build dan Menguji Image Production
Untuk menguji apakah
Dockerfile.prod berjalan dengan baik, jalankan perintah build di terminal lokal kamu:Bash
# Build image production
docker build -t laravel-app:prod -f Dockerfile.prod .
Setelah proses selesai, mari kita buktikan efisiensinya. Bandingkan ukuran image development dengan image production yang baru saja dibuat:
Bash
docker images | grep laravel-app
Outputnya biasanya akan terlihat seperti ini:
Plaintext
REPOSITORY TAG IMAGE ID CREATED SIZE
laravel-app dev a1b2c3d4e5f6 2 days ago 1.15GB
laravel-app prod f6e5d4c3b2a1 10 seconds ago 118MB
Image berhasil dipangkas hingga hampir 90% lebih kecil tanpa mengorbankan fungsionalitas aplikasi sama sekali.
Eksekusi Startup di Production (Entrypoint Script)
Di production, container
app perlu melakukan beberapa tugas inisialisasi saat pertama kali boot sebelum menerima request (seperti caching konfigurasi dan migrasi database).Buat file bernama
docker/entrypoint.prod.sh:Bash
#!/bin/sh
set -e
# Cache konfigurasi, route, dan blade view agar booting framework instan
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
# Jalankan migrasi database otomatis (opsional, gunakan dengan hati-hati jika cluster multi-pod)
# php artisan migrate --force
# Eksekusi command utama (php-fpm)
exec "$@"
Beri izin eksekusi pada file script tersebut di host kamu:
Bash
chmod +x docker/entrypoint.prod.sh
Lalu di bagian akhir
Dockerfile.prod, tambahkan script tersebut sebelum CMD:Dockerfile
COPY docker/entrypoint.prod.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
CMD ["php-fpm"]
Checklist Keamanan Sebelum Deploy ke Registry
Sebelum image ini di-push ke Amazon ECR, Google Artifact Registry, atau DigitalOcean Container Registry, pastikan checklist ini lolos:
-
File
.envtidak bocor ke dalam image: Pastikan.envterdaftar di.dockerignore. Nilai kredensial production wajib diinjeksi saat runtime melalui container environment variables atau secret manager (Kubernetes Secrets / AWS Secrets Manager), bukan di-hardcode saat build. -
User non-root aktif: Pastikan perintah
USER www-datatetap aktif di baris akhir Dockerfile. Menjalankan proses web server sebagairootdi production adalah risiko keamanan fatal. -
Storage directory permissions: Direktori
storagedanbootstrap/cacheharus tetap bisa ditulisi oleh userwww-datauntuk menyimpan sessions, logs, dan framework file caches. -
Scanning vulnerability: Sebelum deploy, scan image kamu menggunakan scanner container modern seperti Trivy:
Bashtrivy image laravel-app:prod
Mengadopsi multi-stage build bukan sekadar trik menghemat ruang hard disk server. Pola ini adalah pondasi arsitektur cloud-native yang membedakan aplikasi amatir dengan sistem enterprise yang siap di-scale secara horizontal, aman dari eksploitasi dependensi build, dan memiliki siklus deployment hitungan detik.

Sigit Wasis Subekti
Software Engineer & Tech Educator
Software Engineer and Tech Educator sharing insights on web development and software architecture.