Lewati ke konten
getnextpdf.com

Mengontainerisasi aplikasi NextPDF

Anda menginginkan sebuah image Docker yang kecil dan reproducible yang menjalankan native engine NextPDF core yang berjalan in-processcomposer require nextpdf/core, menghasilkan PDF di dalam proses PHP Anda. Halaman ini membangun persis itu: sebuah image php:8.4 dengan hanya ekstensi yang benar-benar dibutuhkan engine, tanpa dependensi pengembangan di layer akhir, font terbundel, pengguna runtime non-root, opcache yang disetel untuk produksi, dan langkah verifikasi yang menggagalkan build jika ada yang hilang.

Halaman ini hanya untuk native engine. Chrome bridge (writeHtmlChrome melalui nextpdf/artisan) dan server Connect adalah runtime terpisah dengan image-nya sendiri yang lebih berat — instalasi headless Chromium untuk bridge, sebuah layanan yang berjalan lama untuk Connect. Jangan menambahkan browser atau server ke image ini; native engine tidak membutuhkan keduanya.

Sebelum Anda mulai, pastikan bagian-bagian ini telah tersedia:

  • Aplikasi Anda memiliki composer.json dan composer.lock yang ter-commit, dengan nextpdf/core sebagai dependensi.
  • Anda memiliki berkas font yang ingin Anda sematkan, dan Anda berlisensi untuk menyematkannya.
  • Anda dapat menjalankan docker build terhadap direktori aplikasi Anda.

Ini adalah how-to operasi. Hampir tidak ada PHP di sini; pekerjaannya adalah Dockerfile dan beberapa pengaturan lingkungan.

Image harus memenuhi constraint platform nyata engine, tidak lebih. Membacanya langsung dari paket, nextpdf/core membutuhkan php: >=8.4 <9.0 dan ekstensi PHP berikut:

EkstensiMengapa engine membutuhkannya
ext-mbstringPenanganan string multi-byte untuk teks dan pengkodean
ext-intlDukungan Unicode, locale, dan internasionalisasi
ext-gdPendekodean dan pemrosesan gambar raster
ext-opensslKriptografi untuk penandatanganan dan hashing aman
ext-zlibKompresi stream (Flate) atas object PDF
ext-curlKlien HTTP untuk panggilan keluar engine

Petakan itu ke image resmi php:8.4. openssl, curl, dan zlib sudah dikompilasi ke dalam image PHP resmi, jadi Anda tidak docker-php-ext-install keduanya. mbstring, gd, dan intl tidak dibundel dan harus dipasang, dan masing-masing membutuhkan system development header-nya hadir terlebih dahulu — mbstring selain itu membutuhkan dependensi build libonig-dev (Oniguruma). Jangan menambahkan ekstensi engine yang tidak dicantumkan paket — setiap docker-php-ext-install ekstra adalah build time dan attack surface yang tidak Anda butuhkan. Satu ekstensi non-engine yang dipasang image ini adalah opcache: ia adalah ekstensi performa runtime, tidak dibundel dalam keadaan aktif pada image resmi, dan penyetelan opcache di bawah bergantung pada keberadaannya (lihat “Opcache untuk produksi”).

Ini adalah build dua-stage. Stage pertama memasang dependensi Composer dengan paket pengembangan dikecualikan; stage kedua adalah image runtime ramping yang dikirim.

Pertama, tambahkan sebuah .dockerignore di samping Dockerfile. Tugas utamanya adalah menjaga lingkungan host — sebuah vendor/ yang dibangun di host, berkas secret lokal, dan cache build — sepenuhnya keluar dari konteks build, sehingga COPY . /var/www/app hanya mengirim apa yang Anda inginkan: build yang lebih kecil, lebih cepat, dan lebih aman yang tidak dapat membocorkan secret .env lokal atau membawa megabyte vendor/ host ke dalam image.

Mengecualikan vendor/ juga penting karena COPY sebuah direktori adalah merge, bukan replace. Dockerfile di bawah menjalankan RUN rm -rf /var/www/app/vendor sebelum COPY --from=vendor ... /var/www/app/vendor, sehingga dalam image ini sebuah vendor/ host tidak pernah dapat bertahan di bawah pohon dependensi yang bersih. Tetapi jika Anda pernah menghapus pengaman rm -rf itu, sebuah vendor/ yang dibangun di host dalam konteks akan mendarat lebih dulu dan salinan vendor-stage hanya akan menimpa path yang dimuat pohon bersih — berkas host ekstra apa pun (sebuah paket usang atau yang dipasang via dev, sebuah kelas yatim) akan kemudian bertahan di bawahnya. Menjaga vendor/ keluar dari konteks menutup celah itu terlepas dari rm -rf.

