# How to get WebGPU in Headless Chrome on Cloud GPUs

# Introduction

I got WebGPU working in headless Chrome with a real NVIDIA GPU in a container (no display).

Tested and verified on RunPod (A40, driver 570), Google Colab (T4, driver 580), and Modal (T4, driver 580).

Struggled on this for about ~6 hours. Had to fix bunch of undocumented issues. 🫩

# Results

| Platform | GPU | Driver | Adapter Found | Compute Shader |
| --- | --- | --- | --- | --- |
| RunPod | A40 | 570 | 3/3 consistent | not tested (SSH corruption) |
| Colab | T4 | 580 | 3/3 consistent | \[1,2,3,4,5,6,7,8\] → \[2,4,6,8,10,12,14,16\] with 18 features |

---

# The 4 Problems and Their Fixes

## Problem 1: Vulkan loader too old

**Symptom:** `vulkaninfo` shows `ERROR_INCOMPATIBLE_DRIVER` or `Could not get vkCreateInstance`.

**Cause:** Ubuntu 22.04 ships Vulkan loader 1.3.204. NVIDIA drivers 570+ declare Vulkan 1.4. The old loader rejects the newer driver entirely.

**Fix:**

```bash
wget -qO - https://packages.lunarg.com/lunarg-signing-key-pub.asc | apt-key add -
wget -qO /etc/apt/sources.list.d/lunarg-vulkan.list \
  https://packages.lunarg.com/vulkan/lunarg-vulkan-jammy.list
apt update -qq && apt install -qqq libvulkan1
```

**Verify:** `vulkaninfo --summary` should show your GPU (e.g. Tesla T4 or NVIDIA A40) with Vulkan 1.4.x.

## Problem 2: Missing NVIDIA Vulkan ICD

**Symptom:** `vulkaninfo` says `no vulkan icd` or `no GPU devices found`.

**Cause:** The container has `libnvidia-gl-525` installed but the actual driver is 570/580. The ICD package must match the running driver version.

**Fix:**

```bash
# Check your driver version first
nvidia-smi | head -3

# Install matching package (replace 580 with your version)
apt install -qqq libnvidia-gl-580
```

**Note:** If this fails with `Invalid cross-device link` (RunPod), the driver libs are host-mounted and can't be replaced. In that case the ICD may already exist at `/etc/vulkan/icd.d/nvidia_icd.json` — check with `cat /etc/vulkan/icd.d/*.json`.

## Problem 3: navigator.gpu is undefined

**Symptom:** `navigator.gpu` returns `undefined` in `page.evaluate()`.

**Cause:** Two things:

