ScreenshotNeo

BlogHow-to

How to Deploy Puppeteer on Azure Function Apps

Deploy Puppeteer reliably on Azure Functions: choose the right plan, package Chrome, avoid read-only filesystem traps, and fix missing-browser errors.

By the ScreenshotNeo team29 September 20269 min read

How to Deploy Puppeteer on Azure Function Apps

Direct answer: Deploying Puppeteer to Azure Functions works when the deployed artifact contains a browser compatible with your Puppeteer release, the function can execute that binary on the selected environment, and temporary files are written outside a read-only package mount. Choose the Functions plan and operating system first, then choose between an install-time Puppeteer download, a managed Chrome or Chromium binary, or a Linux container. The deployment setting WEBSITE_RUN_FROM_PACKAGE is not universal: Linux Consumption uses an external package URL, while other plan and operating-system combinations have different settings documented by Microsoft.

This guide covers the hosting choices, a Node.js function, browser packaging, deployment checks, troubleshooting, performance, reliability, and an API alternative when maintaining Chromium is unnecessary.

1. Choose the Azure Functions hosting model first

Your plan controls package storage, deployment configuration, scaling, and how much control you have over system libraries. Confirm the current support matrix for your exact Functions runtime, Node.js version, operating system, and region before deploying.

Option What it means for Puppeteer Key constraint
Flex Consumption Package deployment is the supported code deployment technology and is the default path. The plan setup includes a deployment storage container. Follow the Flex-specific package workflow instead of copying settings from another plan.
Consumption Package settings differ by operating system. Linux Consumption uses an external package URL for local package execution; Microsoft recommends a private Blob container accessed with managed identity. There is 500 MB temporary storage per plan for unpacking packages.
Elastic Premium or Dedicated Package deployment is available. Microsoft documents WEBSITE_RUN_FROM_PACKAGE=1 for Linux and Windows package deployments. Artifact size and startup behavior still depend on the package and instance.
Linux container Build Chrome or Chromium and its system dependencies into an image you control. You own image updates, security patches, and registry operations.

Azure’s package guidance sets a maximum deployment package size of 1 GB. Puppeteer’s installation guide gives an approximate Linux Chrome for Testing download of 282 MB; that figure varies by release and is not the final compressed package size. Keep these limits separate when estimating capacity. See Microsoft’s package deployment guidance and Puppeteer’s installation guide.

2. Decide how the browser gets into the deployment

Option A: Let Puppeteer download its compatible browser during install

A normal Puppeteer installation downloads a compatible Chrome for Testing build and a headless-shell binary. This is the least manual option, but your CI or deployment build must run Puppeteer’s install script. If install scripts are disabled, the package can contain JavaScript but no browser.

The deployment artifact must contain a compatible browser and a writable temporary path.
The deployment artifact must contain a compatible browser and a writable temporary path.
npm install puppeteer
node -e "const p=require('puppeteer'); console.log(p.executablePath())"

Inspect the final ZIP or package, not only your local node_modules. Confirm that the path printed by puppeteer.executablePath() exists in the artifact and that its permissions allow execution on the target operating system.

Option B: Manage a separate Chrome or Chromium binary

Some teams download a browser in a dedicated build step or use a base image that already contains one. Configure Puppeteer with an explicit executable path and keep the browser compatible with the Puppeteer release and target Linux libraries.

const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({
  executablePath: process.env.CHROME_EXECUTABLE_PATH,
  headless: true
});

Set CHROME_EXECUTABLE_PATH to a path that exists after deployment. Do not assume a system Chrome exists in a managed Functions sandbox.

Option C: Build a Linux container

A container is useful when you need deterministic system libraries, fonts, or a browser build that is awkward to package. Start from an Azure-supported Functions base image, install Puppeteer and its browser during the image build, and deploy through the plan and registry combination Azure documents. This shifts work from each invocation to image maintenance; it does not remove the need to validate executable compatibility.

3. Create a minimal Node.js Function

The following HTTP-triggered function opens a URL and returns a PNG. It uses an environment variable for the target URL and an explicit temporary directory. Adapt the handler shape to your Functions programming model.

const puppeteer = require('puppeteer');
module.exports = async function (context, req) {
  const target = req.query.url || process.env.TARGET_URL;
  if (!target) {
    context.res = { status: 400, body: 'Pass ?url=https://example.com' };
    return;
  }
  let browser;
  try {
    const launchOptions = {
      headless: true,
      userDataDir: process.env.PUPPETEER_USER_DATA_DIR || '/tmp/puppeteer-profile'
    };
    if (process.env.CHROME_EXECUTABLE_PATH) {
      launchOptions.executablePath = process.env.CHROME_EXECUTABLE_PATH;
    }
    browser = await puppeteer.launch(launchOptions);
    const page = await browser.newPage();
    await page.goto(target, { waitUntil: 'networkidle2', timeout: 45000 });
    const png = await page.screenshot({ fullPage: true });
    context.res = { status: 200, headers: { 'Content-Type': 'image/png' }, isRaw: true, body: png };
  } catch (error) {
    context.log.error(error);
    context.res = { status: 502, body: 'Browser capture failed' };
  } finally {
    if (browser) await browser.close();
  }
};

Keep the browser lifecycle inside the invocation unless you have measured a safe reuse strategy. Reusing a browser can reduce launch overhead, but stale pages, memory growth, and concurrent requests need explicit limits.

