コンテンツにスキップ
getnextpdf.com

NextPDF アプリケーションをコンテナ化する

ネイティブのインプロセス NextPDF コアエンジン――composer require nextpdf/core で、あなたの PHP プロセス内で PDF を生成する――を実行する、小さく再現可能な Docker イメージが欲しいとします。このページはまさにそれを構築します。エンジンが実際に必要とする拡張のみを持つ php:8.4 イメージ、最終レイヤーに開発依存なし、同梱フォント、 非 root のランタイムユーザー、本番向けにチューニングされた opcache、そして何かが欠けていればビルドを失敗させる検証ステップです。

このページは ネイティブエンジン専用 です。Chrome ブリッジ(nextpdf/artisan 経由の writeHtmlChrome)と Connect サーバーは、独自のより重いイメージを持つ別のランタイムです――ブリッジにはヘッドレス Chromium のインストール、Connect には長時間稼働するサービスです。このイメージにブラウザやサーバーを追加しないでください。ネイティブエンジンはどちらも必要としません。

始める前に、これらの要素が揃っていることを確認してください。

  • アプリケーションが、nextpdf/core を依存として持つ、コミット済みの composer.jsoncomposer.lock を持っている。
  • 埋め込むつもりのフォントファイルがあり、それらを埋め込むライセンスを持っている。
  • アプリケーションディレクトリに対して docker build を実行できる。

これは運用のハウツーです。ここに PHP はほとんどなく、作業は Dockerfile といくつかの環境設定です。

イメージは、エンジンの実際のプラットフォーム制約を満たさなければならず、それ以上は不要です。パッケージから直接読み取ると、nextpdf/corephp: >=8.4 <9.0 とこれらの PHP 拡張を必要とします。

拡張エンジンがそれを必要とする理由
ext-mbstringテキストとエンコーディングのためのマルチバイト文字列処理
ext-intlUnicode、ロケール、国際化のサポート
ext-gdラスター画像のデコードと処理
ext-openssl署名とセキュアハッシュのための暗号
ext-zlibPDF オブジェクトのストリーム(Flate)圧縮
ext-curlエンジンの送信呼び出しのための HTTP クライアント

それらを公式の php:8.4 イメージにマッピングします。opensslcurlzlib はすでに公式 PHP イメージにコンパイルされているため、docker-php-ext-install する必要は ありませんmbstringgdintl は同梱されて おらず、インストールしなければなりません。それぞれ、まずシステムの開発ヘッダーが存在している必要があります――mbstring はさらに libonig-dev(Oniguruma)ビルド依存を必要とします。 パッケージが列挙していないエンジン拡張を追加しないでください――余分な docker-php-ext-install はすべて、不要なビルド時間と攻撃面です。このイメージがインストールする 1 つの非エンジン拡張は opcache です。これはランタイムのパフォーマンス拡張で、公式イメージで有効化されて同梱されてはおらず、下の opcache チューニングはそれが存在することに依存します(「本番向けの opcache」を参照)。

これは 2 ステージのビルドです。最初のステージは開発パッケージを除外して Composer 依存をインストールし、2 番目のステージは出荷される無駄のないランタイムイメージです。

まず Dockerfile の隣に .dockerignore を追加します。その主な役割は、ホスト環境 ――ホストで構築された vendor/、ローカルのシークレットファイル、ビルドキャッシュ ――をビルドコンテキストから完全に締め出すことで、COPY . /var/www/app が意図したものだけを出荷するようにすることです。より小さく、速く、そしてローカルの .env シークレットを漏らしたり数メガバイトのホスト vendor/ をイメージへ運んだりできない安全なビルドです。

vendor/ を除外することは、ディレクトリの COPY が置き換えではなく マージ であるためにも重要です。下の Dockerfile は COPY --from=vendor ... /var/www/app/vendor の前に RUN rm -rf /var/www/app/vendor を実行するため、この イメージではホストの vendor/ がクリーンな依存ツリーの下に生き残ることは決してありません。しかし、その rm -rf のセーフガードをいつか取り除いた場合、コンテキスト内のホスト構築の vendor/ が先に着地し、vendor ステージのコピーはクリーンなツリーが含むパスのみを上書きするため――余分なホストファイル(古い、または開発でインストールされたパッケージ、 孤立したクラス)は、その下に生き残ることになります。vendor/ をコンテキストの外に保つことが、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

実際のローカルシークレットファイル(.env.env.local.env.*.local)を除外し、 一律の .env.* ではありません――そのワイルドカードは、イメージが文書化された構成のベースラインを運ぶよう出荷 したい .env.example のような非シークレットのテンプレートも落としてしまいます。コミット済みの非シークレットの env テンプレートはコンテキストに保ち、実際にローカルシークレットを保持するファイルのみを除外してください。

