Kode yang berjalan mulus di laptop sering menolak jalan di server: versi PHP berbeda, ekstensi lupa terpasang, atau konfigurasi sistem tidak sama persis. Docker menyelesaikan masalah parity tersebut dengan mengemas aplikasi bersama runtime PHP, ekstensi, dan seluruh dependensinya menjadi satu image yang dapat dijalankan identik di komputer mana pun. Container juga memberi isolation: beberapa aplikasi dengan kebutuhan berbeda dapat berbagi satu server tanpa saling mengganggu. Pada chapter ini kita akan menulis Dockerfile untuk aplikasi PHP, memisahkan tahap build dari runtime, menyusun stack lengkap dengan Docker Compose, dan menjaga image tetap ramping.
Masalah yang Diselesaikan Container #
Ada dua masalah klasik yang diselesaikan container. Pertama, parity lingkungan: tanpa container, setiap server punya sejarah instalasi yang berbeda sehingga “sudah jalan di komputer saya” tidak menjamin apa pun di produksi. Kedua, isolation: ketika dua aplikasi pada satu host membutuhkan versi PHP atau ekstensi yang berbeda, instalasi manual hampir pasti bertabrakan. Container mengemas proses bersama filesystem sendiri, sehingga aplikasi pertama dan aplikasi kedua masing-masing melihat runtime versinya sendiri.
Tabel berikut merangkum perbedaan praktisnya:
| Aspek | Tanpa container | Dengan container |
|---|---|---|
| Instalasi PHP | Manual per server, versi bisa berbeda | Versi PHP terkunci di dalam image |
| Ekstensi | Sering terlupa ikut terpasang | Dideklarasikan eksplisit di Dockerfile |
| Banyak aplikasi satu server | Rentan bentrok versi dan dependensi | Tiap container punya runtime terpisah |
| Membuat lingkungan baru | Mengikuti dokumentasi yang mudah basi | docker compose up dari definisi yang sama |
Dockerfile untuk Aplikasi PHP #
Dockerfile adalah resep membangun image: base image apa yang dipakai, ekstensi apa yang dipasang, dan berkas apa yang disalin. Untuk aplikasi PHP yang berjalan di belakang Nginx, base image yang tepat adalah varian fpm dari image resmi PHP, karena PHP-FPM adalah proses yang menerima request dari web server melalui protokol FastCGI (topik ini kita bahas tuntas pada chapter PHP-FPM
).
FROM php:8.4-fpm-alpine
RUN docker-php-ext-install pdo_mysql opcache
WORKDIR /var/www/app
COPY . .
RUN chown -R www-data:www-data /var/www/app
USER www-data
CMD ["php-fpm"]Mari kita bedah baris per baris:
FROM php:8.4-fpm-alpine: memulai dari image resmi PHP versi 8.4, varian FPM, dengan basis Alpine Linux yang kecil. Tag mengunci versi sehingga build berikutnya konsisten.RUN docker-php-ext-install pdo_mysql opcache: image resmi menyertakan skrip helperdocker-php-ext-installuntuk mengompilasi dan mengaktifkan ekstensi dari source, sehingga deklarasi ekstensi menjadi bagian dari image.WORKDIR /var/www/app: menetapkan direktori kerja untuk instruksiCOPY,RUN, danCMDberikutnya.COPY . .: menyalin kode aplikasi dari direktori build ke dalam image.RUN chown -R ...danUSER www-data: berkas dimiliki dan proses dijalankan sebagai user non-root. Bila proses di dalam container berhasil dieksploitasi, penyerang tidak langsung mendapat akses root.CMD ["php-fpm"]: perintah yang dijalankan saat container start, yaitu master process PHP-FPM.
Multi-Stage Build #
Instalasi dependensi Composer membutuhkan Composer itu sendiri dan berkas build sementara. Benda-benda tersebut tidak dibutuhkan saat aplikasi berjalan, tetapi ikut membuat image membengkak bila dijalankan pada satu tahap saja. Multi-stage build memecah proses menjadi beberapa tahap: tahap builder berisi alat build lengkap, tahap runtime hanya menyalin hasilnya.
FROM php:8.4-cli-alpine AS builder
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --prefer-dist
FROM php:8.4-fpm-alpine
RUN docker-php-ext-install pdo_mysql opcache
WORKDIR /var/www/app
COPY --from=builder /app/vendor ./vendor
COPY . .
USER www-data
CMD ["php-fpm"]Dua instruksi FROM membagi berkas menjadi dua tahap: tahap pertama diberi nama builder lewat AS, tahap kedua menjadi image final. Tahap builder menyalin binary Composer dari image resmi Composer, lalu menjalankan composer install --no-dev sehingga dependensi development seperti PHPUnit tidak ikut terpasang. Tahap runtime memulai ulang dari base image FPM yang bersih, hanya menyalin direktori vendor/ hasil tahap builder lewat COPY --from=builder, lalu melengkapi image dengan kode aplikasi. Composer, cache, dan berkas sementara tidak pernah mencapai image produksi.
Menjalankan Stack dengan Docker Compose #
PHP-FPM tidak berbicara HTTP; stack produksi minimal membutuhkan Nginx di depan dan database di belakang. Docker Compose mendefinisikan seluruh stack dalam satu berkas docker-compose.yml sehingga satu perintah docker compose up menjalankan semuanya.
services:
app:
build: .
environment:
APP_ENV: production
DB_HOST: db
DB_PORT: "3306"
DB_NAME: toko
depends_on:
db:
condition: service_healthy
volumes:
- storage:/var/www/app/storage
nginx:
image: nginx:1.27-alpine
ports:
- "8080:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
db:
image: mysql:8.4
environment:
MYSQL_DATABASE: toko
MYSQL_ROOT_PASSWORD: YOUR_DATABASE_PASSWORD
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
retries: 10
volumes:
storage:
db-data:Ada tiga hal penting pada definisi di atas. Pertama, nama service menjadi hostname: aplikasi PHP cukup mengisi DB_HOST=db karena jaringan internal Compose menerjemahkan nama db ke container database. Kedua, kunci environment menginjeksi environment variable ke container aplikasi — inilah satu-satunya jalur konfigurasi yang dibutuhkan karena image sudah memuat kode. Ketiga, depends_on dengan condition: service_healthy menahan start container app sampai database lulus healthcheck, mencegah aplikasi menyala sebelum database siap.
Volume versus Kode yang Dibake #
Kode aplikasi yang dibake ke image bersifat immutable: dua container dari image yang sama menjalankan kode yang identik bit per bit. Sebaliknya, data yang ditulis aplikasi saat berjalan (unggahan pengguna, log, berkas database) harus hidup di volume agar tidak hilang saat container diganti. Volume adalah penyimpanan yang dikelola Docker di luar filesystem container.
| Aspek | Kode di dalam image | Data di volume |
|---|---|---|
| Cara berubah | Build image baru, lalu deploy ulang | Ditulis aplikasi saat berjalan |
| Konsistensi antar container | Identik karena satu image | Tiap container volume-nya berbeda |
| Cocok untuk | Source code, direktori vendor/ | Unggahan, log, berkas database |
Membaca Environment Variable di Container #
Karena konfigurasi mengalir lewat environment variable, aplikasi perlu membacanya dengan aman termasuk menyiapkan nilai default. Fungsi getenv() mengembalikan nilai variable lingkungan, atau false bila variable tersebut tidak ada. Pada contoh berikut, putenv() dipakai untuk mensimulasikan injection yang dilakukan Compose sehingga kode dapat diuji di CLI:
<?php
function envConfig(string $nama, ?string $default = null): ?string
{
$nilai = getenv($nama);
return ($nilai === false || $nilai === "") ? $default : $nilai;
}
// simulasi bagian "environment:" pada docker-compose.yml
putenv("APP_ENV=production");
putenv("DB_HOST=db");
putenv("DB_USER=");
echo "APP_ENV=" . envConfig("APP_ENV") . "\n";
echo "DB_HOST=" . envConfig("DB_HOST") . "\n";
echo "DB_USER=" . envConfig("DB_USER", "app") . "\n";Output yang Dihasilkan:
APP_ENV=production
DB_HOST=db
DB_USER=appFungsi envConfig() memperlakukan variable kosong sama seperti variable yang tidak ada, sehingga DB_USER yang sengaja dikosongkan jatuh kembali ke default app. Pola ini menjaga aplikasi tetap hidup ketika operator lupa mengisi salah satu variable.
Untuk flag boolean seperti APP_DEBUG, string perlu diterjemahkan menjadi bool dengan aturan yang jelas. Fungsi filter_var() dengan FILTER_VALIDATE_BOOL menerima "true", "1", "on" sebagai benar dan "false", "0", "off" sebagai salah:
<?php
function envBool(string $nama, bool $default): bool
{
$nilai = getenv($nama);
if ($nilai === false || $nilai === "") {
return $default;
}
return filter_var($nilai, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? $default;
}
putenv("APP_DEBUG=false");
putenv("CACHE_ENABLED=1");
putenv("FITUR_BARU=");
var_dump(envBool("APP_DEBUG", true));
var_dump(envBool("CACHE_ENABLED", false));
var_dump(envBool("FITUR_BARU", false));Output yang Dihasilkan:
bool(false)
bool(true)
bool(false)Nilai "false" membuat APP_DEBUG bernilai false meskipun default-nya true — perhatikan bahwa string "false" justru bernilai truthy bila dikonversi langsung, jadi selalu gunakan FILTER_VALIDATE_BOOL alih-alih cast. Variable kosong (FITUR_BARU) dan variable yang tidak dikenal sama-sama memakai default. Daftar lengkap variable yang biasa dibaca aplikasi PHP dibahas pada chapter Environment Variables
.
Optimasi Ukuran Image #
Image yang kecil ditarik lebih cepat saat deploy dan memperkecil permukaan serangan karena berisi lebih sedikit alat yang bisa disalahgunakan. Empat teknik berikut memberi dampak terbesar:
- Base image Alpine — varian
-alpineberukuran jauh lebih kecil dibanding varian Debian. - Urutan layer — salin
composer.jsondancomposer.lockterlebih dahulu, jalankancomposer install, baru salin sisa kode. Selama lock file tidak berubah, layer instalasi dipakai ulang dari cache meski kode berubah. - Multi-stage build — alat build tidak ikut ke image final, seperti dicontohkan di atas.
--no-dev— dependensi development tidak diperlukan di produksi.
Berkas .dockerignore mencegah berkas yang tidak perlu masuk ke context build:
.git
vendor/
node_modules/
tests/
.env
storage/*.logImage Development versus Image Production #
Kebutuhan development dan produksi berbeda arah: development mengutamakan kecepatan iterasi, produksi mengutamakan ukuran dan keamanan. Keduanya tetap dibangun dari source yang sama, hanya berbeda tahap akhirnya:
| Aspek | Image development | Image production |
|---|---|---|
| Dependensi Composer | Termasuk --dev (PHPUnit, alat analisis) | Hanya --no-dev |
| Source code | Bind mount dari host, perubahan langsung terlihat | Dibake permanen ke image |
| Xdebug | Terpasang untuk profiling | Tidak terpasang |
| User proses | Bebas, sering root agar mudah | Non-root (www-data) |
| APP_DEBUG | true untuk melihat error detail | false |
Pola yang umum: satu Dockerfile dengan target multi-stage berbeda (--target dev versus tahap final), atau dua Dockerfile kecil yang berbagi base image yang sama.
Ringkasan #
Container menyelesaikan masalah parity dan isolation dengan mengunci runtime PHP, ekstensi, dan kode ke dalam satu image. Dockerfile adalah resepnya, multi-stage build menjaga image final ramping, Docker Compose menyusun stack app-Nginx-database dalam satu definisi, dan perbedaan lingkungan mengalir lewat environment variable alih-alih build berbeda. Dengan image yang siap, pertanyaan berikutnya adalah bagaimana memindahkannya ke server secara berulang, atomik, dan mudah dibatalkan — subjek chapter Deployment Workflows .