4. Package and deploy without assuming a writable wwwroot

  1. Select plan, OS, and runtime. Record the exact combination and check Azure’s current matrix.
  2. Build in CI. Run npm ci without disabling install scripts, run the Puppeteer executable-path check, and assemble the deployment artifact.
  3. Audit size. Measure the compressed package and unpacked browser. Stay under the 1 GB package maximum and account for the 500 MB temporary-storage limit on Consumption.
  4. Configure paths. Package-mounted wwwroot is read-only when running from a package. Put profiles, downloads, screenshots, and caches under an accessible temporary location such as /tmp on Linux, or the platform’s documented temporary directory on Windows.
  5. Apply the plan-specific deployment setting. Linux Consumption needs a package URL, preferably a private Blob container with managed identity. Premium and Dedicated package deployments commonly use WEBSITE_RUN_FROM_PACKAGE=1; Flex has its own package workflow.
  6. Deploy and inspect logs. Log the resolved executable path, package version, and a redacted launch error. Never log cookies, authorization headers, or page contents.
  7. Invoke a representative URL. Test redirects, delayed assets, and a failure case in the actual plan.

Do not download or modify the browser under the mounted application directory at runtime. A package mount is immutable; attempting to write there produces an error according to Microsoft’s documentation.

Keep immutable application files separate from mutable browser profiles and image output.
Keep immutable application files separate from mutable browser profiles and image output.

5. Browser launch and capture configuration

  • Executable: use Puppeteer’s managed path or set executablePath to your packaged binary.
  • Navigation: choose waitUntil deliberately. networkidle2 can wait indefinitely on analytics or streaming pages; combine it with a timeout and, where appropriate, wait for a selector.
  • Viewport and device: set a fixed viewport and device scale factor for repeatable images.
  • Fonts and locales: install required fonts in the image or package and set locale or timezone if visual output depends on them.
  • Authentication: inject credentials through environment-backed headers or cookies, never source control.
  • Concurrency: cap simultaneous pages per instance. Each Chromium process consumes memory; scale-out is safer than unbounded in-process parallelism.
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.setExtraHTTPHeaders({ 'Accept-Language': 'en-US' });
await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForSelector('#app', { timeout: 15000 });
await page.screenshot({ path: '/tmp/result.png', fullPage: true });

6. Troubleshooting checklist

Symptom Likely cause Fix
Could not find Chrome Puppeteer’s install script was skipped or the browser is outside the artifact. Enable the install step, inspect the deployed package, or set executablePath to a managed binary.
Executable doesn't exist The configured path is local-only, moved during packaging, or uses the wrong OS path. Log the resolved path, use an environment variable, and verify the file inside the deployed artifact.
Permission denied on launch Binary mode bits or filesystem location are unsuitable. Preserve executable permissions during packaging and place mutable runtime data in temporary storage.
Package deployment fails Wrong WEBSITE_RUN_FROM_PACKAGE value, package too large, or inaccessible URL. Recheck plan-specific Azure guidance, package size, Blob permissions, and managed identity.
Browser starts then crashes Missing system libraries, fonts, or insufficient memory. Use a compatible browser build, add dependencies in a controlled image, reduce concurrency, and inspect logs.
Timeouts on busy pages Never-ending network activity or an overly strict wait condition. Set navigation and selector timeouts, prefer domcontentloaded when suitable, and validate any resource blocking.
Writes fail with a read-only error Code writes into package-mounted wwwroot. Write screenshots, profiles, and caches to the documented temporary directory or external storage.

7. Performance, reliability, and cost notes

Browser startup is often the expensive part of a cold invocation, but the research does not establish a universal startup time or fastest plan. Measure your URL mix in the selected plan. Track launch failures, navigation time, screenshot time, memory, and package-unpack behavior. A warm browser may improve latency while increasing leak and isolation risk; close pages, recycle the browser after a bounded number of jobs, and use queue-based throttling for bursts.

Package size affects deployment and unpacking. The approximate 282 MB Linux Chrome download plus dependencies must fit the package and temporary-storage constraints. Containers provide more control over libraries but add image-build and patching work. Consumption can scale to zero and has the documented 500 MB temporary limit; Premium or Dedicated may be preferable when predictable warm capacity matters, but pricing and performance depend on your Azure configuration.

Make captures idempotent: include the URL and rendering options in a job key, retry transient navigation failures with a limit, and store output outside the function filesystem when it must survive an instance recycle. Test redirects, consent dialogs, bot checks, large documents, and lazy-loaded content.

8. Or skip the browser setup

If your goal is a dependable website screenshot endpoint rather than maintaining Chromium in Functions, ScreenshotNeo accepts one GET request and returns PNG, JPEG, WebP, or PDF. See the API documentation for all options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. It also provides an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

Advanced options include full-page lazy-image loading, CSS selectors, dark mode, device presets or custom viewports, retina scale, PDF paper and page options, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Parameter names used by other screenshot APIs also work.

The Free plan includes 1,000 shots each month with no card. Paid plans start at $5 for 3,000 shots; every feature is on every plan. Create a free ScreenshotNeo account.

9. Frequently asked questions

Can I install Chrome when the function starts?

Do not depend on writing into package-mounted wwwroot. Supply the browser during build or in a container, and use temporary storage only for mutable runtime data.

Is WEBSITE_RUN_FROM_PACKAGE=1 correct everywhere?

No. Linux Consumption uses an external package URL, while Flex, Premium, Dedicated, and Windows combinations have their own documented settings.

Should I always add --no-sandbox?

The available research does not establish that as a blanket Azure fix. Diagnose the actual launch error and follow the security guidance for your environment.

How do I prove the right browser shipped?

Log Puppeteer’s resolved executable path, check that the file exists in the final artifact, and invoke a representative capture after deployment.

When is a container the better choice?

Choose one when you need to control browser versions, native libraries, or fonts and can own image patching and registry operations.