# .dockerignore — keep the host environment out of the build context.
vendor/
.git/
.env
.env.local
.env.*.local
var/cache/
storage/
node_modules/
*.log

Kecualikan berkas secret lokal yang sebenarnya (.env, .env.local, .env.*.local), bukan sebuah .env.* menyeluruh — wildcard itu juga membuang template non-secret seperti .env.example yang memang ingin Anda kirim sehingga image membawa baseline konfigurasi yang terdokumentasi. Pertahankan template env non-secret yang ter-commit dalam konteks; kecualikan hanya berkas yang benar-benar memuat secret lokal.

# syntax=docker/dockerfile:1
# ---- Stage 1: dependencies (no dev) ---------------------------------------
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
# Install production dependencies only. --no-dev excludes phpunit, phpstan,
# infection, and the other require-dev tooling from the shipped image.
# --optimize-autoloader builds a class map for the *vendor* tree here; the
# application's own classes are not present in this stage yet, so they are
# optimized after the source copy in the runtime stage (see below).
RUN composer install \
--no-dev \
--no-interaction \
--no-progress \
--prefer-dist \
--optimize-autoloader \
--no-scripts
# ---- Stage 2: runtime ------------------------------------------------------
FROM php:8.4-cli AS runtime
# System headers for the gd, intl, and mbstring extensions that need compiling.
# The PHP image already provides openssl, curl, and zlib, so those are NOT
# listed; gd, intl, and mbstring are installed below. opcache has no system
# headers and is installed in the same step. mbstring is built against
# Oniguruma, so libonig-dev is in the *-dev set and its runtime lib (libonig5)
# is preserved by the same detection below.
#
# Build the *-dev headers (which pull in the runtime libs), compile the
# extensions, then mark only the runtime shared libraries the extensions
# actually link against so they survive the --auto-remove purge of the headers.
# Removing libicu / libpng / libjpeg / libfreetype / libonig here would unlink
# intl.so, gd.so, or mbstring.so at runtime ("undefined symbol" / "cannot open
# shared object file").
RUN set -eux; \
savedAptMark="$(apt-mark showmanual)"; \
apt-get update; \
apt-get install -y --no-install-recommends \
libicu-dev \
libpng-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
libonig-dev; \
docker-php-ext-configure gd --with-freetype --with-jpeg; \
docker-php-ext-install -j"$(nproc)" gd intl mbstring opcache; \
# Detect the runtime .so dependencies of the just-built extensions and
# mark them manual so --auto-remove keeps them while dropping the headers.
apt-mark auto '.*' > /dev/null; \
apt-mark manual $savedAptMark > /dev/null; \
find /usr/local/lib/php/extensions -type f -name '*.so' -exec \
sh -c 'ldd "$1" 2>/dev/null \
| awk "/=>/ { print \$3 }" \
| grep -E "^/" \
| xargs -r dpkg-query -S 2>/dev/null \
| cut -d: -f1 \
| sort -u \
| xargs -r apt-mark manual' _ {} \; ; \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
rm -rf /var/lib/apt/lists/*
# Production opcache settings (see the opcache section below). The opcache
# extension is installed above (docker-php-ext-install opcache); this file only
# tunes it.
COPY docker/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
# A static Composer binary for the one optimized-autoloader rebuild below. It is
# copied into the build but the final stage runs no Composer at request time.
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/app
# Application code, then the vendor tree from the dependency stage. The .dockerignore
# should already keep a host vendor/ out of the context; the rm here is a second line
# of defense so a stale host-built vendor/ can never merge under the clean one (a
# directory COPY merges, it does not replace).
COPY . /var/www/app
RUN rm -rf /var/www/app/vendor
COPY --from=vendor /app/vendor /var/www/app/vendor
# Now that the application source is present, regenerate the optimized class map
# so the APP's own classes are in the optimized autoloader, not just the vendor
# packages. --no-dev keeps require-dev out; --no-scripts avoids running
# application hooks during the image build.
RUN composer dump-autoload \
--optimize \
--no-dev \
--no-interaction \
--no-scripts \
&& rm -f /usr/bin/composer
# The bundled fonts live at /var/www/app/resources/fonts. The native engine does
# NOT read any font-path environment variable — the entrypoint registers that
# directory in PHP (see "Bundle fonts into the image" below). There is no ENV
# line for fonts here.
# Run as a non-root user (see the non-root section below).
RUN useradd --system --no-create-home --uid 10001 appuser \
&& chown -R appuser:appuser /var/www/app
USER appuser
CMD ["php", "bin/generate.php"]

Stage dependensi berjalan dengan --no-scripts sehingga tidak ada hook post-install aplikasi yang berjalan terhadap pohon yang tidak lengkap; jalankan langkah build aplikasi apa pun (kompilasi aset, pemanasan cache) di stage selanjutnya setelah kode disalin.

Instalasi Composer multi-stage (tanpa dependensi dev)

Bagian berjudul “Instalasi Composer multi-stage (tanpa dependensi dev)”

Image yang dikirim tidak boleh memuat perkakas pengembangan. Flag --no-dev pada composer install adalah baris yang load-bearing: ia melewati segala sesuatu di bawah require-dev pada nextpdf/core dan aplikasi Anda — test runner, static analyzer, dan alat mutasi — yang tidak satu pun memiliki tempat di produksi. Pasangkan dengan --optimize-autoloader sehingga autoloader adalah class map yang dihasilkan alih-alih pemindaian filesystem pada setiap permintaan.

Salin composer.json dan composer.lock sebelum sisa source sehingga Docker men-cache layer dependensi dan hanya me-resolve ulang ketika berkas lock berubah. Karena instalasi pertama itu berjalan terhadap berkas lock saja — tanpa source aplikasi — --optimize-autoloader di sana membangun class map untuk pohon vendor saja; kelas-kelas aplikasi Anda sendiri belum ada. Itulah mengapa stage runtime menjalankan composer dump-autoload --optimize --no-dev --no-scripts sekali setelah menyalin source: ia melipat kelas-kelas aplikasi ke dalam class map teroptimasi yang sama. Jangan menjalankan composer dump-autoload terpisah di sebuah worktree yang juga Anda kembangkan (ia akan meng-commit class map produksi ke pohon dev); rebuild itu berada di dalam image, setelah salinan source, seperti ditunjukkan di atas.

Native engine me-resolve font dari berkas font yang dapat dibacanya, bukan dari font terpasang OS. Memasang paket fonts-* atau menjalankan fc-cache tidak melakukan apa pun yang dapat dilihat jalur native, sehingga image ini tidak memasang font sistem. Bundel berkas .ttf / .otf Anda di bawah resources/fonts/; COPY . /var/www/app di atas sudah membawanya ke dalam image.

Memasukkan berkas ke dalam image hanyalah separuh pekerjaan. Native engine polos tidak membaca variabel lingkungan pencarian-font — NEXTPDF_FONTS_PATH adalah nilai default dari kunci config fonts_path paket nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))) dan hanya dikonsumsi oleh integrasi framework itu, bukan oleh nextpdf/core. Sebuah entrypoint php bin/generate.php polos dengan hanya variabel itu yang disetel tidak mendaftarkan font dan merender tofu yang sama yang ingin dicegah keberadaan image ini. Entrypoint harus mendaftarkan direktori terbundel di PHP:

use NextPDF\Typography\FontRegistry;
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
// Register the directory the Dockerfile bundled the fonts into.
$registry = new FontRegistry('/var/www/app/resources/fonts');
// (equivalently, $registry->addFontDirectory('/var/www/app/resources/fonts');)
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
$doc = $factory->create();

Itulah seluruh urusan Docker untuk font. Aturan penamaan berkas, API registry, pola warmup-and-lock, dan penanganan filesystem read-only semuanya berada di halaman tersendiri — jangan menduplikasinya di sini. Baca Menyediakan font untuk native engine di produksi untuk pola selengkapnya, dan daftarkan direktori yang sama yang Anda bundel.

Image PHP resmi berjalan sebagai root secara default. Sebuah generator PDF tidak membutuhkan root, jadi buat pengguna tanpa hak istimewa dan beralih ke sana. Dockerfile di atas menambahkan pengguna sistem appuser dengan UID tinggi yang tetap (10001), memberinya kepemilikan pohon aplikasi, dan berakhir dengan USER appuser sehingga setiap proses yang dimulai kontainer tanpa hak istimewa.

Jaga aplikasi tetap read-only saat runtime jika memungkinkan. Engine membaca berkas font-nya dan hanya menulis keluarannya dan sebuah cache font-terurai opsional, sehingga sebuah kontainer readOnlyRootFilesystem bekerja selama path keluaran dan direktori cache apa pun adalah mount yang dapat ditulis. Gabungkan ini dengan Linux capabilities yang dibuang dan sebuah flag no-new-privileges pada orchestrator Anda untuk defense in depth.

Opcache bermanfaat untuk PHP worker yang berumur panjang — sebuah pool FPM atau proses Apache mod_php yang melayani banyak permintaan dari satu proses yang hangat. Proses-proses itu mengompilasi kelas Anda sekali lalu tidak pernah men-stat berkas source pada hot path, yang persis dibeli oleh opcache.validate_timestamps=0. Opcache tidak aktif secara default pada image resmi php:8.4, sehingga Dockerfile di atas memasangnya dengan docker-php-ext-install opcache (Anda dapat setara dengan docker-php-ext-enable opcache jika ekstensi sudah dikompilasi). Berkas conf.d di bawah adalah penyetelan, bukan langkah pengaktifan — ia tidak melakukan apa pun hingga ekstensi dimuat. Kirim sebagai sebuah include conf.d (docker/opcache.ini, disalin dalam Dockerfile):

opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=192
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0

opcache.validate_timestamps=0 berarti cache tidak pernah memeriksa ulang berkas source — benar untuk image imutabel, karena satu-satunya cara kode berubah adalah sebuah image baru. Setel memory_consumption dan max_accelerated_files ke jumlah kelas aplikasi Anda.

CMD yang ditunjukkan adalah generator CLI sekali-jalan, dan opcache.enable_cli=0 benar untuknya. Sebuah proses php bin/generate.php berumur pendek dimulai, mengompilasi, merender sekali, dan keluar, sehingga cache opcode yang tidak dapat dibagikannya dengan permintaan berikutnya tidak memberi manfaat — biarkan opcache CLI mati dan tidak membayar biaya memorinya. Opcache hanya bermanfaat di tempat prosesnya digunakan kembali: sebuah SAPI FPM/Apache, atau worker CLI yang benar-benar berjalan lama (sebuah konsumen queue atau server gaya RoadRunner). Hanya worker CLI residen semacam itu yang akan menyetel opcache.enable_cli=1; untuk generator sekali-jalan di sini, biarkan 0.

Jika Anda memang menjalankan setup yang menggunakan preloading opcache (sebuah worker FPM berumur panjang dengan skrip opcache.preload), setel opcache.preload=/path/to/preload.php dan tambahkan opcache.preload_user=appuser sehingga preload berjalan sebagai pengguna tanpa hak istimewa. Tanpa sebuah skrip opcache.preload yang sebenarnya, opcache.preload_user tidak melakukan apa pun, itulah mengapa ia tidak ada di config baseline di atas — jangan menambahkannya kecuali Anda juga menyetel opcache.preload.

Tambahkan langkah verifikasi sehingga image yang salah-build gagal dengan lantang alih-alih menghasilkan tofu atau fatal pada permintaan pertama. NextPDF mengirim sebuah CLI yang perintah doctor-nya memeriksa lingkungan PHP yang berjalan dan melaporkan persis ekstensi yang diperhatikan engine — openssl, zlib, mbstring, gd, curl, dan intl. Paket mendeklarasikan "bin": ["bin/nextpdf"], sehingga dalam aplikasi yang mengonsumsi Composer memasang executable di vendor/bin/nextpdf (bukan bin/nextpdf, yang merupakan path di dalam paket nextpdf/core itu sendiri). Jalankan di dalam image yang dibangun:

Terminal window
docker run --rm your-app:latest php vendor/bin/nextpdf doctor

Hasil yang sehat mengonfirmasi PHP 8.4 dan setiap ekstensi yang diperlukan dimuat. Kaitkan pemanggilan yang sama ke dalam build (atau sebuah CI smoke job) sehingga ekstensi yang hilang menghentikan pipeline:

Terminal window
# Fail the pipeline if the engine's environment is not healthy.
docker run --rm your-app:latest php vendor/bin/nextpdf doctor || exit 1

Untuk pemeriksaan end-to-end, render satu halaman melalui entrypoint Anda sendiri dan tegaskan pada keluarannya, seperti yang dijelaskan halaman font untuk sebuah pemeriksaan smoke font.

  • php:8.4-fpm atau -apache alih-alih -cli. Gunakan SAPI yang benar-benar dilayani aplikasi Anda. List ekstensi identik; hanya tag base dan CMD/entrypoint yang berbeda. Untuk queue worker atau batch job CLI, -cli benar.
  • Alpine (php:8.4-alpine) membutuhkan nama paket yang berbeda. Baris apt-get di atas adalah untuk image default berbasis Debian. Pada Alpine, pasang header *-dev sebagai sebuah grup build virtual (apk add --no-cache --virtual .build-deps icu-dev libpng-dev freetype-dev libjpeg-turbo-dev oniguruma-dev) dan, setelah langkah docker-php-ext-install gd intl mbstring opcache, apk del .build-deps — tetapi pertama apk add --no-cache library runtime yang ditautkan ekstensi (icu-libs, libpng, freetype, libjpeg-turbo, oniguruma) sehingga menghapus grup build tidak meng-unlink intl.so / gd.so / mbstring.so. Ini adalah aturan keep-the-runtime-libs yang sama yang ditegakkan blok Debian dengan apt-mark.
  • Jangan memasang paket fonts-*. Mereka tidak terlihat oleh native engine. Bundel berkas font sebagai gantinya — lihat halaman font yang ditautkan di atas.
  • Premium dan ionCube adalah urusan image yang berbeda. Build NextPDF Pro / Enterprise yang dienkode ionCube membutuhkan ionCube Loader terpasang di image dan dicocokkan dengan build PHP persis kontainer (8.4, NTS vs. ZTS). Itu berada di luar cakupan image core; jika Anda men-deploy premium, ikuti bagian Docker dari Pengaturan ionCube Loader.
  • Jaga sebuah vendor/ host keluar dari konteks build. .dockerignore (mengecualikan vendor/, .git/, dan cache lokal) menjaga pohon host keluar dari konteks sepenuhnya — itulah yang membuat build kecil, cepat, dan bebas dari secret lokal yang bocor. Ia juga menjaga kasus directory-merge: sebuah COPY direktori adalah merge, bukan replace, sehingga sebuah vendor/ yang dibangun di host yang mencapai konteks akan mendarat lebih dulu dan COPY --from=vendor /app/vendor /var/www/app/vendor hanya akan menimpa path yang dimuat pohon dependensi bersih. Dalam Dockerfile ini, RUN rm -rf /var/www/app/vendor sebelum salinan vendor sudah menghapus direktori semacam itu, sehingga residu itu tidak dapat terjadi di sini; risiko merge hanya kembali jika Anda menghapus pengaman rm -rf itu, itulah mengapa pengecualian .dockerignore adalah perbaikan yang tahan lama.
  • Jangan kirim dependensi dev. --no-dev menjaga perkakas test dan analisis, serta paket transitifnya, keluar dari image runtime dan attack surface-nya.
  • Jalankan tanpa hak istimewa. USER appuser akhir memastikan tidak ada proses kontainer yang berjalan sebagai root. Pasangkan dengan root filesystem read-only dan capabilities yang dibuang pada orchestrator Anda.
  • Pin base image. Pin php:8.4 ke sebuah digest di produksi sehingga sebuah rebuild tidak dapat diam-diam menarik base yang berubah, dan rebuild pada kadensi untuk mengambil patch keamanan secara sengaja.
  • Jaga font dan lisensi keluar dari layer publik. Bundel hanya font yang Anda berlisensi untuk menyematkannya, dan jangan pernah menanamkan berkas lisensi premium ke dalam image yang di-push secara publik — mount saat runtime sebagai gantinya.

Panduan ini tidak membuat klaim standar normatif. Fakta platform dibaca langsung dari paket nextpdf/core: constraint php: >=8.4 <9.0 dan ekstensi yang diperlukan ext-mbstring, ext-intl, ext-gd, ext-openssl, ext-zlib, dan ext-curl. Perintah verifikasi adalah handler doctor CLI nextpdf yang sebenarnya — dideklarasikan sebagai "bin": ["bin/nextpdf"] pada nextpdf/core dan karena itu terpasang di vendor/bin/nextpdf pada aplikasi yang mengonsumsi — yang melaporkan set ekstensi yang sama. Native engine mendaftarkan font melalui NextPDF\Typography\FontRegistry (argumen constructor direktori / addFontDirectory()) yang dikaitkan via NextPDF\Core\DocumentFactory; NEXTPDF_FONTS_PATH adalah kunci config fonts_path paket nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))), bukan variabel yang dibaca nextpdf/core. Perilaku registry didokumentasikan pada halaman font yang ditautkan di bawah Lihat juga.