# 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"]

依存ステージは --no-scripts で実行されるため、不完全なツリーに対してアプリケーションのポストインストールフックが走ることはありません。アプリケーションのビルドステップ (アセットのコンパイル、キャッシュのウォーミング)は、コードがコピーされた後の後続ステージで実行してください。

出荷されるイメージには開発ツールが含まれてはなりません。composer install--no-dev フラグが要となる行です。これは nextpdf/core とあなたのアプリケーションの require-dev の下にあるすべて――テストランナー、静的アナライザー、ミューテーションツール――をスキップします。そのいずれも本番に居場所はありません。--optimize-autoloader と組み合わせて、オートローダーがリクエストごとのファイルシステムスキャンではなく、 生成されたクラスマップになるようにしてください。

Docker が依存レイヤーをキャッシュし、ロックファイルが変わったときにのみ再解決するよう、 残りのソースの 前に composer.jsoncomposer.lock をコピーします。その最初のインストールはロックファイル単独に対して――アプリケーションソースなしで――実行されるため、そこでの --optimize-autoloadervendor ツリーのみのクラスマップを構築します。あなたのアプリケーション自身のクラスはまだ存在しません。だからこそ、ランタイムステージはソースのコピー 後に 一度 composer dump-autoload --optimize --no-dev --no-scripts を実行するのです。それがアプリのクラスを同じ最適化済みクラスマップへ折り込みます。あなたが開発もしているワークツリーで別の composer dump-autoload を実行しないでください(本番用クラスマップを開発ツリーにコミットしてしまいます)。 再構築は、上に示したように、ソースのコピー後にイメージ内で行うべきものです。

ネイティブエンジンは、OS にインストールされたフォントからではなく、読み取れる フォントファイル からフォントを解決します。fonts-* パッケージをインストールしたり fc-cache を実行したりしても、ネイティブパスが参照できるものは何もないため、 このイメージはシステムフォントを一切インストールしません。.ttf / .otf ファイルを resources/fonts/ の下に同梱してください。上の COPY . /var/www/app がすでにそれらをイメージへ運びます。

ファイルをイメージへ入れるのは仕事の半分にすぎません。素のネイティブエンジンは、 フォント検索環境変数を 読み取りません――NEXTPDF_FONTS_PATHnextpdf/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、 ウォームアップ&ロックのパターン、そして読み取り専用ファイルシステムの扱いは、すべて専用のページにあります――ここで複製しないでください。完全なパターンについては 本番環境でネイティブエンジン向けにフォントをプロビジョニングする を読み、同梱したのと同じディレクトリを登録してください。

公式の PHP イメージはデフォルトで root として実行されます。PDF ジェネレーターに root は不要なので、非特権ユーザーを作成してそれに切り替えます。上の Dockerfile は、 固定の高い UID(10001)を持つシステムユーザー appuser を追加し、アプリケーションツリーの所有権を与え、USER appuser で終わるため、コンテナが起動するすべてのプロセスは非特権です。

可能な限り、ランタイムでアプリケーションを読み取り専用に保ってください。エンジンはそのフォントファイルを読み取り、出力とオプションの解析済みフォントキャッシュのみを書き込むため、出力パスとあらゆるキャッシュディレクトリが書き込み可能なマウントである限り、readOnlyRootFilesystem コンテナは機能します。多層防御のために、オーケストレーター内でこれを、ドロップされた Linux ケイパビリティと no-new-privileges フラグと組み合わせてください。

opcache は 長命の PHP ワーカー ――1 つのウォームなプロセスから多くのリクエストを処理する 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 インクルード(docker/opcache.ini、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 は、キャッシュがソースファイルを決して再チェックしないことを意味します――イミュータブルなイメージには正しい設定です。コードが変わる唯一の方法は新しいイメージだからです。memory_consumptionmax_accelerated_files をアプリケーションのクラス数に合わせてチューニングしてください。

示された CMD は単発の CLI ジェネレーターであり、opcache.enable_cli=0 がそれにとって正しいものです。 短命の php bin/generate.php プロセスは、起動し、コンパイルし、 一度レンダリングして終了するため、次のリクエストと共有できないオペコードキャッシュには利点がありません――CLI opcache はオフのままにし、そのメモリコストを一切払わないでください。opcache がその対価を稼ぐのは、プロセスが再利用される場合のみです。FPM/Apache SAPI、または本物の長時間稼働する CLI ワーカー(キューコンシューマーや RoadRunner 形式のサーバー)です。そのような常駐 CLI ワーカーだけが opcache.enable_cli=1 を設定します。 ここの単発ジェネレーターには、0 のままにしてください。

opcache プリロードopcache.preload スクリプトを持つ長命の FPM ワーカー)を使うセットアップを実行する場合は、opcache.preload=/path/to/preload.php を設定し、 プリロードが非特権ユーザーとして実行されるよう opcache.preload_user=appuser を追加します。実際の opcache.preload スクリプトがなければ opcache.preload_user は何もしません。だからこそ上のベースライン構成にはありません――opcache.preload も設定しない限り、追加しないでください。

