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:

AspekTanpa containerDengan container
Instalasi PHPManual per server, versi bisa berbedaVersi PHP terkunci di dalam image
EkstensiSering terlupa ikut terpasangDideklarasikan eksplisit di Dockerfile
Banyak aplikasi satu serverRentan bentrok versi dan dependensiTiap container punya runtime terpisah
Membuat lingkungan baruMengikuti dokumentasi yang mudah basidocker 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 ).

dockerfile
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:

  1. 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.
  2. RUN docker-php-ext-install pdo_mysql opcache: image resmi menyertakan skrip helper docker-php-ext-install untuk mengompilasi dan mengaktifkan ekstensi dari source, sehingga deklarasi ekstensi menjadi bagian dari image.
  3. WORKDIR /var/www/app: menetapkan direktori kerja untuk instruksi COPY, RUN, dan CMD berikutnya.
  4. COPY . .: menyalin kode aplikasi dari direktori build ke dalam image.
  5. RUN chown -R ... dan USER www-data: berkas dimiliki dan proses dijalankan sebagai user non-root. Bila proses di dalam container berhasil dieksploitasi, penyerang tidak langsung mendapat akses root.
  6. 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.

dockerfile
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.

yaml
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.

AspekKode di dalam imageData di volume
Cara berubahBuild image baru, lalu deploy ulangDitulis aplikasi saat berjalan
Konsistensi antar containerIdentik karena satu imageTiap container volume-nya berbeda
Cocok untukSource 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
<?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:

text
APP_ENV=production
DB_HOST=db
DB_USER=app

Fungsi 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
<?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:

text
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:

  1. Base image Alpine — varian -alpine berukuran jauh lebih kecil dibanding varian Debian.
  2. Urutan layer — salin composer.json dan composer.lock terlebih dahulu, jalankan composer install, baru salin sisa kode. Selama lock file tidak berubah, layer instalasi dipakai ulang dari cache meski kode berubah.
  3. Multi-stage build — alat build tidak ikut ke image final, seperti dicontohkan di atas.
  4. --no-dev — dependensi development tidak diperlukan di produksi.

Berkas .dockerignore mencegah berkas yang tidak perlu masuk ke context build:

text
.git
vendor/
node_modules/
tests/
.env
storage/*.log

Image 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:

AspekImage developmentImage production
Dependensi ComposerTermasuk --dev (PHPUnit, alat analisis)Hanya --no-dev
Source codeBind mount dari host, perubahan langsung terlihatDibake permanen ke image
XdebugTerpasang untuk profilingTidak terpasang
User prosesBebas, sering root agar mudahNon-root (www-data)
APP_DEBUGtrue untuk melihat error detailfalse

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 .

Referensi #