PHP #
PHP adalah salah satu bahasa paling umum di dunia web, dan juga salah satu yang paling sering disalahgunakan di Docker. Image PHP di production sering > 500 MB, penuh tool build, dan bercampur antara dev dan runtime. Padahal, PHP bisa dibuat sangat ramping, aman, dan layak production — setara dengan Node.js, Python, bahkan mendekati Go — jika diperlakukan dengan benar.
Artikel ini membahas strategi Docker image PHP yang kecil dan production-grade, dengan pendekatan engineering, bukan sekadar tutorial php:apache. Kita akan lihat kenapa image PHP mudah membengkak, strategi yang benar (multi-stage + PHP-FPM + distroless), dan trade-off yang harus dipahami.
1. Realita Ukuran Image PHP #
Mari kita lihat angka realistis di production.
| Setup | Ukuran Image |
|---|---|
php:apache (default) |
600-800 MB |
php:fpm + debian |
400-600 MB |
php:8.3-fpm-alpine |
120-200 MB |
| Multi-stage + slim | 90-150 MB |
| Multi-stage + distroless | 60-100 MB |
Insight: Selisih antara 800 MB dan 60 MB adalah lebih dari 10x lipat. Image PHP yang optimal bukan mustahil — hanya butuh disiplin Dockerfile yang berbeda dari kebiasaan umum.
2. Kenapa Image PHP Mudah Membengkak? #
PHP = Runtime + Web Server + Extension #
Image php:apache (atau php:*-apache) membawa Apache + PHP module + banyak extension. Untuk production modern, ini pemborosan. Apache memakan memory dan CPU yang seharusnya bisa untuk aplikasi.
Pendekatan yang lebih modern: pisahkan web server dari PHP process. PHP-FPM berjalan sebagai service terpisah, dan Nginx (atau Caddy) di depan sebagai reverse proxy. Ini lebih scalable dan lebih ramping.
Extension PHP Membengkakkan Image #
PHP punya banyak extension yang sering dibutuhkan:
pdo_pgsql/pdo_mysql— database driver.gd— image processing.intl— internationalization.zip— arsip.bcmath— matematika presisi tinggi.opcache— bytecode cache.
Setiap extension menarik dependency OS-nya sendiri. Tanpa kontrol, semua ikut masuk image.
Composer Bocor ke Runtime #
Composer adalah dependency manager PHP. Tanpa pemisahan build vs runtime, Composer dan cache-nya akan masuk image akhir — padahal runtime tidak butuh Composer sama sekali.
Default Image php:* Penuh
#
Image php:latest (atau php:8.3) membawa Debian lengkap dengan banyak utilitas. Untuk production, php:8.3-fpm-alpine atau php:8.3-fpm-slim adalah alternatif yang lebih ramping.
3. Prinsip Utama: FPM Saja, Distroless, Composer Terpisah #
Runtime image PHP idealnya hanya berisi: PHP-FPM + extension production + source code aplikasi.
Titik. Tidak ada Apache, tidak ada Composer, tidak ada extension development, tidak ada test framework, tidak ada dokumentasi.
Tiga pilar untuk mencapainya:
- Multi-stage build — pisahkan build (butuh Composer + dev extension) dari runtime (butuh FPM + prod extension).
- PHP-FPM, bukan mod_php — FPM lebih scalable, dan image
php:*-fpmtidak bawa Apache. - Distroless base untuk runtime — image JRE-like yang sangat ramping, tanpa shell.
4. Strategi Multi-Stage dengan Composer #
4.1 Pola Dasar: FPM + Alpine #
# ==== Stage 1: Composer Install ====
FROM composer:2.7 AS vendor
WORKDIR /app
# Copy dependency file dulu (untuk cache)
COPY composer.json composer.lock ./
RUN composer install \
--no-dev \
--no-scripts \
--no-autoloader \
--prefer-dist \
--no-interaction
# ==== Stage 2: Build Assets (jika pakai Laravel) ====
FROM node:20-alpine AS assets
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ==== Stage 3: Runtime ====
FROM php:8.3-fpm-alpine
WORKDIR /app
# Install production extension
RUN apk add --no-cache \
libpq \
icu-libs \
oniguruma \
libzip \
libpng \
libjpeg-turbo \
freetype
# Install PHP extension
RUN docker-php-ext-install -j$(nproc) \
pdo \
pdo_pgsql \
intl \
zip \
opcache \
bcmath
# Copy Composer dependencies
COPY --from=vendor /app/vendor /app/vendor
# Generate optimized autoloader
RUN composer dump-autoload --optimize --no-dev --classmap-authoritative
# Copy source code
COPY . .
# Copy built assets
COPY --from=assets /app/public/build /app/public/build
# Optimize Laravel (jika pakai)
RUN php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache
# Non-root user
RUN addgroup -g 1001 -S appgroup \
&& adduser -u 1001 -S appuser -G appgroup
USER appuser
EXPOSE 9000
CMD ["php-fpm"]
Ukuran tipikal: 90-150 MB.
Penjelasan penting:
Stage 1: Composer
composer:2.7— image Composer official, kecil dan self-contained.composer install --no-dev— install hanya production dependency, skip dev (testing, debug tools).--no-scripts— skip script yang mungkin jalan saat install (misal Laravel post-install).--no-autoloader— skip generate autoloader; akan di-generate di stage runtime untuk optimasi.
Stage 2: Assets (opsional, hanya untuk Laravel yang pakai Vite/Mix)
- Build JavaScript dan CSS di sini.
- Output
public/builddi-copy ke runtime stage.
Stage 3: Runtime
php:8.3-fpm-alpine— FPM di Alpine, sudah jauh lebih ramping dariphp:apache.docker-php-ext-install— script resmi yang disediakan image PHP untuk install extension.-j$(nproc)— paralel compile untuk mempercepat build.- Composer dump-autoload dengan
--optimizedan--classmap-authoritativeuntuk autoloader yang lebih cepat. php artisan config:cachedll — pre-compile konfigurasi, route, dan view.
Kapan pakai: Default untuk production service PHP. Trade-off ukuran vs debugging能力 masih seimbang — Alpine punya shell.
4.2 Versi Distroless: Production Mature #
# ==== Stage 1: Composer ====
FROM composer:2.7 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
--no-dev \
--no-scripts \
--no-autoloader \
--prefer-dist
# ==== Stage 2: PHP + Extensions ====
FROM php:8.3-cli-alpine AS builder
WORKDIR /app
RUN apk add --no-cache $PHPIZE_DEPS \
libpq-dev \
icu-dev \
oniguruma-dev \
libzip-dev \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev
# Build extension
RUN docker-php-ext-install -j$(nproc) \
pdo \
pdo_pgsql \
intl \
zip \
opcache \
bcmath \
gd
# Copy Composer deps
COPY --from=vendor /app/vendor /app/vendor
COPY . .
# Generate optimized autoloader
RUN composer dump-autoload --optimize --no-dev --classmap-authoritative
# ==== Stage 3: Runtime Distroless ====
FROM gcr.io/distroless/base-debian12:nonroot
WORKDIR /app
# Copy PHP + extension dari build stage
COPY --from=builder /usr/local /usr/local
COPY --from=builder /app /app
# Copy production dependencies OS
COPY --from=builder /usr/lib/x86_64-linux-gnu/libpq* /usr/lib/x86_64-linux-gnu/
COPY --from=builder /usr/lib/x86_64-linux-gnu/libicu* /usr/lib/x86_64-linux-gnu/
# Konfigurasi PHP
ENV PATH="/usr/local/bin:/usr/local/sbin:$PATH"
EXPOSE 9000
USER nonroot:nonroot
# Custom PHP-FPM config
COPY docker/php-fpm.conf /usr/local/etc/php-fpm.d/zz-app.conf
COPY docker/www.conf /usr/local/etc/php-fpm.d/www.conf
CMD ["php-fpm", "-F"]
Ukuran tipikal: 60-100 MB.
Catatan penting untuk distroless PHP:
- Distroless tidak punya shell, tidak punya
apt. Semua yang dibutuhkan runtime harus di-copy dari build stage. - PHP CLI + extensions di-copy dari
/usr/local(tempat PHP official install di Linux). - Library OS yang dibutuhkan (libpq, libicu) di-copy manual dari build stage.
- PHP-FPM config harus di-mount sebagai file, karena tidak bisa di-edit runtime.
php-fpm -Fartinya run di foreground (penting untuk container PID 1).- Debugging interaktif tidak memungkinkan — observability harus solid.
Kapan pakai: Production high-maturity, security-first, ukuran kritis.
5. Memilih Base Image PHP #
| Base Image | Ukuran | Catatan |
|---|---|---|
php:8.3-apache |
600-800 MB | Apache + mod_php + Debian, jangan pakai production |
php:8.3-fpm |
400-600 MB | PHP-FPM di Debian, default yang aman |
php:8.3-fpm-alpine |
120-200 MB | PHP-FPM di Alpine, kompromi terbaik |
php:8.3-fpm-slim |
200-300 MB | PHP-FPM di Debian slim |
| Distroless + custom | 60-100 MB | Paling ramping, butuh setup lebih |
Rekomendasi:
- Default:
php:8.3-fpm-alpine— ukuran wajar, debugging能力. - Production high-maturity: Distroless — ukuran kecil, observability solid.
- Development:
php:8.3-cli-alpine— ringan, ada composer built-in.
6. Extension Management #
Install Extension Resmi #
Image PHP official menyediakan script docker-php-ext-install untuk install extension yang bundled dengan PHP source:
RUN docker-php-ext-install \
pdo \
pdo_pgsql \
intl \
zip \
opcache
Install Extension dengan PECL #
Untuk extension yang tidak bundled (misal Redis, APCu, Imagick), pakai PECL:
RUN pecl install redis-6.0.2 \
&& docker-php-ext-enable redis
Install dari Source #
Untuk extension custom, compile dari source:
RUN apk add --no-cache $PHPIZE_DEPS \
librdkafka-dev \
&& pecl install rdkafka \
&& docker-php-ext-enable rdkafka \
&& apk del $PHPIZE_DEPS
Penting: Selalu bersihkan $PHPIZE_DEPS setelah install untuk mengecilkan image.
Production vs Dev Extension #
Pisahkan extension yang dibutuhkan runtime dari yang hanya untuk development:
Production wajib:
opcache— bytecode cache, wajib untuk performa.pdo+ driver database (pdo_pgsql, pdo_mysql).intl— internationalization.mbstring— string handling.bcmath— matematika presisi.zip/gd/imagick— sesuai kebutuhan aplikasi.
Development only (jangan di production):
xdebug— debugger, menambah ~10 MB.pcov/xdebug— code coverage.devpackage dariapk add(sepertigcc,make).
7. Composer Optimization #
Production Install Flag #
composer install \
--no-dev \ # Skip dev dependency
--no-scripts \ # Skip composer scripts
--prefer-dist \ # Pakai dist archive, bukan clone git
--classmap-authoritative \ # Autoloader strict, lebih cepat
--no-interaction \ # Non-interactive mode
--optimize-autoloader # Optimasi classmap
Bonus: Tambahkan --no-progress untuk log CI yang lebih bersih.
Autoloader Optimization #
Setelah install, generate autoloader yang optimal:
composer dump-autoload \
--optimize \ # PSR-0/PSR-4 -> classmap
--no-dev \ # Skip dev class
--classmap-authoritative # Class tidak akan di-cari di PSR-4 lagi
Untuk Laravel, ini bisa menurunkan memory usage dan mempercepat bootstrap.
Autoload Cache di CI #
Untuk build yang lebih cepat, cache Composer cache:
# GitHub Actions
- name: Cache Composer
uses: actions/cache@v4
with:
path: ~/.composer/cache
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
8. PHP Framework (Laravel / Symfony) #
Laravel Optimization #
Di build stage, jalankan optimasi Laravel:
# Pre-compile konfigurasi, route, view
RUN php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache
Pastikan APP_ENV=production saat compile agar cache yang dihasilkan sesuai dengan production.
Trick: php artisan optimize menjalankan semua cache di atas sekaligus.
Penting: Runtime image tidak boleh punya Node.js, npm, atau frontend build tools. Asset harus sudah di-compile di build stage dan di-copy ke runtime.
Symfony Optimization #
Untuk Symfony:
# Pre-compile container
RUN APP_ENV=prod php bin/console cache:clear --no-warmup \
&& APP_ENV=prod php bin/console cache:warmup
Atau untuk production build yang lebih ketat, pakai Composer autoloader optimization.
9. PHP-FPM Configuration #
Default PHP-FPM configuration sering tidak optimal untuk container. Buat custom config:
docker/php-fpm.d/zz-app.conf:
[www]
listen = 0.0.0.0:9000
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 5
pm.max_requests = 1000
Tuning untuk container:
pm.max_children— harus sesuai memory limit container. Formula:(container_memory_limit) / (avg_php_memory_per_child).pm.max_requests— set agar worker di-recycle secara berkala untuk mencegah memory leak.pm = dynamiclebih fleksibel daripm = staticuntuk traffic variabel.
OPCache configuration:
; /usr/local/etc/php/conf.d/opcache.ini
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=128
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0
opcache.revalidate_freq=0
opcache.fast_shutdown=1
Untuk production, opcache.validate_timestamps=0 artinya container harus di-restart saat kode berubah (immutable deployment). Ini sesuai dengan pola container.
10. Logging #
PHP-FPM default log ke /var/log/php-fpm.log. Untuk container, redirect ke STDOUT/STDERR:
docker/php-fpm.d/zz-app.conf:
[global]
error_log = /proc/self/fd/2
log_level = warning
[www]
access.log = /proc/self/fd/1
catch_workers_output = yes
/proc/self/fd/1= STDOUT/proc/self/fd/2= STDERR
Untuk Laravel, konfigurasi logging di config/logging.php agar pakai single channel dengan output STDOUT:
'channels' => [
'stderr' => [
'driver' => 'monolog',
'level' => 'debug',
'handler' => Monolog\Handler\StreamHandler::class,
'with' => [
'stream' => 'php://stderr',
],
],
],
11. Web Server Terpisah #
Best practice: Pisahkan PHP-FPM dari web server. Jangan pakai php:apache.
# docker-compose.yml
services:
app:
image: myapp:latest
expose:
- "9000"
web:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./public:/var/www/html/public:ro
depends_on:
- app
Keuntungan:
- Image PHP lebih ramping (tanpa Nginx/Apache).
- Scaling independen (Nginx bisa di-scale terpisah dari PHP-FPM).
- Web server bisa di-cache (Nginx cache static asset).
- Nginx static asset serving lebih cepat dari PHP.
12. Production-Grade Logging #
// config/logging.php (Laravel)
'channels' => [
'production' => [
'driver' => 'stack',
'channels' => ['stdout_json'],
],
'stdout_json' => [
'driver' => 'monolog',
'handler' => Monolog\Handler\StreamHandler::class,
'with' => [
'stream' => 'php://stdout',
],
'formatter' => Monolog\Formatter\JsonFormatter::class,
],
],
Konfigurasi ini:
- Log ke STDOUT (bukan file).
- Format JSON (mudah di-parse aggregator).
- Include trace ID jika pakai OpenTelemetry.
13. Security Hardening #
Non-Root User #
# Untuk alpine
RUN addgroup -g 1001 -S appgroup \
&& adduser -u 1001 -S appuser -G appgroup
USER appuser
# Untuk distroless, sudah ada nonroot
USER nonroot:nonroot
Permission File #
Pastikan file source code dimiliki user yang akan menjalankan PHP:
COPY --chown=appuser:appgroup . /app
Jangan Expose .env #
.env harus di-mount saat runtime, tidak pernah di-bake ke image.
# docker-compose.yml
services:
app:
env_file: .env
Pastikan .env ada di .dockerignore untuk jaga-jaga:
# .dockerignore
.env
.env.*
Vulnerability Scanning #
- name: Build
run: docker build -t myapp:${{ github.sha }} .
- name: Scan
run: trivy image --exit-code 1 --severity CRITICAL myapp:${{ github.sha }}
PHP image biasanya menarik banyak package OS (library untuk extension). Rebuild berkala untuk menarik patch.
14. Anti-Pattern yang Harus Dihindari #
✗ Pakai php:apache
#
// ✗ Image 600+ MB, Apache tidak scalable
FROM php:8.3-apache
COPY . /var/www/html/
Solusi: Pisahkan web server dari PHP-FPM. Pakai php:8.3-fpm-alpine.
✗ Composer di Runtime #
// ✗ Composer dan cache-nya ada di runtime
FROM php:8.3-fpm
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY . .
RUN composer install
Solusi: Multi-stage. Composer di build stage, copy vendor/ saja ke runtime.
✗ Install Dev Dependency #
// ✗ phpunit, debug tools ikut ke production
RUN composer install
Solusi: composer install --no-dev di production build.
✗ Build Tools Bocor #
// ✗ gcc, make, header file masuk runtime image
FROM php:8.3-fpm-alpine
RUN apk add $PHPIZE_DEPS gcc make
RUN docker-php-ext-install pdo_pgsql
Solusi: Build stage punya build tools, runtime stage tidak.
✗ Tidak Ada OPcache #
// ✗ PHP di-interpret setiap request (lambat)
FROM php:8.3-fpm
Solusi: Selalu install dan enable OPcache di production.
✗ Tag Tanpa Versi #
// ✗ Build tidak deterministik
FROM php:latest
FROM php:8
Solusi: Pin tag: php:8.3.11-fpm-alpine3.20.
15. Contoh Dockerfile PHP Production-Grade (Laravel) #
# syntax=docker/dockerfile:1.7
# ==== Stage 1: Composer ====
FROM composer:2.7.7 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
--no-dev \
--no-scripts \
--no-autoloader \
--prefer-dist \
--no-interaction
# ==== Stage 2: Frontend Assets ====
FROM node:20-alpine AS assets
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ==== Stage 3: Runtime ====
FROM php:8.3.11-fpm-alpine3.20
WORKDIR /app
# Install production extension
RUN apk add --no-cache \
libpq \
icu-libs \
oniguruma \
libzip \
libpng \
libjpeg-turbo \
freetype
RUN docker-php-ext-install -j$(nproc) \
pdo \
pdo_pgsql \
intl \
zip \
opcache \
bcmath
# OPcache config untuk production
COPY docker/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
# PHP-FPM config
COPY docker/php-fpm.conf /usr/local/etc/php-fpm.d/zz-app.conf
# Copy Composer dependencies
COPY --from=vendor /app/vendor /app/vendor
# Generate optimized autoloader
RUN composer dump-autoload --optimize --no-dev --classmap-authoritative
# Copy source code
COPY . .
# Copy built assets
COPY --from=assets /app/public/build /app/public/build
# Laravel optimization
ENV APP_ENV=production
RUN php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache
# Non-root user
RUN addgroup -g 1001 -S appgroup \
&& adduser -u 1001 -S appuser -G appgroup
USER appuser
EXPOSE 9000
CMD ["php-fpm"]
Karakteristik:
- Ukuran: 90-150 MB.
- Composer hanya di build stage.
- Frontend assets di build terpisah.
- PHP-FPM (bukan mod_php).
- OPcache optimal.
- Laravel pre-compiled.
- Non-root user.
16. Kapan Pakai Strategi Mana #
| Kondisi | Pilihan | Alasan |
|---|---|---|
| Laravel API standar | fpm-alpine + multi-stage |
Ukuran wajar, debugging能力 |
| Production high-maturity | Distroless | Ukuran minimal, observability solid |
| WordPress / CMS | fpm-alpine + custom config |
Butuh tool, plugin flexibility |
| Microservice | Distroless | Image kecil, attack surface minimal |
| Legacy dengan banyak extension | fpm-slim |
Butuh glibc, debugging能力 |
| Serverless | Distroless atau alpine | Cold start time penting |
17. Checklist Review Dockerfile PHP #
BASE IMAGE:
□ Tag eksplisit (php:8.3.11-fpm-alpine3.20, bukan latest)
□ Pakai FPM, bukan apache
□ Pakai alpine atau slim, bukan default debian
□ Bukan php:latest (terlalu besar)
BUILD:
□ Multi-stage build
□ Composer di build stage, copy vendor/ saja
□ composer install --no-dev --optimize-autoloader
□ Frontend assets di build terpisah (jika perlu)
□ Laravel optimize / Symfony cache di build
□ Build tool (gcc, make) hanya di build stage
EXTENSION:
□ Production extension hanya (no xdebug, no pcov)
□ docker-php-ext-install -j$(nproc) untuk paralel
□ apk del $PHPIZE_DEPS setelah install
RUNTIME:
□ USER nonroot
□ OPcache aktif dan dioptimasi
□ PHP-FPM config dituning
□ Log ke STDOUT
□ CMD dalam exec form
UKURAN:
□ < 200 MB untuk fpm-alpine
□ < 100 MB untuk distroless
□ docker history tidak ada layer aneh
KEAMANAN:
□ Tidak ada secret di image
□ .dockerignore tegas
□ .env di-exclude
□ Image di-scan trivy/grype
□ Non-root user
□ Base image up-to-date
FRAMEWORK:
□ Laravel: config:cache, route:cache, view:cache
□ Symfony: cache:clear --no-warmup + cache:warmup
□ Composer: --classmap-authoritative --optimize
Ringkasan #
- Image PHP ramping sangat mungkin — bukan legacy. Yang membedakan image 800 MB dengan image 90 MB adalah pilihan FPM (bukan apache), multi-stage, dan Composer terpisah.
- Realita ukuran: 60-100 MB (distroless), 90-150 MB (multi-stage + alpine), 120-200 MB (fpm-alpine), 400-600 MB (fpm default), 600-800 MB (apache — anti-pattern).
- Pakai PHP-FPM, bukan mod_php/apache — pisahkan web server dari PHP. Image lebih ramping, scaling lebih fleksibel.
- Multi-stage build wajib — Composer di build stage, copy
vendor/saja ke runtime. Composer dan cache-nya tidak boleh masuk runtime.- Composer install –no-dev –optimize-autoloader –classmap-authoritative — skip dev, optimalkan classmap, dan jangan generate autoloader yang lambat.
- Pisahkan dev vs prod extension — OPcache wajib production, xdebug/pcov hanya development. Build tool (gcc, make) hanya di build stage.
- Laravel/Symfony optimization — pre-compile config, route, view di build stage. Runtime tidak boleh punya Node.js atau build tools.
- OPcache konfigurasi optimal —
validate_timestamps=0untuk immutable deployment (container di-restart saat kode berubah).- Web server terpisah — Nginx di depan, PHP-FPM di belakang. Scaling independen, static asset lebih cepat.
- Log ke STDOUT —
/proc/self/fd/1dan/proc/self/fd/2untuk PHP-FPM. Laravel: konfigurasimonologdenganphp://stdout.- Tag eksplisit —
php:8.3.11-fpm-alpine3.20, bukanlatest. Reproducibility penting untuk audit dan rollback.- Image ramping butuh observability solid — log JSON, metrics, healthcheck, graceful shutdown. PHP bisa se-ramping Go dengan disiplin Dockerfile yang tepat.