Bỏ qua để đến nội dung
getnextpdf.com

Container hóa một ứng dụng NextPDF

Bạn muốn một ảnh Docker nhỏ, tái lập được, chạy engine lõi NextPDF native, trong tiến trìnhcomposer require nextpdf/core, tạo PDF bên trong tiến trình PHP của bạn. Trang này build đúng cái đó: một ảnh php:8.4 chỉ với những extension mà engine thực sự cần, không có phụ thuộc phát triển ở lớp cuối, phông chữ đã đóng gói, một người dùng runtime non-root, opcache được tinh chỉnh cho môi trường thực tế, và một bước xác minh làm thất bại bản build nếu thiếu bất cứ thứ gì.

Trang này chỉ dành cho engine native. Cầu nối Chrome (writeHtmlChrome qua nextpdf/artisan) và máy chủ Connect là các runtime riêng biệt với các ảnh nặng hơn của riêng chúng — một bản cài Chromium headless cho cầu nối, một dịch vụ chạy lâu cho Connect. Đừng thêm một trình duyệt hay một máy chủ vào ảnh này; engine native không cần cái nào.

Trước khi bắt đầu, hãy xác nhận các phần này đã sẵn sàng:

  • Ứng dụng của bạn có composer.jsoncomposer.lock đã được commit, với nextpdf/core là một phụ thuộc.
  • Bạn có các tệp phông chữ bạn định nhúng, và bạn được cấp phép để nhúng chúng.
  • Bạn có thể chạy docker build đối với thư mục ứng dụng của bạn.

Đây là một hướng dẫn vận hành. Hầu như không có PHP ở đây; công việc là Dockerfile và một vài thiết lập môi trường.

Ảnh phải thỏa mãn các ràng buộc nền tảng thực của engine, không hơn. Đọc thẳng từ gói, nextpdf/core yêu cầu php: >=8.4 <9.0 và các extension PHP sau:

ExtensionVì sao engine cần nó
ext-mbstringXử lý chuỗi đa byte cho văn bản và mã hóa
ext-intlHỗ trợ Unicode, locale, và quốc tế hóa
ext-gdGiải mã và xử lý ảnh raster
ext-opensslMật mã cho việc ký và băm an toàn
ext-zlibNén luồng (Flate) các đối tượng PDF
ext-curlClient HTTP cho các lệnh gọi đi ra của engine

Hãy ánh xạ những cái đó sang ảnh php:8.4 chính thức. openssl, curl, và zlib đã được biên dịch sẵn vào ảnh PHP chính thức, nên bạn không docker-php-ext-install chúng. mbstring, gd, và intl không đi kèm và phải được cài đặt, và mỗi cái cần các header phát triển hệ thống của nó hiện diện trước — mbstring còn cần phụ thuộc build libonig-dev (Oniguruma). Đừng thêm các extension engine mà gói không liệt kê — mỗi docker-php-ext-install thêm là thời gian build và bề mặt tấn công bạn không cần. Một extension không-engine duy nhất mà ảnh này có cài là opcache: nó là một extension hiệu năng runtime, không được bật sẵn trên ảnh chính thức, và việc tinh chỉnh opcache bên dưới phụ thuộc vào việc nó hiện diện (xem “Opcache cho môi trường thực tế”).

Đây là một bản build hai giai đoạn. Giai đoạn đầu cài các phụ thuộc Composer với các gói phát triển được loại trừ; giai đoạn thứ hai là ảnh runtime tinh gọn được gửi đi.

Trước tiên hãy thêm một .dockerignore cạnh Dockerfile. Việc chính của nó là giữ môi trường host — một vendor/ được build trên host, các tệp bí mật cục bộ, và các bộ đệm build — hoàn toàn ngoài ngữ cảnh build, để COPY . /var/www/app chỉ gửi đi những gì bạn dự định: các bản build nhỏ hơn, nhanh hơn, và an toàn hơn, không thể làm lộ các bí mật .env cục bộ hay mang hàng megabyte vendor/ của host vào ảnh.

Loại trừ vendor/ cũng quan trọng vì một COPY thư mục là một phép gộp, không phải một phép thay thế. Dockerfile bên dưới chạy RUN rm -rf /var/www/app/vendor trước COPY --from=vendor ... /var/www/app/vendor, nên trong ảnh này một vendor/ của host không bao giờ có thể sống sót dưới cây phụ thuộc sạch. Nhưng nếu bạn từng gỡ bỏ bảo vệ rm -rf đó, một vendor/ được build trên host trong ngữ cảnh sẽ đáp xuống trước và bản sao của giai đoạn vendor chỉ ghi đè các đường dẫn mà cây sạch chứa — bất kỳ tệp host thừa nào (một gói cũ hoặc được cài dev, một lớp mồ côi) khi đó sẽ sống sót bên dưới nó. Giữ vendor/ ngoài ngữ cảnh đóng lỗ hổng đó bất kể 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

