How to Run Headless Chrome on Linux
Run Chrome headlessly on Linux from the command line or with Puppeteer and Selenium. Learn which mode to choose, how to capture pages, and how to troubleshoot containers.
Run Headless Chrome on Linux
Install Chrome or a compatible Chrome for Testing build, then run its binary with --headless. For example:
google-chrome --headless https://example.com
Headless Chrome runs without a visible browser window; it does not require Xvfb. For scripts, use Puppeteer or Selenium-WebDriver. For repeatable CI, pin Chrome for Testing and its matching ChromeDriver, or let Puppeteer download a compatible browser. See Chrome Headless documentation and Chrome for Testing.
1. Choose the Headless mode
Since Chrome 112, modern Headless uses the regular Chrome browser implementation, creating platform windows without displaying them. It is the better fit when you need behavior close to visible Chrome, such as end-to-end tests or coverage of browser features.
Since Chrome 132.0.6793.0, the older implementation is available as the separate chrome-headless-shell binary. Chrome describes it as a lighter option with fewer dependencies. Consider it for capture workloads when its reduced feature set is acceptable. See the mode details.
| Choice | Use it when | Trade-off |
|---|---|---|
Chrome --headless |
You want full-browser behavior and feature coverage. | Uses the regular Chrome implementation. |
chrome-headless-shell |
You want a lighter capture-oriented binary and have checked its limitations. | Less authentic than full Chrome for browser testing. |
2. Install Chrome and locate its binary
The command examples assume a Chrome binary named google-chrome on your PATH. Binary names and installation steps depend on the Linux distribution and packaging method. Use Chrome’s official installation instructions for your distribution; do not assume one package command or dependency list works everywhere.
command -v google-chrome
command -v google-chrome-stable
command -v chromium
If one command prints a path, use that executable in the examples below. If none does, install Chrome or Chrome for Testing first. Chrome Headless does not need an X server or Xvfb.
3. Run common command-line tasks
Open a URL without showing a window:
google-chrome --headless https://example.com
Dump the rendered DOM:
google-chrome --headless --dump-dom https://example.com
--dump-dom serializes the parsed DOM after page scripts have had a chance to modify it; it is not simply the original HTML response.
Save a screenshot with a defined viewport:
google-chrome --headless --screenshot --window-size=1280,900 https://example.com
Chrome saves the screenshot as screenshot.png in the current working directory. Set the window size when the rendered dimensions matter.
Print the page to PDF:
google-chrome --headless --print-to-pdf=output.pdf https://example.com
For capture commands that need a bounded wait, Chrome’s CLI reference documents --timeout. For timer-driven pages, --virtual-time-budget can fast-forward page timers. These options affect capture timing; check the installed Chrome version’s supported syntax and validate the result against the page. See Chrome’s command-line reference.
4. Automate launches with Puppeteer
Puppeteer launches modern Headless by default. The following Node.js example launches Chrome, navigates to a page, captures a screenshot, and closes the browser:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 900 });
await page.goto('https://example.com', { waitUntil: 'networkidle2', timeout: 30_000 });
await page.screenshot({ path: 'shot.png', fullPage: true });
} finally {
await browser.close();
}
Install Puppeteer in your project with the package manager you use; its documented setup can acquire a compatible Chrome for Testing browser automatically. Use headless: 'shell' to select Headless Shell when that is the intended trade-off:
const browser = await puppeteer.launch({ headless: 'shell' });
Choose a navigation wait that fits the site. Network-idle conditions can wait indefinitely or too long on pages with persistent requests; a navigation event plus a selector wait can be more reliable for those pages. Puppeteer’s launch and page APIs are documented at Puppeteer Headless modes.
5. Automate launches with Selenium-WebDriver
Selenium’s Chrome options let you pass --headless. This runnable Python example opens a URL, saves a screenshot, and closes the session:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument('--headless')
options.add_argument('--window-size=1280,900')
driver = webdriver.Chrome(options=options)
try:
driver.set_page_load_timeout(30)
driver.get('https://example.com')
driver.save_screenshot('shot.png')
finally:
driver.quit()
Install Selenium using its current official instructions. Keep Chrome and ChromeDriver versions compatible. For CI, Chrome for Testing publishes version-specific Chrome and corresponding ChromeDriver downloads. Selenium bindings and driver management behavior can vary, so use the current documentation for your language binding: Selenium Chrome documentation.
6. Make CI and container runs reproducible
- Pin a Chrome for Testing version rather than allowing an unplanned browser update during a CI run.
- Use the corresponding ChromeDriver version when your automation stack requires a driver.
- Record the browser version in CI logs so failures can be tied to a specific binary.
- Run Chrome as a non-root user in containers and preserve its sandbox configuration.
- Set explicit viewport dimensions and navigation timeouts in automation code.
Chrome’s sandbox is a security isolation mechanism. Chrome documentation says a correctly configured container with a user does not need --no-sandbox; running as root without the sandbox is unsupported. Do not add --no-sandbox as a universal launch fix. Inspect the container user, permissions, and sandbox setup instead. See Chrome Headless guidance.
7. Configure timing, rendering, and graphics carefully
- Viewport: Use
--window-size=width,heightfor CLI screenshots, or set the viewport through your automation library. - Page readiness: A page load event may precede client-rendered content. Wait for the selector or application state that signals the content you need.
- Timeouts: Bound navigation and capture waits. Increase them only when the target page needs more time, rather than allowing a hung job to consume resources indefinitely.
- Virtual time: Use
--virtual-time-budgetfor timer-driven content when appropriate, and verify the page’s behavior under virtual time. - GPU workloads: Basic Headless browsing does not call for GPU flags. Chrome’s Linux graphics guidance discusses Vulkan flags such as
--use-angle=vulkan,--enable-features=Vulkan, and--disable-vulkan-surfacefor specific graphics scenarios. Treat these as workload-specific and follow the current graphics and sandbox guidance.
Do not copy GPU flags into a general browser command without a GPU-specific need. Graphics support depends on the environment and workload; the cited guidance is not a universal compatibility recipe. See Chrome’s Linux graphics guidance.
8. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Make one GET request to capture a URL as an image or PDF. Read the ScreenshotNeo API documentation for parameters and response details.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get(
'https://api.screenshotneo.com/v1/shot',
params={'access_key': 'YOUR_API_KEY', 'url': 'https://example.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://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', image));
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf 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 shots. Create a free ScreenshotNeo account.
Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
google-chrome: command not found |
Chrome is absent or has a different binary name or path. | Check command -v for Chrome and Chromium names; install using the official instructions for the distribution or invoke the installed path. |
| Chrome starts locally but fails in a container | Container user, permissions, or sandbox configuration is incorrect. | Inspect the user and sandbox setup. Prefer a correctly configured non-root container; do not disable the sandbox as a blanket fix. |
| WebDriver reports a session or version error | Chrome and ChromeDriver are mismatched. | Pin a Chrome for Testing release and its corresponding driver, or use Puppeteer’s compatible browser download. |
| Screenshot is blank or misses content | The page has not rendered the target content when capture occurs, or a page-specific script failed. | Wait for a meaningful selector or application state, set the viewport explicitly, and inspect the rendered page or dumped DOM. |
| Capture hangs or times out | Navigation, persistent network activity, or page scripts do not reach the chosen readiness condition. | Set a bounded timeout, choose a less restrictive wait condition, or wait for the specific content you need. |
| GPU or WebGL content fails | The container’s graphics stack does not provide the required support. | Confirm GPU acceleration is required, then follow Chrome’s current Linux graphics guidance for that workload; avoid generic flag recipes. |
Performance, reliability, and cost notes
- Performance: The research sources provide no general speed benchmark comparing modern Headless and Headless Shell. Measure your own page and environment; browser startup, page scripts, assets, and wait conditions all affect job time.
- Reliability: Pin browser and driver versions, use explicit timeouts and readiness conditions, and configure the sandbox correctly. These steps make CI behavior easier to reproduce.
- Cost: Self-hosting has no per-screenshot API charge in the cited material, but your infrastructure and engineering costs depend on deployment. ScreenshotNeo’s free tier is 1,000 shots monthly; paid tiers are Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan.
Frequently asked questions
Do I need Xvfb to run Chrome headlessly?
No. Chrome Headless runs without a display server such as Xvfb.
Does --headless change the original page HTML?
No. It controls browser display mode. The --dump-dom command outputs the parsed DOM after scripts can modify it.
Should I use Chrome Headless or Headless Shell?
Use modern Headless for full Chrome behavior and feature coverage. Consider Headless Shell for a lighter capture workload when its reduced feature set is acceptable.
Can I run Chrome as root in Docker?
Chrome’s documented guidance says running as root without the sandbox is unsupported. Configure a non-root container user and retain the sandbox.


