콘텐츠로 이동
getnextpdf.com

NextPDF 애플리케이션 컨테이너화하기

네이티브 인프로세스 NextPDF 코어 엔진을 실행하는 작고 재현 가능한 Docker 이미지를 원합니다 — composer require nextpdf/core, PHP 프로세스 안에서 PDF를 생성합니다. 이 페이지는 정확히 그것을 빌드합니다. 엔진이 실제로 필요로 하는 확장만 가진 php:8.4 이미지, 최종 레이어에 개발 의존성 없음, 번들된 글꼴, 비루트 런타임 사용자, 프로덕션용으로 튜닝된 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-intl유니코드, 로케일, 국제화 지원
ext-gd래스터 이미지 디코딩 및 처리
ext-openssl서명 및 보안 해싱을 위한 암호화
ext-zlibPDF 객체의 스트림(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” 참고).

이것은 2단계 빌드입니다. 첫 번째 단계는 개발 패키지를 제외하고 Composer 의존성을 설치하고, 두 번째 단계는 배포되는 린(lean) 런타임 이미지입니다.

먼저 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 단계 복사는 깨끗한 트리가 담은 경로만 덮어쓰게 됩니다 — 추가 호스트 파일 (낡았거나 dev로 설치된 패키지, 고아 클래스)은 그 아래에서 살아남게 됩니다. 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, .env.local, .env.*.local)을 제외하십시오 — 그 와일드카드는 배포하고 싶은 .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 설치(개발 의존성 없음)

섹션 제목: “다단계 Composer 설치(개발 의존성 없음)”

배포되는 이미지는 개발 도구를 포함해서는 안 됩니다. 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를 실행하지 마십시오 (프로덕션 클래스 맵을 dev 트리로 커밋하게 됨). 재빌드는 위에 표시된 대로 소스 복사 후 이미지 안에 속합니다.

네이티브 엔진은 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 워커에서 보상을 줍니다 — 하나의 워밍업된 프로세스에서 많은 요청을 처리하는 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는 실행 중인 PHP 환경을 검사하고 엔진이 신경 쓰는 정확한 확장 — openssl, zlib, mbstring, gd, curl, intl — 을 보고하는 doctor 명령을 가진 CLI를 제공합니다. 패키지는 "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

엔드 투 엔드 검사를 위해서는, 글꼴 페이지가 글꼴 스모크 검사에 대해 설명하는 대로, 여러분 자신의 엔트리포인트를 통해 한 페이지를 렌더링하고 출력에 대해 단언하십시오.

  • -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를 하되, 먼저 빌드 그룹을 삭제해도 intl.so / gd.so / mbstring.so가 언링크되지 않도록 확장이 링크하는 런타임 라이브러리(icu-libs, libpng, freetype, libjpeg-turbo, oniguruma)를 apk add --no-cache하십시오. 이는 Debian 블록이 apt-mark로 강제하는 것과 동일한 런타임 라이브러리 유지 규칙입니다.
  • fonts-* 패키지를 설치하지 마십시오. 그것들은 네이티브 엔진에게 보이지 않습니다. 대신 글꼴 파일을 번들하십시오 — 위에 연결된 글꼴 페이지를 보십시오.
  • 프리미엄과 ionCube는 다른 이미지 고려 사항입니다. ionCube로 인코딩된 NextPDF Pro / Enterprise 빌드는 이미지에 ionCube Loader가 설치되어 컨테이너의 정확한 PHP 빌드(8.4, NTS 대 ZTS)에 맞춰져야 합니다. 그것은 코어 이미지의 범위 밖입니다. 프리미엄을 배포한다면 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로 실행되지 않도록 보장합니다. 이를 오케스트레이터의 읽기 전용 루트 파일 시스템 및 드롭된 기능과 짝지으십시오.
  • 베이스 이미지를 고정하십시오. 프로덕션에서 php:8.4를 다이제스트에 고정하여 재빌드가 바뀐 베이스를 조용히 끌어오지 못하게 하고, 보안 패치를 의도적으로 가져오기 위해 일정에 따라 재빌드하십시오.
  • 글꼴과 라이선스를 공개 레이어 밖에 두십시오. 임베드할 라이선스가 있는 글꼴만 번들하고, 공개적으로 푸시되는 이미지에 프리미엄 라이선스 파일을 결코 구워 넣지 마십시오 — 대신 런타임에 마운트하십시오.

이 안내는 규범적 표준 주장을 하지 않습니다. 플랫폼 사실은 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\Core\DocumentFactory를 통해 연결된 NextPDF\Typography\FontRegistry(디렉터리 생성자 인수 / addFontDirectory())를 통해 글꼴을 등록합니다. NEXTPDF_FONTS_PATHnextpdf/laravel 패키지의 fonts_path 구성 키(env('NEXTPDF_FONTS_PATH', resource_path('fonts')))이며, nextpdf/core가 읽는 변수가 아닙니다. 레지스트리 동작은 참고 자료 아래 연결된 글꼴 페이지에 문서화되어 있습니다.