Sign inSign up

deaddev/browser

By deaddev

•Updated about 9 hours ago

Image
0

10K+

deaddev/browser repository overview

⁠browser

Source: Dockerfile⁠

Headless Browser (Chrome/Chromium) with a builtin web tool to interactive with the browser.

docker pull deaddev/browser
# or
docker pull ghcr.io/zigzagt/browser

linux/amd64, linux/arm64 — Playwright installs Chrome for Testing⁠ on amd64, Chromium on arm64.

⁠Supported Tags
TagContent
latestFull browser + browser-bridge CDP proxy
chromeAlias for latest
chromiumAlias for latest

⁠Components

The image has a layered wrapper chain and a CDP proxy server:

PathRole
/usr/local/bin/chromeEntry point. It starts both Chrome and the bridge. You should call this to start chrome
/usr/local/bin/chrome-launcherLower-level wrapper that generate launch arguments and launches the browser
/usr/local/bin/chrome-rawSymlink to the actual Chrome/Chromium binary
/usr/local/etc/chrome-uaUser-Agent string to be used
/opt/browser-bridge/Web based tool that allows access the chrome instance in the container. Default launches at port 6080

⁠Usage

⁠Run as standalone browser
docker run -d --init --name browser --shm-size=1g \
  -p 6080:6080 \
  deaddev/browser

Open http://<host>:6080 to access the browser.

⁠docker compose
services:
  browser:
    init: true
    image: deaddev/browser
    shm_size: 1g
    ports:
      - "6080:6080"
    environment:
      CHROME_USER_DATA_DIR: /data  # persist profile (cookies, sessions, extensions); omit for ephemeral
    volumes:
      - browser-data:/data
    devices:
      - /dev/dri:/dev/dri  # GPU acceleration on Linux hosts (Intel/AMD)

volumes:
  browser-data:

Drop the devices line if your host has no /dev/dri (macOS, Windows). The container falls back to bundled SwiftShader (CPU rendering) automatically.

⁠GPU acceleration

The image ships with software rendering (SwiftShader) by default. To enable hardware acceleration on Linux hosts:

HostSteps
Linux + Intel iGPUMount /dev/dri. Mesa drivers and Intel VA-API drivers are pre-installed in the amd64 image. Verify with chrome://gpu in the viewer — WebGL Renderer should report your iGPU instead of SwiftShader.
Linux + AMD GPUMount /dev/dri. Mesa Vulkan/GL drivers handle AMD.
Linux + NVIDIA GPUMount /dev/dri is not enough. Install NVIDIA Container Toolkit⁠ on the host and add --gpus all to docker run.
macOS / Windows / arm64 hostsFalls back to SwiftShader (CPU). No way to pass GPU into Linux containers via Docker Desktop. The arm64 image omits Mesa packages to stay small.
⁠Use as base image

Set executablePath to chrome in puppeteer/playwright.

FROM deaddev/browser

RUN npm install -g your-app

CMD ["your-app"]
const browser = await puppeteer.launch({ executablePath: 'chrome' });

When called with --* flags (as puppeteer/playwright do), chrome starts the bridge in the background then execs into the browser directly.

⁠Start and stop
CommandWhat it does
chrome [...args]Start Chrome + bridge in foreground (default CMD)
chrome start-detached [...args]Start Chrome + bridge in background
chrome restartRestart both, reuses saved args
chrome stopStop both
⁠Custom Chrome flags

Append extra flags to any start command. Such as chrome [... your args].

docker run -d --init --shm-size=1g -p 6080:6080 \
  deaddev/browser chrome --proxy-server=socks5://host:1080
⁠Drive from agents (MCP)

The bridge exposes a Model Context Protocol (MCP) endpoint at /mcp so any MCP-capable agent can drive this browser as a tool. Streamable-HTTP transport, stateless.

Add to your agent's MCP config (Claude Code, Cursor, etc.):

{
  "mcpServers": {
    "browser": {
      "type": "http",
      "url": "https://<your-bridge-host>/mcp"
    }
  }
}

