Xvfb (X virtual framebuffer) is an X11 display server that keeps the whole screen in memory instead of sending it to a monitor, so programs that need a graphical display, such as a headed browser, can run on a Linux server that has no screen.
Before browsers had a real headless mode, Xvfb was how everyone ran Chrome and Firefox in CI and on servers. Today headless Chrome needs no display at all, so Xvfb is a tool for the cases where you deliberately run the browser with a window. This entry is part of the screenshot glossary.
What it means in plain terms
On Linux, a program with a window does not draw on the screen by itself. It connects to a display server (X11 or Wayland) through the DISPLAY environment variable, for example DISPLAY=:99, and asks it to draw. Xvfb is a display server that does everything a normal X server does, except that its "screen" is a block of memory. Nobody sees it, but the program cannot tell the difference.
flowchart LR N["Your script<br/>(Puppeteer, Playwright, Selenium)"] -- "CDP / WebDriver" --> B["Browser, headed mode"] B -- "X11 protocol via DISPLAY=:99" --> X["Xvfb<br/>screen in memory"] B -- "page.screenshot() pixels" --> N
Note the last arrow. A Puppeteer or Playwright screenshot comes from the browser's own renderer through the automation protocol, not from Xvfb's memory. Xvfb only gives the browser a place to open its window.
When you still need Xvfb
| Situation | Need Xvfb? | Why |
|---|---|---|
| Screenshots or PDFs with Puppeteer or Playwright | No | Use headless mode. It is the same Chrome without a window. |
| Debugging a page that behaves differently in headless mode | Sometimes | Run headed under Xvfb on the same server to compare. |
| Playwright or Selenium tests that must run headed in Linux CI | Yes | Headed mode on Linux requires a display. |
Tools that capture the whole X screen (ffmpeg -f x11grab, xwd) |
Yes | They record the display, not the page. |
| Desktop apps such as Electron apps in CI | Usually | Many cannot start without a display. |
| Production screenshot service | No | Every Xvfb process is one more thing to start, monitor and kill. |
The "behaves differently" row is real but shrinking. Chrome's new headless mode runs the full browser, so most differences people used to blame on headless mode, such as fonts or missing features, now come from the server environment instead.
How to set it up
These commands are for Debian or Ubuntu images. We did not run them for this page: our test machine is macOS, which has no Xvfb, and our Docker host was not available on the test date.
Install it next to the browser's system libraries:
FROM node:22-bookworm-slim RUN apt-get update \ && apt-get install -y --no-install-recommends xvfb xauth \ && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY package*.json ./ RUN npm ci && npx puppeteer browsers install chrome --install-deps COPY . . CMD ["xvfb-run", "-a", "--server-args=-screen 0 1920x1080x24", "node", "shot.mjs"]
xvfb-run starts Xvfb, sets DISPLAY and XAUTHORITY for the child command, and stops the server when the command exits. -a picks a free display number, which matters when several jobs run on one machine. -screen 0 1920x1080x24 sets the virtual screen size and a 24-bit colour depth.
Puppeteer, headed
// shot.mjs — run with: xvfb-run -a node shot.mjs import puppeteer from 'puppeteer'; if (!process.env.DISPLAY) throw new Error('No DISPLAY: start this script under xvfb-run'); const browser = await puppeteer.launch({ headless: false, args: ['--window-size=1400,1000'] }); try { const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 800 }); await page.goto('https://example.com', { waitUntil: 'load' }); await page.screenshot({ path: 'headed.png' }); } finally { await browser.close(); }
Playwright, headed
# Playwright's own CLI flag; xvfb-run supplies the display xvfb-run -a npx playwright test --headed
The same works for a plain script that calls chromium.launch({ headless: false }).
Manual Xvfb, for long-running workers
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp & export DISPLAY=:99 node worker.mjs
-nolisten tcp keeps the display off the network. If you start Xvfb this way, your process manager must also stop it; otherwise it outlives the worker, like an orphaned browser (see zombie processes).
RenderScreenshot
There is no display to configure. Captures run in a managed headless Chromium on Cloudflare's browser rendering service, and the API takes the viewport size directly:
curl "https://api.renderscreenshot.com/v1/screenshot?url=https%3A%2F%2Fexample.com&width=1280&height=800" \ -H "Authorization: Bearer $RS_API_KEY" -o example.png
This request was not run for this page.
Common mistakes
- Adding Xvfb to fix a headless problem. If a capture is wrong in headless mode, check fonts, waits and the user agent first. A virtual display rarely changes the pixels.
- A fixed display number with parallel jobs. Two
Xvfb :99servers cannot share the number. Usexvfb-run -a. - A screen smaller than the window. Make the virtual screen at least as large as
--window-size. - Leaving Xvfb running. Use
xvfb-run, or stop the server in the same supervisor that starts the worker. - Forgetting
xauth.xvfb-runneeds it, and slim images do not include it.
Related terms
For full container setups, read Puppeteer in Docker or Playwright in Docker. To capture pages without a browser or display to run, create a free account.