Hãy loại trừ các tệp bí mật cục bộ thực (.env, .env.local, .env.*.local), không phải một .env.* bao trùm — wildcard đó cũng bỏ các template không-bí-mật như .env.example mà bạn muốn gửi đi để ảnh mang theo một mốc cấu hình được tài liệu hóa. Hãy giữ bất kỳ template env không-bí-mật, đã commit nào trong ngữ cảnh; chỉ loại trừ các tệp thực sự giữ các bí mật cục bộ.

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

Giai đoạn phụ thuộc chạy với --no-scripts để không có hook post-install nào của ứng dụng chạy đối với một cây chưa hoàn chỉnh; hãy chạy bất kỳ bước build ứng dụng nào (biên dịch asset, làm nóng cache) trong một giai đoạn sau khi mã đã được sao chép.

Cài Composer đa giai đoạn (không có phụ thuộc dev)

Phần tiêu đề “Cài Composer đa giai đoạn (không có phụ thuộc dev)”

Ảnh được gửi đi không được chứa công cụ phát triển. Cờ --no-dev trên composer install là dòng chịu tải: nó bỏ qua mọi thứ dưới require-dev trong nextpdf/core và ứng dụng của bạn — bộ chạy test, bộ phân tích tĩnh, và các công cụ đột biến — không cái nào có chỗ trong môi trường thực tế. Hãy ghép nó với --optimize-autoloader để bộ tải tự động là một class map được tạo ra thay vì một lần quét hệ thống tệp trên mỗi yêu cầu.

Hãy sao chép composer.jsoncomposer.lock trước phần còn lại của mã nguồn để Docker cache lớp phụ thuộc và chỉ giải quyết lại khi tệp lock thay đổi. Vì lần cài đầu tiên đó chạy đối với chỉ tệp lock — không có mã nguồn ứng dụng — --optimize-autoloader ở đó dựng class map cho chỉ cây vendor; các lớp của chính ứng dụng bạn chưa hiện diện. Đó là lý do giai đoạn runtime chạy composer dump-autoload --optimize --no-dev --no-scripts một lần sau khi sao chép mã nguồn: nó gấp các lớp của app vào cùng class map đã tối ưu. Đừng chạy một composer dump-autoload riêng trong một worktree mà bạn cũng phát triển trong đó (nó sẽ commit một class map môi trường thực tế vào một cây dev); việc dựng lại thuộc về ảnh, sau khi sao chép mã nguồn, như được trình bày ở trên.

Engine native giải quyết phông chữ từ các tệp phông chữ nó có thể đọc, không phải từ các phông chữ do hệ điều hành cài. Cài các gói fonts-* hoặc chạy fc-cache không làm gì mà đường native có thể thấy, nên ảnh này không cài phông chữ hệ thống nào. Hãy đóng gói các tệp .ttf / .otf của bạn dưới resources/fonts/; lệnh COPY . /var/www/app ở trên đã mang chúng vào ảnh.

Đưa các tệp vào ảnh chỉ là một nửa công việc. Engine native trần đọc không biến môi trường tìm-kiếm-phông-chữ nào — NEXTPDF_FONTS_PATH là giá trị mặc định của khóa config fonts_path của gói nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))) và chỉ được tiêu thụ bởi tích hợp framework đó, không phải bởi nextpdf/core. Một entrypoint php bin/generate.php thuần với chỉ biến đó được đặt không đăng ký phông chữ nào và kết xuất ra cùng thứ tofu mà ảnh này tồn tại để ngăn chặn. Entrypoint phải đăng ký thư mục đã đóng gói trong 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();

Đó là toàn bộ mối quan tâm Docker đối với phông chữ. Các quy tắc đặt tên tệp, API registry, mẫu làm-nóng-và-khóa, và xử lý hệ thống tệp chỉ-đọc đều nằm trên trang chuyên biệt — đừng nhân đôi chúng ở đây. Hãy đọc Cung cấp phông chữ cho engine native trong môi trường thực tế để biết mẫu đầy đủ, và đăng ký cùng thư mục bạn đã đóng gói.

Chạy với tư cách một người dùng non-root

Phần tiêu đề “Chạy với tư cách một người dùng non-root”

Các ảnh PHP chính thức chạy với tư cách root theo mặc định. Một bộ tạo PDF không cần root, nên hãy tạo một người dùng không-đặc-quyền và chuyển sang nó. Dockerfile ở trên thêm một người dùng hệ thống appuser với một UID cao cố định (10001), trao cho nó quyền sở hữu cây ứng dụng, và kết thúc bằng USER appuser để mọi tiến trình mà container khởi động đều không-đặc-quyền.

