Skip to content

Chrome renderer setup for NextPDF Artisan

The bridge launches and drives a local Chrome/Chromium process through chrome-php/chrome. Use this page to set up that runtime so Portable Document Format (PDF) renders succeed, and to make the right container and sandbox decisions.

BrowserPool constructs a chrome-php/chromeBrowserFactory (optionally with an explicit binary path) and launches Chrome with a fixed flag set: headless: true, keepAlive: true, windowSize: [1200, 800], sendSyncDefaultTimeout: renderTimeout * 1000, and the custom flags listed on the /integrations/artisan/configuration/ page. The bridge then drives the launched process over the Chrome DevTools Protocol (CDP). It does not connect to a separate Chrome process over a remote debugging port, so there is no network endpoint to expose or authenticate. Chrome runs as a child process of the PHP worker. The test tests/Unit/Artisan/BrowserPoolTest.php::getBrowserCreatesAndReusesInstanceWithExpectedOptions asserts these launch options exactly.

Install a Chrome or Chromium build that the worker user can execute:

Terminal window
# Debian / Ubuntu
apt-get install -y chromium
# RHEL / Fedora
dnf install -y chromium
# Alpine (containers)
apk add --no-cache chromium nss freetype harfbuzz ttf-freefont

Verify it runs headless as the worker user:

Terminal window
chromium --headless --dump-dom about:blank

Exit code 0 with an empty Document Object Model (DOM) means the binary and its shared libraries are present. A non-zero exit is the same failure the bridge surfaces as a ChromeRenderException. Fix it here first.

Auto-detection (the chrome-php/chrome default) works when a binary is on a standard path. For deterministic production behavior, pin it explicitly:

$config = new ChromeRendererConfig(
chromeBinaryPath: '/usr/bin/chromium',
);

or via array config:

$config = ChromeRendererConfig::fromArray([
'chrome_binary' => '/usr/bin/chromium',
]);

Container provisioning and the sandbox decision

Section titled “Container provisioning and the sandbox decision”

In a container, Chrome’s operating system sandbox often cannot initialize as root / process identifier (PID) 1 without extra kernel capabilities. You have two paths:

  1. Keep the sandbox (preferred). Run the worker as a non-root user, and grant the container the capabilities Chrome’s sandbox needs (commonly SYS_ADMIN, or a seccomp profile that permits user-namespace creation). This keeps Chrome process isolation intact.
  2. Disable the sandbox. Set no_sandbox: true. Chrome launches with --no-sandbox. This removes Chrome’s process-isolation sandbox: a real reduction in containment, not a cosmetic flag. Use it only where the sandbox cannot be enabled, run Chrome as a non-root user inside a constrained container, and treat the deployment as requiring higher trust in the input. The bridge’s network barriers, Content Security Policy (CSP) and the CDP block, stay in force either way, but they do not substitute for process isolation. This aligns with OWASP ASVS least-privilege and isolation guidance for rendering untrusted content.

The full boundary statement, including what the sandbox does and does not protect, is on the /integrations/artisan/security-and-operations/ page. This page does not claim that disabling the sandbox is safe.

FROM php:8.4-cli
RUN apt-get update && apt-get install -y --no-install-recommends \
chromium fonts-liberation \
&& rm -rf /var/lib/apt/lists/*
RUN useradd -m -u 10001 worker
USER worker
ENV CHROME_BINARY=/usr/bin/chromium
# Set CHROME_NO_SANDBOX=1 only if the sandbox cannot be enabled in your runtime.

Run the worker as worker (user ID 10001), not root. The bridge already applies the --disable-dev-shm-usage flag, which avoids the small-/dev/shm crash common in containers without further tuning.

The bridge blocks remote font fetches (--disable-remote-fonts and CSP). Install the fonts you need at the operating system layer, or embed them as data: Uniform Resource Identifier (URI) @font-face sources inside defaultCss or the Hypertext Markup Language (HTML). Chinese, Japanese, and Korean (CJK) output requires a CJK font package (for example fonts-noto-cjk) installed in the image.

Use this standalone probe to exercise the full bridge path without the host application:

chrome-health.php
<?php
declare(strict_types=1);
use NextPDF\Artisan\ChromeHtmlRenderer;
use NextPDF\Artisan\ChromeRendererConfig;
require __DIR__ . '/vendor/autoload.php';
$renderer = new ChromeHtmlRenderer(
ChromeRendererConfig::fromArray([
'chrome_binary' => getenv('CHROME_BINARY') ?: null,
'no_sandbox' => (bool) getenv('CHROME_NO_SANDBOX'),
]),
);
$result = $renderer->render('<p>ok</p>', 200.0, 0.0);
fwrite(STDOUT, strlen($result->getPdfData()) > 0 ? "CHROME_OK\n" : "CHROME_EMPTY\n");
$renderer->close();

CHROME_OK confirms launch, render, and import. A thrown exception is the precise failure. Match it on the /integrations/artisan/troubleshooting/ page. Wire this probe in as a readiness check in orchestrated deployments.

  • Run Chrome as a dedicated non-root user.
  • Apply a host memory limit; the bridge bounds growth with a 100-render restart, but a host ceiling is still required.
  • Pair render_timeout with an upstream request budget on any path reachable by untrusted input.
  • Do not expose a Chrome remote-debugging port. The bridge does not use one, and an open CDP port is an unauthenticated control channel.
SymptomLikely causeWhere to look
ChromeNotAvailableExceptionchrome-php/chrome not installed/integrations/artisan/install/
ChromeRenderException on first renderBinary missing / sandbox cannot initializeThis page; /integrations/artisan/troubleshooting/
Empty PDFNo visible box / Chrome crash/integrations/artisan/troubleshooting/
Blank remote imagesNetwork blocked by design/integrations/artisan/security-and-operations/
Periodic latency spike100-render restart/integrations/artisan/production-usage/
  • /integrations/artisan/install/
  • /integrations/artisan/configuration/
  • /integrations/artisan/security-and-operations/
  • /integrations/artisan/troubleshooting/
  • /integrations/artisan/production-usage/