Menyusun Dockerfile multi-stage untuk Laravel production: stage builder composer dan runtime PHP-FPM dengan ekstensi, stage web Nginx alias, entrypoint dengan UID konsisten untuk NFS, serta optimasi ukuran image dengan dockerignore

Tujuan episode 8 sederhana tapi krusial: menghasilkan image Laravel yang layak produksi — kecil, aman, tidak menyulitkan ketika dipasang bersama volume RWX. Image ini akan menjadi komponen yang di-deploy dari episode 10 sampai akhir series.
Mengapa episode ini penting bagi shared storage? Karena image menentukan siapa berjalan sebagai user apa. Ketika Pod menulis ke NFS, permission file yang dihasilkan bergantung pada UID proses. Image yang berjalan sebagai www-data (UID 33) akan menulis file milik 33 ke NFS — dan kita harus konsisten dengan itu sejak awal (detail di episode 13).
Kita memakai tiga stage: builder (composer), runtime (PHP-FPM), dan web (Nginx).
# STAGE 1: builder
FROM composer:2 AS builder
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --prefer-dist --no-interaction
COPY . .
RUN composer dump-autoload --optimize
# STAGE 2: runtime PHP-FPM
FROM php:8.3-fpm AS runtime
RUN apt-get update && apt-get install -y --no-install-recommends \
libpng-dev libjpeg-dev zip unzip libzip-dev \
&& docker-php-ext-install pdo_mysql gd zip \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /app /var/www
WORKDIR /var/www
RUN mkdir -p storage/app/public storage/framework/{cache,sessions,views} \
&& chown -R www-data:www-data /var/www
RUN php artisan config:cache && php artisan route:cache
EXPOSE 9000
USER www-data
# STAGE 3: web Nginx
FROM nginx:1.27-alpine AS web
COPY nginx-default.conf /etc/nginx/conf.d/default.conf
COPY --from=runtime /var/www/public /var/www/publicFile di atas disederhanakan demi fokus: stage web membaca public/ dari stage runtime. Yang penting adalah konsepnya:
composer install --no-dev di image composer:2. Dependency di-cache lewat COPY composer.json composer.lock.php:8.3-fpm dipasang ekstensi pdo_mysql (MySQL/PostgreSQL), gd (pengolahan gambar/thumbnail), zip. Ikuti kebutuhan proyek kalian.nginx:1.27-alpine menyajikan public/ dan meneruskan request PHP ke FPM.Note
Stage "web" di atas membaca public/ dari builder. Pada deployment asli, public/storage adalah symlink ke folder dalam volume — kita pastikan mount & init container di episode 10 dan 12, bukan baked ke image.
Masalah klasik di image multi-stage: permission. Lambatnya kontainer NFS-squash membuat file yang perlu ditulis aplikasi menjadi tidak tertulis bila owner tidak cocok. Solusinya ada di image:
WORKDIR /var/www — titik mount volume kita nanti.mkdir -p storage/... lalu chown -R www-data:www-data /var/www — menjamin folder storage milik UID 33 (www-data).USER www-data — proses FPM berjalan sebagai 33, bukan root.Mengapa ini penting untuk NFS? Saat Pod menulis ke NFS, sistem container menentukan UID pemilik file. Dengan no_root_squash (episode 3), root di container = root di NFS; dengan proses sebagai www-data, file hasil upload otomatis milik UID 33.
Penyembuhan tambahan di level deployment (init container untuk symlink dan folder) kita lakukan di episode 12.
Buang yang tidak perlu agar build cepat dan image bersih:
.git
node_modules
.venv
.env
.DS_Store
storage/logs/*
storage/framework/cache/*Perhatikan: storage/ TIDAK ikut di-copy data-datanya saat runtime karena kita override dengan volume — tetapi struktur folder (yang dibuat mkdir -p) tetap ada di image sebagai fallback jika volume belum siap.
-alpine/-slim bila memungkinkan — asalkan ekstensi yang kalian butuh tersedia di varian tersebut.docker build -t ghcr.io/<user>/laravel-media:1.0.0 .
docker images ghcr.io/<user>/laravel-mediaTarget ukuran yang sehat untuk Laravel runtime+nginx: sekitar 150-250 MB (tergantung ekstensi).
Sebelum push, uji image berjalan baik:
docker run --rm -p 8080:80 ghcr.io/<user>/laravel-media:1.0.0
curl -I http://localhost:8080/curl -I harus mengembalikan status HTTP yang wajar (200/301). Jika ada route yang butuh database, ekspektasikan error 500 — itu normal bila DB belum di-setup; yang penting web server dan FPM hidup.
Warning
Jangan setup NFS di lingkungan lokal ini — cukup verifikasi bahwa web server, FPM, dan folder storage berfungsi. Mount NFS dulu dilakukan sebagai volume di Kubernetes, sehingga image tidak perlu tahu-menahu soal NFS.
Pada episode 8 ini, kalian telah membangun image produksi:
Inti yang harus dibawa pulang:
storage/ milik www-data dan proses berjalan sebagai non-root.USER www-data di image runtime menjamin file NFS ber-owner UID 33..dockerignore, varian slim/alpine, multi-stage kecil.docker run + curl sebelum push.Di episode 9 selanjutnya kita akan push image ke registry dan menyiapkan readiness probe: docker tag/push, imagePullSecrets untuk private registry, serta kesehatan nginx dan php-fpm dengan readiness/liveness probe. Sampai jumpa di episode 9!