ScreenshotNeo

BlogHow-to

How to Run Puppeteer on Google Cloud Platform

Deploy Puppeteer with Chromium on Cloud Run. Choose a container or function, configure access, and handle browser failures and costs.

By the ScreenshotNeo team4 October 202612 min read

Direct answer: Run Puppeteer on Google Cloud Platform by packaging your Node.js application, Chromium, and the browser’s required operating-system libraries in a container, then deploying that image as a Cloud Run service. Puppeteer controls Chromium; it does not make the browser binary or its system dependencies appear in the deployed runtime. Google’s browser automation guide specifically says to install Chromium in the Cloud Run container and names Puppeteer as a library for controlling it.

A Cloud Run function can also handle a small HTTP or event-triggered browser task. Source deployment still builds a container image, however, and that runtime needs Chromium too. Choose a container service when you want direct control over the browser and OS package layer; choose a function when the workload naturally fits a single-purpose function and its source-based build flow.

1. Choose a Cloud Run deployment shape

Option Good fit Browser packaging
Cloud Run service from a container image An HTTP application or worker that needs explicit Chromium and OS dependency control You define the image, including Chromium and required libraries.
Cloud Run function from source A small, single-purpose HTTP or event-triggered handler Google’s build flow creates the runtime image. Your chosen runtime and build still need to provide a compatible Chromium installation.

This guide uses a containerized Cloud Run service. Google supports deploying container images to Cloud Run and recommends Artifact Registry for storing them. The function route uses Cloud Build and Artifact Registry as part of its source deployment flow. Pick the path based on how much control you need over the runtime, not on an assumption that functions remove browser setup.

Puppeteer-controlled headless Chromium works for tasks such as page extraction, form submissions, UI checks, PDFs, and screenshots. If a task needs a full desktop, browser extensions, or streamed human-style interaction, that is a different browser automation shape from this headless example.

2. Build a small Puppeteer service

The example below creates an HTTP endpoint that visits a caller-supplied URL and returns the page title and final URL. It uses puppeteer-core and the Chromium executable installed in the image. The URL check is a basic guard, not a complete defense against server-side request forgery (SSRF); for a public service, restrict permitted destinations and network access according to your threat model.

Project files

package.json:

{
  "name": "cloud-run-puppeteer",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": { "start": "node server.js" },
  "dependencies": { "puppeteer-core": "^24.0.0" }
}

For reproducible builds, commit the lockfile generated by npm install and use npm ci in the image. Pin your dependency versions to the versions you have chosen and validated; browser compatibility depends on the Chromium build and Puppeteer version.

server.js:

import http from 'node:http';
import puppeteer from 'puppeteer-core';

const port = Number(process.env.PORT || 8080);
const executablePath = process.env.CHROMIUM_PATH || '/usr/bin/chromium';

const server = http.createServer(async (req, res) => {
  if (req.url === '/healthz') {
    res.writeHead(200, { 'content-type': 'text/plain' });
    res.end('ok');
    return;
  }

  if (req.method !== 'GET' || !req.url.startsWith('/title?')) {
    res.writeHead(404, { 'content-type': 'application/json' });
    res.end(JSON.stringify({ error: 'Use GET /title?url=https%3A%2F%2Fexample.com' }));
    return;
  }

  const requestUrl = new URL(req.url, `http://${req.headers.host}`);
  let target;
  try {
    target = new URL(requestUrl.searchParams.get('url') || '');
    if (!['http:', 'https:'].includes(target.protocol)) throw new Error('scheme');
  } catch {
    res.writeHead(400, { 'content-type': 'application/json' });
    res.end(JSON.stringify({ error: 'Provide a valid http or https URL.' }));
    return;
  }

  let browser;
  try {
    browser = await puppeteer.launch({
      executablePath,
      headless: true,
      args: ['--no-sandbox', '--disable-setuid-sandbox']
    });
    const page = await browser.newPage();
    page.setDefaultNavigationTimeout(30000);
    await page.goto(target.href, { waitUntil: 'domcontentloaded' });
    const result = {
      title: await page.title(),
      finalUrl: page.url()
    };
    res.writeHead(200, { 'content-type': 'application/json' });
    res.end(JSON.stringify(result));
  } catch (error) {
    console.error('Puppeteer request failed:', error);
    res.writeHead(502, { 'content-type': 'application/json' });
    res.end(JSON.stringify({ error: 'Browser navigation failed.' }));
  } finally {
    if (browser) await browser.close().catch((error) => console.error('Browser close failed:', error));
  }
});

server.listen(port, '0.0.0.0', () => {
  console.log(`Listening on ${port}`);
});

The example launches and closes a browser per request so resource ownership is clear. For heavier traffic, reuse a browser process carefully and create a separate page per request; close each page in a finally block, cap concurrent work, and recycle the browser if it becomes unhealthy. Do not return raw exception details to callers.

Container image

Dockerfile:

FROM node:22-bookworm-slim

WORKDIR /app

# Chromium and its shared-library dependencies must be present in the image.
# Package names and browser paths can vary by base image and repository state.
RUN apt-get update && apt-get install -y --no-install-recommends chromium \
    && rm -rf /var/lib/apt/lists/*

COPY package*.json ./
RUN npm ci --omit=dev

COPY server.js ./
ENV NODE_ENV=production
ENV CHROMIUM_PATH=/usr/bin/chromium
ENV PORT=8080
EXPOSE 8080
CMD ["npm", "start"]

This is a concrete Debian-based starting point, not a universal package recipe. Check that the selected base image’s package repositories provide Chromium at the path configured above and that the installed browser’s shared libraries match the image. If they do not, use a compatible base image, adjust the installed package and executable path, and rebuild. Keep the browser and Puppeteer versions compatible. Avoid relying on a browser download that only exists on your development machine.

3. Build and deploy to Cloud Run

  1. Create or select a Google Cloud project, select a deployment region, and configure billing and the APIs needed for the route you chose. Required APIs and permissions depend on deployment mode and organization policy; use Google’s current deployment guide for your project.
  2. Build the image and push it to Artifact Registry. Create the repository first if needed.
  3. Deploy the image as a Cloud Run service. Decide deliberately whether requests require authentication or whether the service is public.
  4. Call the deployed endpoint with a known test page, then inspect the service logs if startup or navigation fails.

Example commands, with placeholders to replace:

# Set values for your project and deployment.
PROJECT_ID="YOUR_PROJECT_ID"
REGION="YOUR_REGION"
REPOSITORY="browser-services"
IMAGE="puppeteer-title"
SERVICE="puppeteer-title"

# Authenticate gcloud and select the project before running these commands.
gcloud config set project "$PROJECT_ID"
gcloud services enable run.googleapis.com artifactregistry.googleapis.com cloudbuild.googleapis.com

gcloud artifacts repositories create "$REPOSITORY" \
  --repository-format=docker \
  --location="$REGION"

gcloud builds submit \
  --tag "$REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/$IMAGE:latest" .

gcloud run deploy "$SERVICE" \
  --image "$REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/$IMAGE:latest" \
  --region "$REGION" \
  --no-allow-unauthenticated

The deployment command above keeps invocation authenticated. To expose an endpoint publicly, use --allow-unauthenticated only when that is intended and your application has appropriate input validation, destination restrictions, and abuse controls. An unauthenticated URL-fetching browser endpoint can be abused to make requests to destinations you did not intend.

Find the service URL from the successful gcloud run deploy output or the Cloud Run service page. For an authenticated service, an identity with invoke permission needs to send a Google identity token. For a deliberately public service, a simple smoke test is:

curl --get "SERVICE_URL/title" \
  --data-urlencode "url=https://example.com"

Cloud Run revisions are tied to the deployed image. For repeatable releases, tag images with a version or commit identifier and retain the build record rather than depending on a mutable latest tag for identification.

4. Configure the browser workload

Chromium and launch arguments

Set executablePath to the actual Chromium binary in the container. Use headless mode for a normal browser automation service. The sample includes --no-sandbox and --disable-setuid-sandbox, which are commonly needed in containerized browser setups; understand the security implications for your threat model and isolate untrusted browsing accordingly. Do not assume flags, paths, or system packages transfer unchanged between base images.

  • domcontentloaded returns after the initial document is parsed; it can be faster than waiting for every network request to finish.
  • load waits for the page load event and its dependent resources, which can take longer on pages with slow resources.
  • networkidle can be useful for pages that fetch data after navigation, but analytics, polling, or long-lived connections may prevent the page from becoming idle.

Choose the readiness condition for the page and task. For dynamic applications, wait for a meaningful selector with a bounded timeout rather than assuming navigation completion means the content you need is ready. Set navigation and operation timeouts so a slow or stuck page does not occupy a request indefinitely.

Request, browser, and response limits

Browser work consumes more memory and CPU than a simple HTTP handler. Bound page navigation, total request duration, concurrent pages, and the size of returned data. Close pages and browsers when work finishes, including error paths. Treat downloads and generated files as temporary unless you have intentionally configured durable storage; a container’s writable filesystem should not be treated as permanent storage.

Cloud Run region is a service-level decision: select one near your users while considering the location of connected Google Cloud products. Cross-location calls can affect latency and cost. A browser’s destination website location can also influence response time, but the supplied Google Cloud documentation does not establish a universal fastest region or performance figure.

5. Validate and operate the service

  1. Build the container and start it locally; request /healthz and then /title?url=....
  2. Check that the exact Chromium binary configured by CHROMIUM_PATH starts in the image.
  3. Deploy a revision and invoke it using the configured authentication mode.
  4. Use a simple page first, then a representative page with redirects or client-side rendering.
  5. Review Cloud Run logs for startup errors, browser launch failures, navigation timeouts, and unexpected concurrency.

Do not infer production capacity from a successful one-request smoke test. Measure your own pages and traffic pattern before setting concurrency and resource allocations. Browser startup, page complexity, external network behavior, and memory use vary by workload.

6. Troubleshooting

Symptom Likely cause What to check or change
Failed to launch the browser process or executable not found The Chromium package is missing, installed at another path, or incompatible with Puppeteer. Inspect the image build, verify the executable path inside the container, and align the browser and Puppeteer versions.
Shared library error at launch The selected Chromium build needs OS libraries absent from the base image. Install the dependencies for that exact image/browser combination. Package names differ by distribution; do not copy an unrelated package list blindly.
Works locally, fails after deployment The local machine has browser files or libraries that were not included in the container. Run the built image locally and make the image self-contained. Confirm the deployed revision uses the intended image.
Navigation timeout The target is slow, waits on ongoing network activity, or blocks automated browsing. Choose an appropriate readiness event, wait for a task-specific selector, set bounded timeouts, and log the failing destination without exposing sensitive data.
Empty or incomplete page data The content renders after initial navigation or requires interaction. Wait for the relevant selector or application state; handle redirects and consent screens as part of the task logic where appropriate.
Request returns 400 The url parameter is missing or is not a valid HTTP(S) URL. URL-encode the query parameter and provide a complete URL beginning with http:// or https://.
Request returns 502 The example caught a browser launch or navigation error. Inspect Cloud Run logs for the underlying cause, then check the executable, libraries, target availability, and timeout.
Permission denied or deployment cannot read image The deployer or Cloud Run service agent lacks the permission needed for the selected image/repository setup, or an API is not enabled. Follow the current Google deployment guide for the chosen path and grant only the roles required by your setup.
Service is unreachable Invocation requires authentication, or ingress/access settings do not match the caller. Check the service’s authentication and network configuration. Use an identity token for authenticated invocation, or intentionally configure public access.
Memory pressure or slow requests under load Too many simultaneous browser pages, unclosed pages, or unusually heavy destinations. Close resources on every path, limit concurrency, simplify the page workload, and size the service based on observed behavior.

7. Performance, reliability, and cost

There is no single meaningful Puppeteer-on-Cloud-Run performance number: browser version, page behavior, resource loading, concurrency, and allocated resources all affect results. Use representative destinations and measure end-to-end latency, failure rates, memory, and CPU in your own service. Lower concurrency can reduce contention but may require more instances; higher concurrency can increase contention and resource pressure. Tune from observed workload behavior.

Reliability depends on handling external websites as unreliable dependencies. Set timeouts, make retries selective, and avoid retrying non-idempotent form submissions without a safe deduplication strategy. Record the target host, operation stage, and failure category in logs while avoiding cookies, authorization headers, or page contents that contain sensitive data. Close browser resources even on failure.

Cloud Run charges and overall cost depend on the deployment configuration and workload; browser CPU and memory consumption matter, as can image storage/builds and network use. The research does not establish a fixed cost for this example. Estimate using your actual region, service configuration, invocation volume, and measured duration with the current Google Cloud pricing information. Avoid keeping a browser active unnecessarily, and do not size capacity from guesses.

Or skip the browser setup

If the job is to capture a website, ScreenshotNeo is a website screenshot API and MCP server. One GET request can return a PNG, JPEG, WebP, or PDF. Its screenshot API can avoid building and maintaining a Chromium container for capture-only work. See the ScreenshotNeo API documentation.

cURL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

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)

Node.js:

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(`ScreenshotNeo returned ${res.status}`);
await Bun.write('shot.webp', res);

The Node example uses Bun’s file helper to save the response; with Node.js, use this equivalent save step:

import { writeFile } from 'node:fs/promises';
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(`ScreenshotNeo returned ${res.status}`);
await writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
  • Cookie and consent banners are accepted like a visitor, and 60+ known consent platforms, newsletter popups, and chat widgets are removed before capture; each step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Response headers report the page verdict and billing status.
  • An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
  • The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.

Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.

FAQ

Can I use Puppeteer in a Cloud Run function?

Yes, if the function’s runtime image includes compatible Chromium and its required libraries. Source-based deployment builds an image; it does not eliminate the browser dependency.

Does Puppeteer install Chromium automatically?

Some Puppeteer package setups download a browser during installation. The example instead installs Chromium in the container and points puppeteer-core at it. Choose one explicit, reproducible browser installation strategy and verify it in the built image.

Should the browser service be public?

Only if the endpoint is intended for public use and is protected against unwanted use. Otherwise require authentication and grant invocation access to the identities that need it.

When is ScreenshotNeo a better fit?

When the required output is a website screenshot or PDF and you do not need custom Puppeteer interactions or browser-side application logic. Use Puppeteer when the task needs direct control of the browser workflow.

Official Google Cloud references