The MCP server also sends an instructions field at the initialize handshake with a full workflow guide; agents typically read it automatically. The table below is a summary.

Discovery and tab management:

ToolPurpose
browser_list_tabs()All tabs with id/url/title/mcpOwned/attention + cap counters. Call this first.
browser_navigate(tabId, url)Navigate an EXISTING tab. Does not consume an FIFO slot. Prefer this to opening new tabs.
browser_open(url)Create a new MCP-owned tab. Counts against the FIFO cap.
browser_reload(tabId)Reload a tab (CDP Page.reload).
browser_close_tab(tabId)Close any tab. Frees the MCP FIFO slot if it was MCP-owned.

Reading pages (pick the right tool):

ToolPurpose
browser_get_snapshot(tabId)Primary. Compact accessibility-tree text with stable [uid=N] markers. Filter follows Puppeteer's interestingOnly — 5-10× denser than HTML.
browser_get_text(tabId, selector?)document.body.innerText (full, or scoped to a selector). Raw content, no structure.
browser_get_html(tabId, path, maxDepth)Last-resort single-element inspection. HTML is markup-heavy — prefer snapshot.
browser_evaluate(tabId, expression)Run JS in the page for reading only. NOT for clicks/typing/scrolling.
browser_screenshot(tabId, fullPage?)Visual confirmation only (base64 PNG).

Interacting (trusted Chrome DevTools Protocol input):

ToolPurpose
browser_click(tabId, at?, uid?, selector?, mouseButton?, doubleClick?, delayMs?, modifiers?, includeSnapshot?)Trusted mouse click. Provide at: {uid}, at: {selector}, or at: {x, y}; alternatively provide uid or selector directly. Use only one targeting field. Reveals element targets automatically. A plain left click on an option without geometry selects it through native keyboard input. Supports left/middle/right buttons, double-click, and a press duration of 0–1000 milliseconds.
browser_type(tabId, text, uid?, selector?, replace?, includeSnapshot?)Trusted text input via Input.insertText. Reveals and clicks-to-focus first if uid/selector given; otherwise uses the focused field. replace defaults to false. replace: true selects all with Ctrl+A (Command+A on macOS) before typing; an empty string with replace: true clears the field. Otherwise inserts at the current caret/selection.
browser_move_pointer(tabId, to, from?, mouseButton?, modifiers?, includeSnapshot?)Provide to: {uid}, to: {selector}, or to: {x, y} as the destination. For dragging, also provide from and mouseButton: "left" (or "middle"/"right"). The call positions at from, presses, moves along a short interpolated path, and releases.
browser_select(tabId, values, uid?, selector?, includeSnapshot?)Provide uid or selector for a native <select>. Selects values with mouse/keyboard input. Single dropdowns commit with Enter; multiple choices can emit intermediate trusted input/change events. Disabled/hidden choices are rejected and the final selection is verified.
browser_press_key(tabId, key, includeSnapshot?)Named key (Enter, Tab, Escape, ArrowDown, …) via trusted keydown/keyup.
browser_scroll(tabId, deltaX, deltaY, at?, method?)Native wheel input at at: {uid}, at: {selector}, or at: {x, y}; defaults to the viewport center. Reports observed scroll positions and whether they changed. method: "javascript" explicitly requests programmatic scrolling instead.
browser_scroll_into_view(tabId, uid?, selector?, method?, maxSteps?, includeSnapshot?)Provide uid or selector. Uses native wheel input and verifies a visible, unobscured point. maxSteps limits wheel inputs (default 30). method: "javascript" explicitly requests programmatic scrolling instead.
browser_wait_for(tabId, expression, timeoutMs, intervalMs?)Poll a JS expression until truthy or timeout.

Text replacement, pointer movement, and selection follow browser input behavior rather than assigning values or dispatching synthetic DOM (Document Object Model) events. Tool and viewer input are serialized per tab so compound gestures do not interleave. Drags use an explicit origin rather than relying on pointer position left by earlier input. Native option clicks use keyboard selection when pointer geometry is unavailable. Trusted events alone do not guarantee acceptance by bot detection.