1. WebGPU requires a **secure context**. `about:blank` (Puppeteer's default page) is NOT a secure context. [`http://localhost`](http://localhost) IS.
    
2. Puppeteer silently injects `--use-angle=swiftshader-webgl` which **overrides** your `--use-angle=vulkan`.
    

**Fix:**

```javascript
// 1. Start a localhost server
const http = require("http");
const server = http.createServer((req, res) => {
  res.writeHead(200);
  res.end("ok");
});
server.listen(8080, "127.0.0.1");

// 2. Override Puppeteer's default ANGLE flag
const browser = await puppeteer.launch({
  headless: "new",
  ignoreDefaultArgs: ["--use-angle=swiftshader-webgl"],
  args: [
    /* your flags */
  ],
});

// 3. Navigate to localhost BEFORE checking navigator.gpu
const page = await browser.newPage();
await page.goto("http://127.0.0.1:8080");
```

Also start dbus before launching Chrome:

```bash
/etc/init.d/dbus start
```

## Problem 4: requestAdapter() returns null

**Symptom:** `navigator.gpu` exists, but `requestAdapter()` returns null.

**Cause:** Chrome's WebGPU engine (Dawn) has its **own GPU blocklist** separate from Chrome's `--ignore-gpu-blocklist` flag. Dawn rejects NVIDIA drivers 570+ by default.

**Fix:** Disable Dawn's adapter blocklist:

```plaintext
--enable-dawn-features=allow_unsafe_apis,disable_adapter_blocklist
--disable-dawn-features=disallow_unsafe_apis
```

Also use `powerPreference: 'low-power'` in the adapter request (the default preference returns null on some setups):

```javascript
const adapter = await navigator.gpu.requestAdapter({
  powerPreference: "low-power",
});
```

---

# The Complete Working Setup

## System setup (run once)

```bash
# 1. Install Chrome
apt update && apt install -y wget
wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt install -y ./google-chrome-stable_current_amd64.deb

# 2. Install matching NVIDIA GL/Vulkan ICD (replace version as needed)
apt install -qqq libnvidia-gl-580  # or 570, match nvidia-smi output

# 3. Upgrade Vulkan loader to 1.4+
wget -qO - https://packages.lunarg.com/lunarg-signing-key-pub.asc | apt-key add -
wget -qO /etc/apt/sources.list.d/lunarg-vulkan.list \
  https://packages.lunarg.com/vulkan/lunarg-vulkan-jammy.list
apt update -qq && apt install -qqq libvulkan1

# 4. Start dbus
/etc/init.d/dbus start

# 5. Verify Vulkan sees your GPU
vulkaninfo --summary 2>&1 | grep -A3 "GPU0"

# 6. Install Node + Puppeteer
npm install puppeteer-core
```

## JavaScript (the exact code that works)

```javascript
import puppeteer from "puppeteer-core";
import { createServer } from "http";

// Localhost server for secure context
const server = createServer((req, res) => {
  res.writeHead(200);
  res.end("ok");
});
await new Promise((r) => server.listen(8080, "127.0.0.1", r));

const browser = await puppeteer.launch({
  executablePath: "/usr/bin/google-chrome-stable",
  headless: "new",
  ignoreDefaultArgs: ["--use-angle=swiftshader-webgl"],
  args: [
    "--no-sandbox",
    "--headless=new",
    "--enable-unsafe-webgpu",
    "--enable-features=Vulkan",
    "--use-angle=vulkan",
    "--disable-vulkan-surface",
    "--ignore-gpu-blocklist",
    "--disable-gpu-sandbox",
    "--enable-dawn-features=allow_unsafe_apis,disable_adapter_blocklist",
    "--disable-dawn-features=disallow_unsafe_apis",
  ],
});

const page = await browser.newPage();
await page.goto("http://127.0.0.1:8080");

const result = await page.evaluate(async () => {
  if (!navigator.gpu) return "no gpu";
  const adapter = await navigator.gpu.requestAdapter({
    powerPreference: "low-power",
  });
  if (!adapter) return "no adapter";
  return (
    "FOUND: features=" +
    adapter.features.size +
    " fallback=" +
    adapter.isFallbackAdapter
  );
});

console.log(result);
await browser.close();
server.close();
```

---

# Chrome Flags Explained

| Flag | Why |
| --- | --- |
| `--no-sandbox` | Required in containers (running as root) |
| `--headless=new` | New headless mode (not legacy) |
| `--enable-unsafe-webgpu` | Enable WebGPU on Linux |
| `--enable-features=Vulkan` | Enable Vulkan graphics backend |
| `--use-angle=vulkan` | Tell ANGLE to use Vulkan |
| `--disable-vulkan-surface` | Disable swapchain (no display needed, offscreen only) |
| `--ignore-gpu-blocklist` | Ignore Chrome's GPU blocklist |
| `--disable-gpu-sandbox` | Disable GPU process sandbox in container |
| `--enable-dawn-features=allow_unsafe_apis,disable_adapter_blocklist` | **THE KEY FLAG.** Disables Dawn's own separate blocklist that rejects newer NVIDIA drivers |
| `--disable-dawn-features=disallow_unsafe_apis` | Companion to above |

# Puppeteer Gotchas

1. `ignoreDefaultArgs: ['--use-angle=swiftshader-webgl']` — Puppeteer silently injects this flag which forces SwiftShader and overrides your Vulkan flag. You MUST exclude it.
    
2. **Navigate to** [`http://localhost`](http://localhost) before checking `navigator.gpu` — WebGPU requires a secure context. `about:blank` is not one. [`http://localhost`](http://localhost) is.
    
3. **Use** `powerPreference: 'low-power'` in `requestAdapter()` — The default preference returns null on some setups.
    
4. **Start dbus** — Chrome expects dbus to be running. `/etc/init.d/dbus start` before launching.
    

# The Stack

```plaintext
Your Three.js code
  → WebGPU API (navigator.gpu)
    → Chrome's Dawn engine
      → Vulkan
        → NVIDIA driver
          → GPU hardware
```

# What Does NOT Work

* **Browserbase** — No GPU. Intentionally blocked. SwiftShader only.
    
* **Browserless self-serve** — No GPU on standard plans. SwiftShader only.
    
* **AWS Lambda** — No GPU available.
    
* **Vercel serverless** — No GPU available.
    
* **Any container with mismatched Vulkan loader + NVIDIA driver versions** — Loader must support the Vulkan version the driver declares.
    
* **Puppeteer default flags without** `ignoreDefaultArgs` — SwiftShader override kills Vulkan.
    
* `about:blank` or [`file://`](file://) URLs — Not secure contexts. WebGPU won't initialize.
