Skip to content

Artisan in production

In production, inject a configured renderer and a PHP Standards Recommendation 3 (PSR-3) logger, reuse the live Chrome process between renders, set explicit heights for multi-element documents, and limit the render path with an upstream timeout.

BrowserPool keeps one Chrome process alive (keepAlive: true) and restarts it every 100 renders to limit memory growth, a known accumulation pattern in long-lived Chrome DevTools Protocol (CDP) clients. For a worker that renders many documents, use one long-lived renderer instead of one renderer per request, so you rarely pay the Chrome startup cost.

render-service.php
<?php
declare(strict_types=1);
use NextPDF\Artisan\ChromeHtmlRenderer;
use NextPDF\Artisan\ChromeRendererConfig;
use NextPDF\Artisan\Exception\ChromeNotAvailableException;
use NextPDF\Artisan\Exception\ChromeRenderException;
use Psr\Log\LoggerInterface;
final class ReportRenderer
{
private ChromeHtmlRenderer $renderer;
public function __construct(LoggerInterface $logger)
{
$config = ChromeRendererConfig::fromArray([
'chrome_binary' => getenv('CHROME_BINARY') ?: null,
'render_timeout' => 45,
'max_html_size' => 2_000_000,
'no_sandbox' => (bool) getenv('CHROME_NO_SANDBOX'),
]);
$this->renderer = new ChromeHtmlRenderer($config, $logger);
}
public function render(string $html, float $widthPt, float $heightPt = 0.0): string
{
try {
return $this->renderer->render($html, $widthPt, $heightPt)->getPdfData();
} catch (ChromeNotAvailableException $e) {
// Deployment fault: Chrome runtime missing. Page the on-call owner.
throw $e;
} catch (ChromeRenderException $e) {
// Render-time fault: timeout, crash, empty output. Retryable once.
throw $e;
}
}
public function shutdown(): void
{
$this->renderer->close();
}
}

Construct the renderer once, then reuse it. Call close() when the worker shuts down to release the Chrome process deterministically instead of waiting for the destructor. The two catch arms separate a deployment fault (missing runtime) from a render-time fault (retryable). Do not use empty catch blocks.

Wire it into a container as a singleton:

$container->singleton(ReportRenderer::class, fn ($c) =>
new ReportRenderer($c->get(Psr\Log\LoggerInterface::class)));

When you omit height, the bridge measures content height in Chrome (max of body/document scroll and offset heights), converts it to points, and adds a ~0.2 inch (~14.4 pt) safety buffer. The buffer covers the gap between Chrome’s viewport layout and its print-layout reflow. Without it, printToPDF can spill content onto a second page that PageImporter (page 0 only) would clip. The bridge enforces a minimum paper height of 0.1 inch. The tests ChromeHtmlRendererTest::renderUsesAutoFitHeightByDefault, ::renderAutoFitBufferIsAddedNotSubtracted, and ::renderAppliesMinimumHeightOf0Point1InchForTinyExplicitHeight assert this behavior.

For fixed-layout documents (invoices, certificates), pass an explicit height in points. When height is explicit, no buffer is added, and the output matches the requested paper size exactly (asserted by ::renderHonorsExplicitHeightWithoutAutoBuffer).

  • Construct one renderer per worker, and reuse it. BrowserPool reuses the live browser and automatically restarts on the 100-render boundary.
  • Call close() at worker shutdown and between large batches when you want a fresh Chrome process sooner than the 100-render boundary.
  • The destructor calls close(), but an explicit close() is deterministic and preferred in long-running processes.
  • Restart notices are logged at notice level with the render count; alert on an elevated restart rate because it indicates heavier-than-expected documents.

Inject a PSR-3 logger. The renderer emits these events and levels:

EventLevelContext
Render startdebugsize, width, height
Render completedebugpdfSize, contentHeight
Browser launchinfobinary
Browser restartnoticecount
Browser closedebugrenderCount

No HTML, PDF bytes, or extracted text is logged. This keeps payloads out of operational logs and aligns with National Institute of Standards and Technology Special Publication (NIST SP) 800-92 log-content guidance. Build latency service-level objectives (SLOs) from the start/complete pair, and build a restart-rate alert from the notice events.

  • Sidecar Chrome: run Chrome in the same container as the PHP worker; pin chrome_binary. Provision a sandbox-capable container; see /integrations/artisan/chrome-renderer-setup/.
  • Containerless / CLI: Artisan has no dependency injection container. Use EInvoiceServiceFactory for Premium e-invoice contracts in command-line interface (CLI) runners; see /integrations/artisan/boot-and-discovery/.
  • Resource bounding: pair render_timeout with an upstream request budget and a host cgroup/ulimit. See the threat model on /integrations/artisan/security-and-operations/.
  • A renderer caught mid-render still closes the Chrome page (finally), and the pool stays usable.
  • Reusing one renderer across threads/processes is not supported; one renderer owns one Chrome process.
  • The 100-render restart is fixed; size batches with it in mind so latency spikes stay predictable.

Steady-state cost is Chrome layout of the input plus printToPDF, not bridge overhead. keepAlive spreads startup cost across renders. Expect a latency spike on every 100th render (process restart); surface it in SLOs rather than treating it as an incident.

Production paths receive untrusted HTML. Re-read /integrations/artisan/security-and-operations/. The network barriers hold regardless of configuration, but no_sandbox: true removes Chrome process isolation and raises the trust requirement on the input.

In containerless workers, EInvoiceServiceFactory returns null when Premium is not installed, so the open-source render path continues unchanged. Install Pro/Enterprise to enable e-invoice embedding and validation on the rendered document.

  • /integrations/artisan/quickstart/
  • /integrations/artisan/configuration/
  • /integrations/artisan/security-and-operations/
  • /integrations/artisan/chrome-renderer-setup/
  • /integrations/artisan/troubleshooting/