ScreenshotNeo

BlogHow-to

How to Deploy Puppeteer on Azure VMs with Complete Dependencies

Deploy Puppeteer on Azure VMs with the right Node runtime, Chrome binary, Linux libraries, cloud-init automation, and production troubleshooting.

By the ScreenshotNeo team1 October 202611 min read

Direct answer: deploy Puppeteer on an Azure Linux VM by choosing a supported distribution and CPU architecture, installing the Node version required by your selected Puppeteer release, installing Puppeteer and its matching Chrome for Testing browser, adding the operating-system libraries and fonts Chrome needs, and verifying the browser as the same non-root account that will run your service. Use SSH for a one-off VM or Azure cloud-init for repeatable first-boot provisioning.

The exact dependency set changes with the Linux distribution and browser build. Puppeteer 25.12.0 documentation currently lists Node 22.12 or newer and supports Chrome for Testing on named Debian/Ubuntu and openSUSE/Fedora Linux families for x64 and arm64. Recheck the current system requirements before pinning an image or runtime.

1. Choose the Azure VM image and Puppeteer package

Start with a supported Linux family and architecture. Ubuntu or Debian images are usually the simplest path because Puppeteer documents a Debian/Ubuntu dependency installer. Do not assume Alpine is a drop-in Chrome for Testing target; validate the chosen image against the current Puppeteer requirements.

Decision Use when Operational consequence
puppeteer You want Puppeteer to download a compatible Chrome for Testing build. The install owns browser acquisition and the browser cache must persist or be rebuilt during deployment.
puppeteer-core You need to manage the browser binary yourself. You must install a browser separately and pass its executable path or otherwise configure browser selection.
SSH provisioning An existing VM or an interactive setup. Fast to inspect and debug, but easy to make inconsistent across machines.
cloud-init Repeatable VM creation. Packages and files are installed on first boot from a declarative configuration.

Puppeteer defines itself as a JavaScript library that controls Chrome or Firefox through the DevTools Protocol or WebDriver BiDi. Its installation guide explains the different browser behavior of puppeteer and puppeteer-core: official installation guide.

2. Create a Linux VM and connect over SSH

For a public-IP setup, Azure’s CLI quickstart creates a Linux VM with an SSH key and network security group. A private-only VM needs an access path such as Azure Bastion or another approved network route; do not assume that a public SSH endpoint exists.

az group create \\
  --name puppeteer-rg \\
  --location eastus

az vm create \\
  --resource-group puppeteer-rg \\
  --name puppeteer-vm \\
  --image Ubuntu2204 \\
  --size Standard_D2s_v5 \\
  --admin-username azureuser \\
  --generate-ssh-keys

az vm show \\
  --resource-group puppeteer-rg \\
  --name puppeteer-vm \\
  --show-details \\
  --query publicIps \\
  --output tsv

ssh azureuser@VM_PUBLIC_IP

VM sizes and image aliases change over time. Treat the command as a starting point and select an image, architecture, and size that your subscription and region support. Azure’s general VM creation and SSH guidance is documented in the Linux VM quickstart and SSH connection guide.

3. Install Node.js and system packages

Install the Node version required by the Puppeteer release you have pinned. For the currently documented Puppeteer 25.12.0 requirements, that means Node 22.12 or newer. Record the Node, npm, Puppeteer, and browser versions in your deployment metadata so an image rebuild cannot silently change them.

sudo apt-get update
sudo apt-get install -y ca-certificates curl git

# Install Node 22 from NodeSource; verify the major version against your
# selected Puppeteer release before production rollout.
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs

node --version
npm --version

Chrome also needs shared libraries, fonts, and related system components. On Debian/Ubuntu, the Puppeteer browser CLI can attempt the browser installation and dependency setup:

mkdir -p /srv/puppeteer-app
cd /srv/puppeteer-app
npm init -y
npm install puppeteer

# Run this as a privileged provisioning step on Debian/Ubuntu.
sudo npx puppeteer browsers install chrome --install-deps

The --install-deps option is intended for Chrome on Debian/Ubuntu and requires system privileges. It is not a universal package manifest for every Azure image. For another distribution, use its native package manager and the dependency guidance for the matching browser build. Puppeteer’s troubleshooting documentation links to Chromium’s live Debian and RPM manifests because package names change: Linux troubleshooting and dependencies.

4. Install the application and launch Chrome

A minimal, production-friendly example keeps the browser lifecycle inside a try/finally block, sets an explicit viewport, and closes the browser even when navigation fails.

// screenshot.js
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    headless: true,
    // Keep this false unless your environment specifically requires it.
    args: ['--disable-dev-shm-usage']
  });

  try {
    const page = await browser.newPage();
    await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
    await page.goto('https://example.com', {
      waitUntil: 'networkidle2',
      timeout: 60_000
    });
    await page.screenshot({ path: 'example.png', fullPage: true });
  } finally {
    await browser.close();
  }
})();
node screenshot.js