Element clicks, typing, selection, scrolling locations, and pointer movement first use an exposed point without scrolling. They automatically use native wheel input when the target is offscreen or clipped, including nested scroll containers. Partially covered elements can be clicked on an exposed part; sticky overlays can be avoided by repositioning the target. Briefly covered targets are retried. Coordinate targets stay literal, and drag endpoints must be visible together.

The tools do not remove overlays, force-click hidden elements, or silently switch to JavaScript scrolling. A persistent modal, hidden control, or page that prevents scrolling still produces an explicit error rather than clicking the wrong element. Native option selection is the semantic fallback: a normal left click without modifiers, double-click, or hold delay selects that exact option using keyboard input and returns method: "keyboard" and selectedValues, rather than a fictitious clickedAt. When browser_scroll first reveals an offscreen location, its revealed field reports that preparation separately from the requested scroll.

Native input temporarily activates the target tab so Chromium can deliver events to background pages, then restores the tab currently shown in the viewer. Explicit JavaScript scrolling does not activate the tab.

Dragging uses one call:

browser_move_pointer({
  tabId, from: { uid: sourceUid }, to: { uid: destinationUid }, mouseButton: "left"
})

Both from and mouseButton are required for dragging; omit both for ordinary movement. Each point accepts {uid}, {selector}, or {x, y}. Another agent or the viewer can move the pointer between calls, so a previous move is not a drag origin. Coordinates are CSS (Cascading Style Sheets) pixels relative to the visible page's top-left corner, not document or screen coordinates.

⁠Native select rendering and remaining limits

Changing a native selection and displaying its popup are separate capabilities. The bridge now opts both ordinary dropdown controls and their ::picker(select) into appearance: base-select. This completes the existing styling workaround so menus are visible and mouse-operable in bridge frames. Multiple and sized listboxes remain native to preserve their keyboard behavior. Existing bridge styles are updated in place when a tab is attached, so a bridge-only restart also upgrades pages carrying the previous incomplete rule.

Measured on Linux headless Chrome/153.0.8010.12, 2026-10-21, through the actual bridge and its viewer frame protocol (tests/77-native-select.mjs):

Rendering modeMenu in bridge framesMenu in screenshotsKeyboard and browser_selectMouse selection
Native appearance: autoNoYesWorked, trusted eventsFailed in the fixture
Previous bridge rule: base-select on the control onlyNoYesWorked, trusted eventsFailed in the fixture
Current bridge rule: base-select on both the control and ::picker(select)YesYesWorked, trusted eventsWorked, trusted events

The measurement compares pixels outside the closed control in fresh bridge frames, not just the document's option values. Native and previous-bridge modes produced zero changed pixels there, while screenshots contained the popup. Their options had zero document geometry. Fully opted-in options had geometry and the menu appeared in both capture paths.

Keyboard input and value-based selection worked with initially open and closed pickers in all three modes. Native popup mouse selection failed both through the viewer and direct Chrome DevTools Protocol input, using option coordinates measured from screenshots. Page.bringToFront left the popup open, so the failure is not explained solely by the bridge's activation wrapper. The exact native-popup mouse-routing cause remains unverified. Public Input.synthesizeTapGesture calls with mouse and touch sources also failed to change the native selection in this fixture; they are not a fallback.

The previous injected rule was incomplete. Chrome's customizable-select documentation⁠ requires opting in both the control and its picker. It also documents changes to sizing, placement, and operating-system integration. Applying that mode changes website rendering; this is a compatibility workaround, not native popup capture. Higher-priority author styles and controls outside the injected document styles, including shadow roots, can retain native popups. Their option actions fall back to trusted keyboard selection, but that does not repair native popup mouse routing or put those menus into screencast frames.

Screenshots and screencasts use different capture paths: the protocol⁠ does not expose an equivalent popup-inclusion switch on startScreencast. Chromium's RenderWidgetHostViewAura::CopyFromSurfaceImpl calls CopyMainAndPopupFromSurface⁠, whereas PageHandler::StartScreencast uses a video consumer. These source paths explain why screenshot visibility must not be treated as screencast coverage.