Hãy giữ ứng dụng chỉ-đọc lúc chạy ở nơi bạn có thể. Engine đọc các tệp phông chữ của nó và chỉ ghi đầu ra của nó cùng một bộ đệm phông chữ đã phân tích tùy chọn, nên một container readOnlyRootFilesystem hoạt động miễn là đường đầu ra và bất kỳ thư mục cache nào là các mount ghi được. Hãy kết hợp điều này với các capability Linux đã bỏ và một cờ no-new-privileges trong bộ điều phối của bạn để phòng thủ theo chiều sâu.

Opcache có lợi cho các worker PHP sống lâu — một pool FPM hoặc một tiến trình Apache mod_php phục vụ nhiều yêu cầu từ một tiến trình ấm. Các tiến trình đó biên dịch các lớp của bạn một lần rồi không bao giờ stat các tệp nguồn trên một đường nóng, đúng là thứ mà opcache.validate_timestamps=0 mua cho bạn. Opcache không được bật sẵn trên ảnh php:8.4 chính thức, nên Dockerfile ở trên cài nó bằng docker-php-ext-install opcache (bạn có thể tương đương docker-php-ext-enable opcache nếu extension đã được biên dịch). Tệp conf.d bên dưới là tinh chỉnh, không phải bước bật — nó không làm gì cho đến khi extension được nạp. Hãy gửi nó như một include conf.d (docker/opcache.ini, được sao chép trong 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 nghĩa là cache không bao giờ kiểm tra lại các tệp nguồn — đúng cho một ảnh bất biến, vì cách duy nhất mã thay đổi là một ảnh mới. Hãy tinh chỉnh memory_consumptionmax_accelerated_files theo số lượng lớp của ứng dụng bạn.

CMD được trình bày là một bộ tạo CLI một-lần, và opcache.enable_cli=0 là đúng cho nó. Một tiến trình php bin/generate.php ngắn hạn khởi động, biên dịch, kết xuất một lần, và thoát, nên một opcode cache mà nó không thể chia sẻ với một yêu cầu kế tiếp không cho lợi ích nào — hãy để opcache CLI tắt và không trả chi phí bộ nhớ nào của nó. Opcache chỉ xứng đáng ở nơi tiến trình được tái sử dụng: một SAPI FPM/Apache, hoặc một worker CLI thực sự chạy lâu (một queue consumer hoặc một máy chủ kiểu RoadRunner). Chỉ loại worker CLI thường trú đó mới đặt opcache.enable_cli=1; với bộ tạo một-lần ở đây, hãy giữ nó là 0.

Nếu bạn có chạy một thiết lập dùng preloading của opcache (một worker FPM sống lâu với một script opcache.preload), hãy đặt opcache.preload=/path/to/preload.php và thêm opcache.preload_user=appuser để preload chạy với tư cách người dùng không-đặc-quyền. Không có một script opcache.preload thực sự, opcache.preload_user không làm gì, đó là lý do nó không có trong config mốc ở trên — đừng thêm nó trừ khi bạn cũng đặt opcache.preload.

Hãy thêm một bước xác minh để một ảnh build sai thất bại lớn tiếng thay vì tạo ra tofu hoặc một fatal tại yêu cầu đầu tiên. NextPDF đi kèm một CLI mà lệnh doctor của nó kiểm tra môi trường PHP đang chạy và báo cáo về đúng các extension mà engine quan tâm — openssl, zlib, mbstring, gd, curl, và intl. Gói khai báo "bin": ["bin/nextpdf"], nên trong một ứng dụng tiêu thụ Composer cài tệp thực thi tại vendor/bin/nextpdf (không phải bin/nextpdf, vốn là đường dẫn bên trong chính gói nextpdf/core). Hãy chạy nó bên trong ảnh đã build:

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

Một kết quả khỏe mạnh xác nhận PHP 8.4 và mọi extension bắt buộc đã được nạp. Hãy nối cùng lệnh gọi đó vào bản build (hoặc một CI smoke job) để một extension bị thiếu dừng 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

Để có một kiểm tra đầu-cuối, hãy kết xuất một trang qua entrypoint của riêng bạn và khẳng định trên đầu ra, như trang phông chữ mô tả cho một kiểm tra khói phông chữ.

  • php:8.4-fpm hoặc -apache thay vì -cli. Hãy dùng SAPI mà app của bạn thực sự phục vụ dưới đó. Danh sách extension giống hệt; chỉ tag nền và CMD/entrypoint khác nhau. Với một queue worker hoặc một CLI batch job, -cli là đúng.
  • Alpine (php:8.4-alpine) cần các tên gói khác. Các dòng apt-get ở trên là cho ảnh mặc định dựa trên Debian. Trên Alpine, hãy cài các header *-dev như một nhóm build ảo (apk add --no-cache --virtual .build-deps icu-dev libpng-dev freetype-dev libjpeg-turbo-dev oniguruma-dev) và, sau bước docker-php-ext-install gd intl mbstring opcache, apk del .build-deps — nhưng trước tiên apk add --no-cache các thư viện runtime mà các extension liên kết tới (icu-libs, libpng, freetype, libjpeg-turbo, oniguruma) để việc xóa nhóm build không hủy liên kết intl.so / gd.so / mbstring.so. Đây là cùng quy tắc giữ-các-thư-viện-runtime mà khối Debian thực thi bằng apt-mark.
  • Đừng cài các gói fonts-*. Chúng vô hình với engine native. Hãy đóng gói các tệp phông chữ thay vào đó — xem trang phông chữ được liên kết ở trên.
  • Premium và ionCube là một mối quan tâm ảnh khác. Các bản dựng NextPDF Pro / Enterprise mã hóa bằng ionCube cần ionCube Loader được cài trong ảnh và khớp với bản build PHP chính xác của container (8.4, NTS so với ZTS). Đó nằm ngoài phạm vi cho một ảnh core; nếu bạn triển khai premium, hãy theo mục Docker của Thiết lập ionCube Loader.
  • Giữ một vendor/ của host ngoài ngữ cảnh build. .dockerignore (loại trừ vendor/, .git/, và các cache cục bộ) giữ cây host hoàn toàn ngoài ngữ cảnh — đó là thứ làm cho bản build nhỏ, nhanh, và không có bí mật cục bộ bị lộ. Nó cũng bảo vệ trường hợp gộp thư mục: một COPY thư mục là một phép gộp, không phải một phép thay thế, nên một vendor/ được build trên host mà tới được ngữ cảnh sẽ đáp xuống trước và COPY --from=vendor /app/vendor /var/www/app/vendor chỉ ghi đè các đường dẫn mà cây phụ thuộc sạch chứa. Trong Dockerfile này, RUN rm -rf /var/www/app/vendor trước bản sao vendor đã loại bỏ bất kỳ thư mục như vậy nào, nên dư lượng đó không thể xảy ra ở đây; rủi ro gộp chỉ trở lại nếu bạn bỏ bảo vệ rm -rf đó, đó là lý do việc loại trừ .dockerignore là cách khắc phục bền vững.
  • Đừng gửi phụ thuộc dev. --no-dev giữ các công cụ test và phân tích, cùng các gói bắc cầu của chúng, ngoài ảnh runtime và bề mặt tấn công của nó.
  • Chạy không-đặc-quyền. USER appuser cuối cùng đảm bảo không tiến trình container nào chạy với tư cách root. Hãy ghép nó với một hệ thống tệp gốc chỉ-đọc và các capability đã bỏ trong bộ điều phối của bạn.
  • Ghim ảnh nền. Hãy ghim php:8.4 về một digest trong môi trường thực tế để một lần build lại không thể âm thầm kéo về một ảnh nền đã thay đổi, và build lại theo một nhịp để chủ động nhận các bản vá bảo mật.
  • Giữ phông chữ và giấy phép ngoài các lớp công khai. Chỉ đóng gói các phông chữ bạn được cấp phép để nhúng, và đừng bao giờ nướng một tệp giấy phép premium vào một ảnh được đẩy công khai — hãy mount nó lúc chạy thay vào đó.

Hướng dẫn này không đưa ra tuyên bố tiêu chuẩn quy phạm nào. Các sự thật nền tảng được đọc trực tiếp từ gói nextpdf/core: ràng buộc php: >=8.4 <9.0 và các extension bắt buộc ext-mbstring, ext-intl, ext-gd, ext-openssl, ext-zlib, và ext-curl. Lệnh xác minh là handler doctor của CLI nextpdf thực — được khai báo là "bin": ["bin/nextpdf"] trong nextpdf/core và do đó được cài tại vendor/bin/nextpdf trong một app tiêu thụ — vốn báo cáo về cùng tập extension. Engine native đăng ký phông chữ qua NextPDF\Typography\FontRegistry (đối số hàm khởi tạo thư mục / addFontDirectory()) được nối qua NextPDF\Core\DocumentFactory; NEXTPDF_FONTS_PATH là khóa config fonts_path của gói nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))), không phải một biến mà nextpdf/core đọc. Hành vi registry được tài liệu hóa trên trang phông chữ được liên kết dưới Xem thêm.