Backend••8 min read•27 views

Docker Multi-Stage Build untuk Laravel Production: Panduan Bikin Image Super Ramping dan Aman

Dengan teknik ini, kita memisahkan proses kompilasi (compile assets dan instalasi vendor) dari runtime final. Hasil akhirnya?

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:

  1. 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_modules raksasa dari Vite/Tailwind.
  2. Deploy lambat dan boros bandwidth: Setiap proses CI/CD harus mengunduh dan mengekstrak image berukuran gigabyte ke server produksi atau worker Kubernetes.
  3. 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 ~/.npm tertinggal seluruhnya di container frontend-builder. Yang kita bawa ke image akhir hanyalah folder statis /public/build.
  • Tool Composer dan package development (fakerphp, mockery) tertinggal di container vendor-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 .env tidak bocor ke dalam image: Pastikan .env terdaftar 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-data tetap aktif di baris akhir Dockerfile. Menjalankan proses web server sebagai root di production adalah risiko keamanan fatal.
  • Storage directory permissions: Direktori storage dan bootstrap/cache harus tetap bisa ditulisi oleh user www-data untuk menyimpan sessions, logs, dan framework file caches.
  • Scanning vulnerability: Sebelum deploy, scan image kamu menggunakan scanner container modern seperti Trivy:

    Bash
    trivy 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

Sigit Wasis Subekti

Software Engineer & Tech Educator

Software Engineer and Tech Educator sharing insights on web development and software architecture.