誤って構築されたイメージが、豆腐や最初のリクエストでの致命的エラーを生成する代わりに大きく失敗するよう、検証ステップを追加します。NextPDF は CLI を同梱しており、その doctor コマンドは稼働中の PHP 環境を検査し、エンジンが気にする拡張――opensslzlibmbstringgdcurlintl ――について正確に報告します。パッケージは "bin": ["bin/nextpdf"] を宣言するため、消費するアプリケーションでは Composer が実行ファイルを vendor/bin/nextpdf にインストールします(bin/nextpdf ではありません。これは nextpdf/core パッケージ自体の中のパスです)。構築したイメージの中で実行してください。

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

健全な結果は、PHP 8.4 とすべての必須拡張が読み込まれていることを確認します。欠けた拡張がパイプラインを止めるよう、同じ呼び出しをビルド(または CI スモークジョブ)へ配線してください。

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

エンドツーエンドのチェックには、フォントページがフォントスモークチェックについて記述するように、あなた自身のエントリーポイントを通じて 1 ページをレンダリングし、 出力をアサートしてください。

  • -cli の代わりに php:8.4-fpm または -apache アプリが実際にサービスする SAPI を使用してください。拡張リストは同一で、ベースタグと CMD/エントリーポイントだけが異なります。キューワーカーや 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 します――ただし、まず拡張がリンクする ランタイム ライブラリ (icu-libslibpngfreetypelibjpeg-turbooniguruma)を apk add --no-cache して、ビルドグループの削除が intl.so / gd.so / mbstring.so のリンクを外さないようにします。これは Debian ブロックが apt-mark で強制するのと同じ、ランタイムライブラリを保つルールです。
  • fonts-* パッケージをインストールしないでください。 それらはネイティブエンジンには見えません。代わりにフォントファイルを同梱してください――上にリンクしたフォントページを参照してください。
  • Premium と ionCube は別のイメージの懸念です。 ionCube でエンコードされた NextPDF Pro / Enterprise ビルドは、イメージに ionCube Loader をインストールし、 コンテナの正確な PHP ビルド(8.4、NTS と ZTS)に一致させる必要があります。それはコアイメージの範囲外です。premium をデプロイする場合は、ionCube Loader のセットアップ の Docker セクションに従ってください。
  • ホストの vendor/ をビルドコンテキストの外に保つ。 .dockerignorevendor/.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 として実行されないことを保証します。オーケストレーター内で、読み取り専用のルートファイルシステムとドロップされたケイパビリティと組み合わせてください。
  • ベースイメージを固定する。 本番では php:8.4 をダイジェストに固定して、再構築が変更されたベースを静かにプルできないようにし、セキュリティパッチを意図的に取り込むために一定の周期で再構築してください。
  • フォントとライセンスを公開レイヤーの外に保つ。 埋め込むライセンスを持つフォントのみを同梱し、公開プッシュされるイメージに premium ライセンスファイルを決して焼き込まないでください――代わりにランタイムでマウントしてください。

このガイドは規範的な標準への適合を主張しません。プラットフォームの事実は nextpdf/core パッケージから直接読み取られます。php: >=8.4 <9.0 制約と、必須拡張の ext-mbstringext-intlext-gdext-opensslext-zlibext-curl です。 検証コマンドは本物の nextpdf CLI の doctor ハンドラーで――nextpdf/core"bin": ["bin/nextpdf"] として宣言され、したがって消費するアプリでは vendor/bin/nextpdf にインストールされます――同じ拡張セットについて報告します。 ネイティブエンジンは、NextPDF\Core\DocumentFactory を介して配線された NextPDF\Typography\FontRegistry(ディレクトリのコンストラクタ引数 / addFontDirectory())を通じてフォントを登録します。NEXTPDF_FONTS_PATHnextpdf/laravel パッケージの fonts_path 構成キー(env('NEXTPDF_FONTS_PATH', resource_path('fonts')))であり、nextpdf/core が読み取る変数ではありません。 レジストリの挙動は、関連項目の下にリンクしたフォントページに文書化されています。