If you use puppeteer-core, install and manage Chrome separately, then point Puppeteer at the actual binary:

npm install puppeteer-core
which google-chrome || which chromium || true
ldd /path/to/chrome | grep not || true
const puppeteer = require('puppeteer-core');

(async () => {
  const browser = await puppeteer.launch({
    headless: true,
    executablePath: process.env.CHROME_BIN,
    args: ['--disable-dev-shm-usage']
  });
  try {
    const page = await browser.newPage();
    await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
    console.log(await page.title());
  } finally {
    await browser.close();
  }
})();

Set CHROME_BIN to the path installed by your image or package manager. Do not copy an executable path from a different VM image without checking it.

5. Automate first boot with Azure cloud-init

Azure cloud-init can install packages and write files during first boot. The following example creates an application directory, installs Node and Puppeteer, and writes a smoke-test script. Pin versions in a real deployment after checking the current Puppeteer requirements.

#cloud-config
package_update: true
packages:
  - ca-certificates
  - curl
  - git
runcmd:
  - [ bash, -lc, "curl -fsSL https://deb.nodesource.com/setup_22.x | bash -" ]
  - [ apt-get, install, -y, nodejs ]
  - [ mkdir, -p, /srv/puppeteer-app ]
  - [ bash, -lc, "cd /srv/puppeteer-app && npm init -y && npm install puppeteer" ]
  - [ bash, -lc, "cd /srv/puppeteer-app && npx puppeteer browsers install chrome --install-deps" ]
  - [ chown, -R, azureuser:azureuser, /srv/puppeteer-app ]
write_files:
  - path: /srv/puppeteer-app/smoke.js
    owner: azureuser:azureuser
    permissions: '0755'
    content: |
      const puppeteer = require('puppeteer');
      (async () => {
        const browser = await puppeteer.launch({
          headless: true,
          args: ['--disable-dev-shm-usage']
        });
        try {
          const page = await browser.newPage();
          await page.goto('https://example.com', {
            waitUntil: 'domcontentloaded',
            timeout: 60000
          });
          console.log(await page.title());
        } finally {
          await browser.close();
        }
      })();

Supply the file when creating the VM:

az vm create \\
  --resource-group puppeteer-rg \\
  --name puppeteer-vm \\
  --image Ubuntu2204 \\
  --admin-username azureuser \\
  --generate-ssh-keys \\
  --custom-data cloud-init.yaml

Cloud-init runs early in the boot process. Inspect its logs after connecting:

sudo cloud-init status --long
sudo journalctl -u cloud-init -u cloud-final --no-pager
sudo tail -n 200 /var/log/cloud-init-output.log

Azure documents the general cloud-init mechanism in its Linux VM cloud-init tutorial. If the VM cannot reach public package repositories, make required dependencies available through reachable repositories or include them in the application package, as described in Microsoft’s VM application package guidance.

6. Verify dependencies under the production identity

  1. Run node --version and record the result.
  2. Confirm the installed Puppeteer version with npm list puppeteer.
  3. Find the browser executable or ask Puppeteer for its managed cache location.
  4. Check unresolved shared libraries with ldd /path/to/chrome | grep not.
  5. Run the smoke test as the same non-root user, with the same HOME, cache directory, filesystem permissions, and service limits used in production.
  6. Navigate to a controlled page and save a screenshot or title as a deployment health check.

A successful npm install does not prove that Chrome can launch. Missing libraries, fonts, sandbox permissions, a read-only cache, or an install script disabled by your package manager can all appear only at runtime. If install scripts are blocked, Puppeteer’s installation documentation says to run npx puppeteer browsers install manually or allow the Puppeteer install script.

7. Runtime options that matter on Azure

Headless mode and sandboxing

Use headless: true for a server VM. Avoid adding --no-sandbox by default; instead, run Chrome with the permissions and user configuration expected by your distribution. If your container or hardened service account prevents sandbox startup, treat that as an environment decision and document the security trade-off before changing flags.

Shared memory

Chrome can fail on constrained /dev/shm mounts. --disable-dev-shm-usage makes Chrome use another temporary location and is a pragmatic fallback, although it can increase disk I/O. Monitor the VM’s temporary storage and clean generated artifacts.

networkidle2 is useful for pages that finish loading, but analytics, long polling, and WebSockets can prevent a quiet network. Use domcontentloaded plus an explicit selector wait when the page has a known readiness element. Always set a finite timeout.

Browser and cache persistence

With puppeteer, the downloaded browser normally lives in Puppeteer’s cache under the installing user’s home directory. Installing as root and running as another user can produce a missing-browser or permission error. Either install and run under a consistent deployment identity, configure a shared writable cache deliberately, or package the browser during image creation.

Architecture

Match x64 or arm64 across the Azure VM image, Node runtime, Puppeteer browser build, and any native dependencies. Do not restore a cache produced on one architecture onto another.

8. Troubleshooting common failures