Native popup capture and mouse routing remain separate limitations. The current policy completes the existing stylesheet without replacing website controls or changing the capture architecture. Passing selection tests alone does not establish native-popup rendering support; test 77 measures the actual bridge frames and input paths separately.

Human-in-the-loop:

ToolPurpose
browser_set_attention(tabId, message)Ask the human to act (captcha/MFA/login). Multi-paragraph message shown in a floating box on that tab.
browser_dismiss_attention(tabId)Clear a prior attention request. Idempotent.
  1. browser_list_tabs — see what's there.
  2. Reuse: browser_navigate(tabId, url) if an existing tab fits. Otherwise browser_open(url).
  3. browser_get_snapshot(tabId) — agent-readable view with [uid=N] markers.
  4. browser_click({tabId, uid}) / browser_type({tabId, uid, text}) / browser_press_key({tabId, key}) — drive the page.
  5. Pass includeSnapshot: true on an action to receive a fresh snapshot in the same response.

Agents don't manage cleanup — FIFO auto-closes the oldest MCP-owned tab when you exceed MCP_MAX_OPEN_TABS (default 3).

⁠MCP-owned tabs vs user-opened tabs
  • MCP-owned: opened via browser_open. Counted against the FIFO cap.
  • User-owned: opened by the human in the viewer, or by another channel. Never counted against FIFO, never auto-closed.
  • browser_navigate(tabId, url) works on either without affecting ownership.
  • An attention-bearing tab — MCP-owned or user-owned — is protected from FIFO eviction. browser_open errors with a clear message naming the protected tab if it would be evicted.
  • Survives restarts. MCP-ownership and attention are persisted to the profile dir and re-established after a bridge or browser restart. Each owned tab carries a sessionStorage marker (history-hash fallback for opaque origins) that's matched back to the restored tab even though Chrome assigns new internal ids. Requires a persistent profile (CHROME_USER_DATA_DIR set) and, for a browser restart, that Chrome is configured to restore the session ("continue where you left off"); with an ephemeral profile there's nothing to restore and recovery is a no-op.
⁠Captcha / human-in-the-loop workflow

When a page presents a captcha, MFA, paywall, or any human-only verification:

  1. browser_set_attention(tabId, "Please solve the captcha so I can continue.") — multi-paragraph messages are supported.
  2. browser_wait_for(tabId, "<expr that goes truthy post-resolution>", timeoutMs) — e.g. wait for a post-captcha element.
  3. browser_dismiss_attention(tabId).

The viewer pulses the tab's blinking dot and renders the message in a floating box on that tab. Concurrent attention requests are capped at MCP_MAX_ATTENTION (default 3).

Notes:

  • All tabs are shared across agents — browser_list_tabs first, reuse where possible.
  • Tools intentionally avoid enabling Runtime/Console/Debugger CDP domains, so the browser stays clean for bot-detection sites.
⁠Reverse proxy

Reverse proxy is supported out of the box, with or without a URL prefix. No configuration needed on the bridge side.

For example:

location /my-browser/ {
    proxy_pass http://browser:6080/;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

Access at https://my-nginx.com/my-browser

⁠Configuration

VariableDefaultDescription
BRIDGE_LOGinfoLog level: error, info, debug
BRIDGE_PORT6080Change this to change the port of the web tool.
CDP_HOST127.0.0.1Chrome CDP host
CDP_PORT18800Chrome remote debugging port
SCREENCAST_QUALITY80JPEG quality (1-100)
VIEWPORT_WIDTH1920Viewport width
VIEWPORT_HEIGHT1080Viewport height
CHROME_USER_DATA_DIR(unset)When set, Chrome persists its profile here. Mount a volume at this path to keep cookies/sessions/extensions across restarts. Unset = ephemeral /tmp/... profile.
MCP_MAX_OPEN_TABS3Max simultaneous MCP-owned tabs (FIFO cap).
MCP_MAX_ATTENTION3Max simultaneous attention requests.

Tag summary

Content type

Image

Digest

sha256:dded508dc…

Size

443.9 MB

Last updated

about 9 hours ago

docker pull deaddev/browser