容器化 NextPDF 應用程式
你想要一個小型、可重現的 Docker 映像,執行原生且在處理程序內的 NextPDF
core 引擎——composer require nextpdf/core,在你的 PHP 處理程序內產生 PDF。本頁建置的正是那個:一個只帶引擎實際所需擴充的 php:8.4 映像,最終層中無開發相依套件、已打包的字型、一個非 root 的執行時使用者、為正式環境調校的 opcache,以及一個在缺少任何東西時讓建置失敗的驗證步驟。
本頁只針對原生引擎。Chrome 橋接
(透過 nextpdf/artisan 的 writeHtmlChrome)與 Connect 伺服器是各自獨立的執行環境,有它們自己、更重的映像——橋接需要安裝無頭 Chromium,Connect 則是一個長存的服務。請勿在這個映像中加入瀏覽器或伺服器;原生引擎兩者皆不需要。
開始前,請確認下列要件都已就位:
- 你的應用程式有一份已提交的
composer.json與composer.lock,並以nextpdf/core作為相依套件。 - 你擁有你打算嵌入的字型檔,且你有權嵌入它們。
- 你能對你的應用程式目錄執行
docker build。
這是一篇維運操作指南。這裡幾乎沒有 PHP;工作落在 Dockerfile 與一些環境設定上。
引擎實際需要什麼
標題為「引擎實際需要什麼」的區段映像必須滿足引擎真正的平台限制,不多不少。直接從套件讀取,nextpdf/core 需要
php: >=8.4 <9.0 與這些 PHP 擴充:
| 擴充 | 引擎為何需要它 |
|---|---|
ext-mbstring | 文字與編碼的多位元組字串處理 |
ext-intl | Unicode、地區設定與國際化支援 |
ext-gd | 點陣影像解碼與處理 |
ext-openssl | 簽署與安全雜湊的密碼學 |
ext-zlib | PDF 物件的串流(Flate)壓縮 |
ext-curl | 引擎對外呼叫的 HTTP 用戶端 |
把那些對應到官方的 php:8.4 映像。openssl、curl 與 zlib 已編入官方 PHP 映像,因此你不用
docker-php-ext-install 它們。mbstring、gd 與 intl 則未隨附,必須安裝,而且各需要它們的系統開發標頭先就位——mbstring 另外需要 libonig-dev(Oniguruma)建置相依套件。請勿加入套件未列出的引擎擴充——每個額外的 docker-php-ext-install 都是你不需要的建置時間與攻擊面。這個映像確實安裝的唯一非引擎擴充是 opcache:它是一個執行時效能擴充,未在官方映像上隨附啟用,而下方的 opcache 調校仰賴它存在(見「為正式環境調校 opcache」)。
正式環境 Dockerfile
標題為「正式環境 Dockerfile」的區段這是一個兩階段建置。第一階段安裝排除開發套件的 Composer 相依套件;第二階段是出貨的精簡執行時映像。
先在 Dockerfile 旁加入一個 .dockerignore。它的首要工作是把主機環境——一個主機建置的 vendor/、本地祕密檔案與建置快取——完全擋在建置情境之外,讓 COPY . /var/www/app 只出貨你打算出貨的內容:更小、更快、更安全的建置,既不會洩漏本地的 .env 祕密,也不會把數十 MB 的主機 vendor/ 帶進映像。
排除 vendor/ 還很重要,因為目錄 COPY 是一個合併,不是取代。下方的 Dockerfile 會在
COPY --from=vendor ... /var/www/app/vendor 之前執行 RUN rm -rf /var/www/app/vendor,因此在這個映像中,主機的 vendor/ 絕不可能在乾淨的相依樹下倖存。但如果你日後移除那道 rm -rf 防護,建置情境中的主機建置 vendor/ 會先落地,而 vendor 階段的複製只會覆寫乾淨樹所包含的路徑——任何額外的主機檔案(一個陳舊或以 dev 安裝的套件、一個孤立的類別)便會在其下倖存。把 vendor/ 擋在情境之外,無論 rm -rf 是否存在都封堵了那個漏洞。
# .dockerignore — keep the host environment out of the build context.vendor/.git/.env.env.local.env.*.localvar/cache/storage/node_modules/*.log排除真正的本地祕密檔案(.env、.env.local、.env.*.local),而非全面的
.env.*——那個萬用字元也會丟棄諸如 .env.example 的非祕密範本,而你確實想出貨它,好讓映像攜帶一個有記載的設定基準。請把任何已提交的非祕密 env 範本留在情境中;只排除那些真正含有本地祕密的檔案。
# syntax=docker/dockerfile:1
# ---- Stage 1: dependencies (no dev) ---------------------------------------FROM composer:2 AS vendor
WORKDIR /appCOPY 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/appRUN rm -rf /var/www/app/vendorCOPY --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/appUSER appuser
CMD ["php", "bin/generate.php"]相依階段以 --no-scripts 執行,因此不會有任何應用程式 post-install 鉤子對不完整的樹執行;請在程式碼複製後的稍後階段,執行任何應用程式建置步驟(資產編譯、快取暖機)。
多階段 Composer 安裝(無開發相依套件)
標題為「多階段 Composer 安裝(無開發相依套件)」的區段出貨的映像不得含有開發工具。composer install 上的 --no-dev 旗標是承重的那一行:它略過 nextpdf/core 與你應用程式中 require-dev 底下的一切——測試執行器、靜態分析器與變異工具——這些在正式環境中都毫無立足之地。把它與
--optimize-autoloader 搭配,讓 autoloader 是一個產生的類別映射,而非每請求對檔案系統的掃描。
在其餘原始碼之前複製 composer.json 與 composer.lock,讓 Docker 快取相依層,並只在 lock 檔變更時才重新解析。由於那次首次安裝只對 lock 檔執行——沒有應用程式原始碼——--optimize-autoloader 在那裡只為 vendor 樹建置類別映射;你應用程式自己的類別尚未存在。這就是為什麼執行時階段在複製原始碼之後執行一次
composer dump-autoload --optimize --no-dev --no-scripts:它把應用程式的類別折入同一個最佳化的類別映射中。請勿在一個你也在其中開發的 worktree 裡執行另一次 composer dump-autoload(那會把一個正式環境的類別映射提交進開發樹);重建屬於映像之中,在原始碼複製之後,如上所示。
把字型打包進映像
標題為「把字型打包進映像」的區段原生引擎從它能讀取的字型檔解析字型,而非從 OS 安裝的字型。安裝 fonts-* 套件或執行
fc-cache 對原生路徑可見的東西毫無作用,因此這個映像不安裝任何系統字型。請把你的
.ttf / .otf 檔打包在 resources/fonts/ 之下;上方的 COPY . /var/www/app
已會把它們帶進映像。
把檔案弄進映像只完成了一半工作。裸的原生引擎不讀取任何字型搜尋環境變數——
NEXTPDF_FONTS_PATH 是 nextpdf/laravel 套件 fonts_path 設定鍵的預設值
(env('NEXTPDF_FONTS_PATH', resource_path('fonts'))),且只由那個框架整合消費,而非由 nextpdf/core 消費。一個只設定了那個變數的純
php bin/generate.php 進入點不會註冊任何字型,並會算繪出這個映像存在所要防止的同樣豆腐字。進入點必須在 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();那就是字型方面的整個 Docker 顧慮。檔案命名規則、登錄表 API、暖機與鎖定模式,以及唯讀檔案系統處理,全都存放在專屬頁面上——請勿在此重複它們。請閱讀 在正式環境為原生引擎佈建字型 了解完整模式,並註冊你所打包的同一個目錄。
以非 root 使用者執行
標題為「以非 root 使用者執行」的區段官方 PHP 映像預設以 root 執行。一個 PDF 產生器不需要 root,因此請建立一個非特權使用者並切換到它。上方的 Dockerfile 加入一個系統使用者 appuser,帶有固定的高 UID(10001),把應用程式樹的擁有權給它,並以 USER appuser 結束,讓容器啟動的每個處理程序都是非特權的。
請在你能做到之處讓應用程式在執行時保持唯讀。引擎讀取它的字型檔,並只寫入它的輸出與一個選用的已剖析字型快取,因此只要輸出路徑與任何快取目錄是可寫掛載,一個
readOnlyRootFilesystem 容器便可運作。把它與你協調器中卸除的 Linux capabilities 及一個
no-new-privileges 旗標結合,以達到縱深防禦。
為正式環境調校 opcache
標題為「為正式環境調校 opcache」的區段opcache 在長存的 PHP worker 上才划算——一個 FPM 池或一個從單一暖處理程序服務眾多請求的 Apache
mod_php 處理程序。那些處理程序把你的類別編譯一次,之後在熱路徑上永不 stat 原始檔,這正是
opcache.validate_timestamps=0 為你買到的。opcache 在官方 php:8.4 映像上並非開箱即啟用,因此上方的 Dockerfile 以
docker-php-ext-install opcache 安裝它(若擴充已編譯,你可等效地
docker-php-ext-enable opcache)。下方的 conf.d 檔是調校,不是啟用步驟——在擴充被載入之前它毫無作用。請把它以一個 conf.d include(docker/opcache.ini,在 Dockerfile 中複製進去)出貨:
opcache.enable=1opcache.enable_cli=0opcache.memory_consumption=192opcache.interned_strings_buffer=16opcache.max_accelerated_files=20000opcache.validate_timestamps=0opcache.validate_timestamps=0 意味著快取永不重新檢查原始檔——對一個不可變映像而言正確,因為程式碼改變的唯一方式就是一個新映像。請依你應用程式的類別數量調校
memory_consumption 與 max_accelerated_files。
所示的 CMD 是一個一次性的 CLI 產生器,而 opcache.enable_cli=0 對它而言是正確的。 一個短命的
php bin/generate.php 處理程序啟動、編譯、算繪一次然後退出,因此它無法與下個請求共享的 opcode 快取毫無益處——讓 CLI opcache 維持關閉,且不付出它任何記憶體成本。opcache 只有在處理程序被重用時才賺回它的價值:一個 FPM/Apache SAPI,或一個真正長存的 CLI worker(一個佇列消費者或一個 RoadRunner 風格的伺服器)。只有那種常駐的 CLI worker 才會設定
opcache.enable_cli=1;對此處的一次性產生器,請維持它為 0。
如果你確實執行一個使用 opcache preloading 的設定(一個帶 opcache.preload 腳本的長存 FPM worker),請設定 opcache.preload=/path/to/preload.php,並加上
opcache.preload_user=appuser,讓 preload 以非特權使用者執行。在沒有實際的 opcache.preload 腳本時,opcache.preload_user 毫無作用,這就是它不在上方基準設定中的原因——除非你也設定了 opcache.preload,否則請勿加入它。
驗證映像
標題為「驗證映像」的區段加入一個驗證步驟,讓一個錯誤建置的映像大聲失敗,而非在第一個請求時產生豆腐字或一個 fatal。NextPDF 隨附一個 CLI,其 doctor 指令檢視執行中的 PHP 環境,並就引擎在意的確切擴充回報——openssl、zlib、mbstring、gd、curl 與 intl。套件宣告了 "bin": ["bin/nextpdf"],因此在一個消費端應用程式中,Composer 把該執行檔安裝在 vendor/bin/nextpdf(而非
bin/nextpdf,那是 nextpdf/core 套件自己內部的路徑)。在建好的映像中執行它:
docker run --rm your-app:latest php vendor/bin/nextpdf doctor一個健康的結果確認 PHP 8.4 與每個必要擴充都已載入。把同一個呼叫接入建置(或一個 CI 煙霧任務)中,讓一個缺失的擴充使管線停止:
# Fail the pipeline if the engine's environment is not healthy.docker run --rm your-app:latest php vendor/bin/nextpdf doctor || exit 1至於端到端檢查,請透過你自己的進入點算繪一頁並斷言輸出,如字型頁面為字型煙霧檢查所描述的那樣。
邊角情況與陷阱
標題為「邊角情況與陷阱」的區段- 使用
php:8.4-fpm或-apache而非-cli。 使用你應用程式實際服務所依的 SAPI。擴充清單相同;只有基底標籤與CMD/進入點不同。對於佇列 worker 或 CLI 批次工作,-cli是正確的。 - Alpine(
php:8.4-alpine)需要不同的套件名稱。 上方的apt-get各行是針對 Debian 基底的預設映像。在 Alpine 上,把*-dev標頭安裝為一個虛擬建置群組(apk add --no-cache --virtual .build-deps icu-dev libpng-dev freetype-dev libjpeg-turbo-dev oniguruma-dev),並在docker-php-ext-install gd intl mbstring opcache步驟之後執行apk del .build-deps——但要先apk add --no-cache擴充所連結的執行時函式庫(icu-libs、libpng、freetype、libjpeg-turbo、oniguruma),讓刪除建置群組不會解連intl.so/gd.so/mbstring.so。這與 Debian 區塊以apt-mark強制的保留執行時函式庫規則相同。 - 請勿安裝
fonts-*套件。 它們對原生引擎不可見。請改打包字型檔——見上方連結的字型頁面。 - Premium 與 ionCube 是另一個映像的顧慮。 ionCube 編碼的 NextPDF Pro / Enterprise 建置需要在映像中安裝 ionCube Loader,並與容器的確切 PHP 建置相符(8.4、NTS 與 ZTS)。那超出 core 映像的範圍;如果你部署 premium,請遵循 ionCube Loader 設定 的 Docker 章節。
- 把主機的
vendor/擋在建置情境之外。.dockerignore(排除vendor/、.git/與本地快取)把主機樹完全擋在情境之外——這正是讓建置小、快且不洩漏本地祕密的原因。它也防護目錄合併情況:目錄COPY是一個合併,不是取代,因此一個到達情境的主機建置vendor/會先落地,而COPY --from=vendor /app/vendor /var/www/app/vendor只會覆寫乾淨相依樹所包含的路徑。在這個 Dockerfile 中,vendor 複製之前的RUN rm -rf /var/www/app/vendor已移除任何這樣的目錄,因此那種殘留在此不會發生;合併風險只有在你拿掉那道rm -rf防護時才回來,這就是為什麼.dockerignore排除是持久的修正。
安全性備註
標題為「安全性備註」的區段- 不出貨開發相依套件。
--no-dev把測試與分析工具及其遞移套件擋在執行時映像及其攻擊面之外。 - 以非特權執行。 最後的
USER appuser確保沒有容器處理程序以 root 執行。把它與你協調器中的唯讀根檔案系統及卸除的 capabilities 結合。 - 釘住基底映像。 在正式環境中把
php:8.4釘到一個 digest,讓重建不會默默拉到一個改變過的基底,並以一定節奏重建以刻意地接收安全修補。 - 把字型與授權擋在公開層之外。 只打包你有權嵌入的字型,並絕不把一份 premium 授權檔烘焙進一個公開推送的映像中——請改在執行時掛載它。
規範性
標題為「規範性」的區段本指南不作任何規範性標準主張。平台事實直接從 nextpdf/core 套件讀取:php: >=8.4 <9.0 限制與必要擴充 ext-mbstring、ext-intl、ext-gd、ext-openssl、
ext-zlib 與 ext-curl。驗證指令是真正的 nextpdf CLI
doctor 處理器——在 nextpdf/core 中宣告為 "bin": ["bin/nextpdf"],因此在一個消費端應用程式中安裝於 vendor/bin/nextpdf——它就同一組擴充進行回報。原生引擎透過
NextPDF\Typography\FontRegistry(目錄建構子引數/addFontDirectory())並經由
NextPDF\Core\DocumentFactory 接入來註冊字型;NEXTPDF_FONTS_PATH 是 nextpdf/laravel
套件的 fonts_path 設定鍵(env('NEXTPDF_FONTS_PATH', resource_path('fonts'))),而非 nextpdf/core 所讀取的變數。登錄表行為記載於「另請參閱」下方連結的字型頁面。
另請參閱
標題為「另請參閱」的區段- 在正式環境為原生引擎佈建字型:此映像所仰賴的字型檔命名、登錄表 API 與暖機鎖定模式。
- 將大型產生的 PDF 串流為 HTTP 回應:從框架控制器服務一份已建文件的記憶體模型。
- 使用 Cloudflare 在邊緣算繪:當一個處理程序內的容器不是正確的執行環境時。
- ionCube Loader 設定:ionCube 編碼 premium 建置的另一個映像顧慮。