Puppeteer Screenshot on GitHub Actions: Install Chrome and Capture a Page
Install a Puppeteer-compatible Chrome in GitHub Actions, capture a page with Node.js, and save the screenshot as a workflow artifact.
To capture a page with Puppeteer in GitHub Actions, install the project’s locked dependencies with npm ci, make sure Puppeteer’s compatible Chrome for Testing browser is installed, run a Node.js script that calls page.screenshot(), then upload the output with actions/upload-artifact. The standard puppeteer package downloads a compatible browser during installation; puppeteer-core does not, so it requires a browser you manage yourself.
This guide uses Puppeteer’s managed browser and a full-page PNG. Adjust the URL, viewport, and readiness condition for your site. The workflow below combines documented Puppeteer and GitHub Actions steps; it is a recipe, not a claim that this exact workflow was tested.
1. Add Puppeteer and a capture script
Install Puppeteer as a project dependency and commit the resulting lockfile. GitHub Actions can then reproduce the dependency versions using npm ci.
npm install --save-dev puppeteer
Create scripts/capture.mjs. This example uses ES modules, navigates to a URL from an environment variable, saves a full-page PNG, and closes the browser even if capture fails.
import puppeteer from 'puppeteer';
const targetUrl = process.env.TARGET_URL ?? 'https://example.com';
const outputPath = process.env.SCREENSHOT_PATH ?? 'artifacts/page.png';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto(targetUrl, {
waitUntil: 'networkidle2',
timeout: 60_000,
});
await page.screenshot({ path: outputPath, fullPage: true });
} finally {
await browser.close();
}
Create the output directory before running the script, as the screenshot API writes to the path you provide. You can also create it in the script with Node’s mkdir function if the path is configurable.
2. Install Chrome when package scripts are blocked
Normally, installing puppeteer downloads a compatible Chrome for Testing browser as part of package installation. If your package manager or CI policy blocks dependency lifecycle scripts, install the browser explicitly after npm ci:
npx puppeteer browsers install
Puppeteer also documents the browser management CLI for selecting a browser build, for example npx @puppeteer/browsers install chrome@stable. If you choose that route, keep the selected browser compatible with the Puppeteer release recorded in your lockfile.
Use the regular puppeteer package when you want Puppeteer to manage its compatible browser. Use puppeteer-core when your environment supplies a browser separately; then configure puppeteer.launch({ executablePath: '...' }) with that browser’s actual path. Do not assume a system Chrome path exists on every GitHub-hosted runner.
3. Add the GitHub Actions workflow
Save this as .github/workflows/capture.yml. It checks out the repository, sets up Node, installs locked dependencies, creates the output directory, captures the page, and uploads the PNG so it remains available after the job finishes.
name: Capture page
on:
workflow_dispatch:
push:
branches: [main]
jobs:
screenshot:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22.12'
cache: npm
- name: Install dependencies
run: npm ci
# Include this step if package install scripts are blocked or the
# Puppeteer browser was not installed during npm ci.
- name: Install Puppeteer browser
run: npx puppeteer browsers install
- name: Create screenshot directory
run: mkdir -p artifacts
- name: Capture page
run: node scripts/capture.mjs
env:
TARGET_URL: https://example.com
SCREENSHOT_PATH: artifacts/page.png
- name: Upload screenshot
uses: actions/upload-artifact@v4
with:
name: page-screenshot
path: artifacts/page.png
if-no-files-found: error
retention-days: 7
The explicit browser installation step is safe to keep when the browser is already present, but it may add download time. If Puppeteer’s install script is allowed and its browser is available, you can remove that step. Keep the Node version aligned with the Puppeteer version and runner operating system in your project: Puppeteer 25.12.0’s requirements specify Node 22.12 or newer, but other releases can have different requirements.
4. Choose when the page is ready
page.goto() supports navigation readiness conditions. The example uses networkidle2, which waits for a period with no more than two network connections. It is a useful starting point, but pages with analytics, streaming requests, or long polling may never reach a network-idle state.
- Use
domcontentloadedwhen the initial document is enough and the page has no important delayed content. - Use
loadwhen the page’s load event is a suitable boundary for its assets. - Use
networkidle2when the page settles after its requests finish, and the site does not keep connections active. - Wait for an application-specific selector when a known element signals that client-rendered content is ready.
For example, wait for a result panel after DOM navigation:
await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.waitForSelector('[data-capture-ready="true"]', { timeout: 30_000 });
Use a selector that is stable and present only when the content you need has rendered. A fixed delay can help with a known animation or delayed widget, but it is less reliable than waiting for a meaningful page condition.
5. Tune the screenshot output
Puppeteer’s Page.screenshot() accepts options for output format and capture behavior. These are the options most likely to matter in a CI capture:
| Need | Option or approach |
|---|---|
| Capture the full document | fullPage: true. Without it, the screenshot covers the current viewport. |
| Capture a viewport-sized image | Omit fullPage or set it to false. |
| Control dimensions and sharpness | Set viewport width, height, and deviceScaleFactor before navigation or capture. |
| Write JPEG | Use a .jpg path and set type: 'jpeg'; JPEG supports a quality value. |
| Write PNG | Use a .png path or set type: 'png'. PNG is lossless. |
| Capture an element | Find it with page.$() and call element.screenshot({ path: ... }). |
| Hide a dynamic region | Apply page CSS before capture, or use a stable test page state, so timestamps or rotating content do not make each artifact differ. |
Example JPEG capture:
await page.screenshot({
path: 'artifacts/page.jpg',
type: 'jpeg',
quality: 85,
fullPage: true,
});
For reproducible visual comparisons, keep viewport dimensions, device scale, browser version, locale, and page data consistent. Full-page capture can create a tall image and use more memory than a viewport shot. Some pages use lazy-loaded images; scrolling the page before capture can trigger them, but do this only if the full document needs those images.
6. Retrieve and retain the artifact
GitHub Actions jobs run in a temporary workspace. The upload step stores the screenshot as a workflow artifact that you can download from the workflow run. Upload either the image path or its containing directory. Make sure the path in actions/upload-artifact matches the exact path passed to page.screenshot().
Set retention-days to the period your team needs, within the limits configured for the repository or organization. For multiple captures, write each image into a directory and upload that directory. GitHub’s artifact upload action can accept a single file or a directory.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Could not find Chrome or no executable found |
Puppeteer’s install script did not run, or the browser download was skipped. | Run npx puppeteer browsers install after npm ci, or allow the package install script under your package manager policy. |
| Browser launches locally but fails on the runner | The Puppeteer release may not support the runner’s Node version, OS, or architecture, or Linux system libraries may be missing. | Check the system requirements for the exact Puppeteer release in the lockfile and use a supported runner configuration. Install required system packages if using a custom image. |
| Navigation times out | The page may keep network connections open or take longer than the default timeout. | Choose a more suitable waitUntil condition, wait for a specific selector, or raise the timeout for a genuinely slow page. |
| Screenshot is blank or content is missing | The app may render after navigation completes, or content may depend on a later API response. | Wait for a page-specific selector or response before capturing. Confirm that the target URL and required authentication are available in the workflow. |
| Upload says no files were found | The script’s output path and artifact path differ, or the capture step did not create the file. | Use one shared path, such as artifacts/page.png, in both places. Keep if-no-files-found: error to make missing output visible. |
| Screenshot differs between runs | Dynamic content, viewport differences, fonts, browser versions, or timing can affect rendering. | Pin dependencies with the lockfile, standardize viewport and scale, wait for stable content, and mask or remove volatile page regions where appropriate. |
| Workflow is slow | Chrome downloads on each clean runner, and full-page images or waits can add time. | Use dependency caching for npm packages, avoid redundant explicit browser installs when installation already succeeded, and wait only for the readiness condition the page needs. |
8. Reliability, performance, and cost
GitHub-hosted runners are fresh environments, so do not depend on a browser installed on a previous job. Keep the lockfile, Node version, runner image, and browser installation approach explicit. GitHub’s Node workflow guidance uses npm ci for lockfile-based installs; caching npm data can reduce package download work, but should not replace installing the required browser binary.
Browser downloads add setup time and network dependency. Puppeteer’s installation documentation estimates its Linux Chrome for Testing download at about 282 MB; actual time depends on runner network conditions and caches. Full-page screenshots of long pages also take more memory and produce larger artifacts. Keep only the dimensions and retention period you need.
The workflow uses GitHub Actions minutes and artifact storage according to your repository’s plan and settings. The research sources do not establish a price for a particular repository, runner type, or artifact retention configuration, so check your GitHub billing settings for those costs.
Or skip the browser setup
If your goal is the screenshot file rather than managing Chrome in CI, ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns an image or PDF. See the API documentation for request 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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
Cookie banners, popups, and chat widgets are removed before the shot, and each step can be turned off. Bot checks, blank pages, and failed loads are never billed; response headers identify the page verdict and billing status. An MCP server gives AI agents a way to take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, with no card required.
FAQ
Should I use puppeteer or puppeteer-core in GitHub Actions?
Use puppeteer when you want its installation process to provide a compatible browser. Choose puppeteer-core when you already manage the browser and executable path.
Can the screenshot be used by a later job?
Yes. Upload it as an artifact in the capture job, then download that artifact in a dependent job using GitHub’s artifact actions.
Why does this example use Node 22.12?
The cited Puppeteer 25.12.0 system requirements specify Node 22.12 or newer. Match the Node version to the Puppeteer release your project actually locks.
Does networkidle2 mean every image is ready?
No. It is a network activity condition, not a guarantee that the application has rendered the exact content you need. For important captures, wait for an application-specific signal.