Symptom Likely cause Fix
Could not find Chrome or browser revision errors puppeteer-core was used without a browser, install scripts were skipped, or the cache belongs to another user. Install the matching browser explicitly, set executablePath for puppeteer-core, or run npx puppeteer browsers install as the deployment identity.
error while loading shared libraries A required Debian/RPM library is absent. Run ldd /path/to/chrome | grep not, then install the missing package using the distribution’s native package manager.
Chrome exits immediately as root The browser sandbox and account configuration are incompatible. Run the service as a dedicated non-root user and verify permissions. Only change sandbox flags after reviewing the security impact.
Cloud-init finished but Node is missing The script failed early, repository access was unavailable, or commands were not run through a shell where needed. Inspect cloud-init-output.log and system journal; make repository access explicit and use bash -lc for pipelines.
Navigation times out The target is slow, blocked, waiting on a never-ending request, or unreachable from the VM network. Check DNS and outbound rules, use a finite but appropriate timeout, choose a more suitable wait condition, and log the URL and failure type.
Blank or incomplete screenshots Lazy content was not triggered, the page was captured before rendering, or the target rejected the VM. Wait for a selector or application-ready signal, scroll when needed, capture after the relevant network or DOM event, and inspect response status and console errors.
Permission denied in the browser cache Chrome was installed as one user and executed as another. Move or reinstall the cache for the runtime user and check ownership of HOME, temporary directories, and the application folder.
Fonts or non-Latin text render incorrectly The minimal image lacks required fonts. Install the font packages appropriate to the target languages and validate the actual rendered output on the VM.

9. Performance, reliability, and cost considerations

  • Startup cost: launching a browser for every request is slower than reusing a controlled browser process. If you reuse pages or browsers, cap concurrency and recycle unhealthy instances.
  • CPU and memory: rendering several pages concurrently increases both. Measure your own workload before selecting an Azure VM size; the cited sources do not provide a universal throughput or VM-size benchmark.
  • Timeouts: use separate limits for browser launch, navigation, selector waits, and the whole job. Return a structured failure so callers can retry safely.
  • Retries: retry transient DNS, connection, or navigation failures with a bounded backoff. Do not blindly retry authentication failures, bot checks, or deterministic JavaScript errors.
  • Idempotency: if a job writes files or triggers actions, use a stable job identifier and avoid repeating side effects on retry.
  • Observability: log Puppeteer and browser versions, VM architecture, URL host, elapsed times, exit reason, and whether the failure occurred before or after navigation. Scrub cookies, authorization headers, and page content from logs.
  • Updates: pin application dependencies and rebuild deliberately. A newer Puppeteer release can change its required Node version or browser build.
  • Azure cost: VM charges depend on the selected size, region, disk, uptime, and other Azure resources. The supplied sources do not support a numeric cost comparison, so estimate using your subscription’s current Azure pricing before deployment.

10. Or skip the browser setup

If your goal is reliable website screenshots rather than operating Chrome on a VM, ScreenshotNeo provides a GET endpoint that returns PNG, JPEG, WebP, or PDF output. Cookie and consent banners are accepted before capture, and more than 60 known consent platforms, newsletter popups, and chat widgets are removed. Bot checks, 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.

See the ScreenshotNeo API documentation for all options, including full-page and element capture, device presets, retina scale, dark mode, custom CSS and JavaScript, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous jobs, bulk capture, usage reporting, and PDF output.

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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', bytes);

ScreenshotNeo also has an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.

11. Deployment checklist

  • Supported Linux family and CPU architecture selected.
  • Node version meets the selected Puppeteer release requirement.
  • Browser ownership model chosen: managed puppeteer browser or explicit puppeteer-core binary.
  • System libraries and fonts installed for the exact image and browser build.
  • Install scripts and browser cache work for the runtime user.
  • ldd reports no unresolved Chrome libraries.
  • Smoke test passes with the production service account.
  • Cloud-init logs and deployment version metadata are retained.
  • Navigation, browser, and job-level timeouts are finite.
  • Retries, concurrency limits, logs, and cleanup are defined.

12. FAQ

Can I install Puppeteer on any Azure Linux image?

No. Match the image distribution and architecture to the current Puppeteer and Chrome for Testing support documentation, then verify the selected image in your environment.

Does npm install puppeteer install Linux dependencies?

It downloads Puppeteer’s compatible browser, but a successful npm install does not guarantee that the VM has every shared library or font Chrome needs. Use the Debian/Ubuntu CLI dependency option or the native package manager for your distribution.

Should production use puppeteer or puppeteer-core?

Use puppeteer when you want Puppeteer to manage a compatible browser download. Use puppeteer-core when your deployment process owns browser installation, versioning, and the executable path.

Is cloud-init required?

No. SSH is sufficient for an existing VM. Cloud-init is useful when you need repeatable first-boot provisioning across VM instances.

Why does the same script work over SSH but fail as a service?

The service may use a different user, home directory, environment, cache path, filesystem permissions, or resource limits. Run the smoke test under the service identity and compare those values.