Skip to content

chrome

Recipe card from the charly-selkies plugin (Images — the deployable catalog).

Property Value
Dependencies supervisord
Composed candies chrome-devtools-mcp
Ports 9222 (CDP, via cdp-proxy), 9224 (Chrome DevTools MCP, via chrome-devtools-mcp)
CDP Proxy cdp-proxy on 0.0.0.0:9222 → Chrome on 127.0.0.1:9223
Volumes chrome-data -> ~/.chrome-debug
Security shm_size: 1g, memory_max: 6g, memory_high: 5g, memory_swap_max: 2g
Install files charly.yml, task:, cdp-proxy, chrome-wrapper, chrome-restart, browser-open
Variable Value
CHROME_FLAGS --ozone-platform=wayland --enable-features=UseOzonePlatform,VaapiVideoDecodeLinuxGL,VaapiIgnoreDriverChecks
BROWSER browser-open

PATH additions: ~/.local/bin

Variable Template Value
BROWSER_CDP_URL http://{{.ContainerName}}:9222

Pod-aware: same-container consumers receive http://localhost:9222, cross-container consumers receive http://charly-<image>:9222. Consumed by hermes (env_accept: BROWSER_CDP_URL) for shared browser automation.

Name URL Template Transport
chrome-devtools http://{{.ContainerName}}:9224/mcp http

Provided by the auto-included chrome-devtools-mcp sub-candy (29 Chrome DevTools tools via mcp-proxy). Consumed by hermes (mcp_accept: chrome-devtools) for MCP-based browser inspection and automation. See /charly-selkies:chrome-devtools-mcp for tool list and architecture.

When deploying with -i <instance>, the MCP server name is automatically disambiguated to chrome-devtools-<instance> (e.g., chrome-devtools-31.58.9.4). See /charly-core:charly-config for MCP name disambiguation details.

Variable Description
HTTP_PROXY HTTP proxy URL for Chrome (e.g., http://proxy:8080)
HTTPS_PROXY HTTPS proxy URL for Chrome (e.g., http://proxy:8080)
NO_PROXY Comma-separated bypass list (e.g., localhost,127.0.0.1,.internal)

Chrome does NOT natively respect HTTP_PROXY/HTTPS_PROXY environment variables. The chrome-wrapper script translates them to Chrome CLI flags at launch:

  • Both set: --proxy-server=http=<HTTP_PROXY>;https=<HTTPS_PROXY>
  • Only one set: --proxy-server=<value> (applies to all protocols)
  • NO_PROXY--proxy-bypass-list=<list> (only added when a proxy is configured)

Uppercase takes precedence over lowercase (HTTP_PROXY over http_proxy).

NO_PROXY auto-enrichment: charly config runs enrichNoProxy() before writing the quadlet, adding every deployed container’s hostname (charly-<image>, charly-<image>-<instance>) to NO_PROXY. This is required because Chrome does not support CIDR notation in NO_PROXY (unlike curl/requests) — only exact hostnames work, so charly pre-computes the list. Semicolons in user-provided values are auto-converted to commas since Chrome only accepts comma-separated lists. See /charly-core:charly-config (Environment Variable Handling → NO_PROXY enrichment).

Terminal window
# Deploy with proxy
charly config selkies-desktop -e HTTP_PROXY=http://proxy:8080 -e "NO_PROXY=localhost,127.0.0.1"
# Per-instance proxy (separate from base deployment)
charly config selkies-desktop -i proxy \
-e HTTP_PROXY=http://31.58.9.4:6077 \
-e HTTPS_PROXY=http://31.58.9.4:6077 \
-e "NO_PROXY=localhost,127.0.0.1" \
-p 3001:3000 -p 9232:9222

RPM (system): liberation-fonts, vulkan-loader, mesa-dri-drivers, alsa-lib, at-spi2-core, cups-libs, gtk3, libdrm, libxkbcommon, nss, procps-ng, iproute, libva-utils RPM (tasks:): google-chrome-stable (from Google’s direct RPM)

charly.yml
my-browser:
candy:
- chrome

Usually used via the chrome-sway or sway-desktop composition candies rather than directly.

Web sign-in at accounts.google.com works via CDP + VNC hybrid automation (see /charly-check:cdp for the full recipe). All clicks locate the element with the cdp: coords verb (CDP selector targeting — it reports the desktop coords) and deliver the pointer via a vnc: click step at those coords, and all text input uses a vnc: type step (real OS-level keysym events). Sign-in cookies persist in the chrome-data volume (~/.chrome-debug), surviving container restarts. Use charly remove <image> --purge to clear for a fresh start — just rebuilding the box does not reset volumes.

App Passwords (required for automation): Google accounts with 2FA (now mandatory for most accounts) require a 16-character App Password. App Passwords bypass all verification challenges and 2FA prompts. Set GMAIL_PASSWORD to the App Password in .env.

Windows spoofing effectiveness: Tested with App Password — no CAPTCHA or verification challenges were triggered. The three-level identity spoofing (User-Agent header, navigator.platform JS, Sec-CH-UA-Platform headers) successfully prevents Google from flagging the sign-in as suspicious.

Fresh profile first-run: A fresh chrome-data volume triggers Chrome’s first-run flow: a separate dialog window (“Make Google Chrome the default browser”) + chrome://intro/ page. The dialog is invisible to CDP and must be dismissed via sway focus + VNC Return key before proceeding.

Chrome Sync: Fully supported. After sign-in, Chrome navigates to chrome://sync-confirmation/ (a chrome:// page, not a native dialog). Click #confirmButton (“Yes, I’m in”) with --vnc to enable sync. Sync state persists in the chrome-data volume.

All Chrome launches go through chrome-wrapper (~/.local/bin/chrome-wrapper), which adds CDP flags (--remote-debugging-port=9223), Windows User-Agent spoofing, and GPU detection. A symlink ~/.local/bin/google-chrome-stable -> chrome-wrapper ensures Chrome always uses the wrapper regardless of how it’s launched (sway exec, desktop file, app menu, or Chrome’s self-restart after close/crash). The wrapper calls /opt/google/chrome/chrome directly to avoid recursion through the symlink.

The chrome-wrapper detects the sway renderer by reading Sway’s /proc/<pid>/environ for WLR_RENDERER. On NVIDIA with gles2 (the default), Chrome gets full GPU acceleration. The wrapper sets NVIDIA EGL vars and VAAPI flags automatically.

If WLR_RENDERER=pixman is detected (e.g., explicit override via WLR_RENDERER env var), the wrapper strips NVIDIA EGL vars and VAAPI flags so Chrome falls back to software rendering naturally. --disable-gpu must NOT be used — it breaks CDP tab creation.

The detection uses pgrep -x sway (exact process name match — NOT pgrep -f which matches sway-wrapper and sway-autotile).

Chrome 146+ rejects HTTP requests to the DevTools API when the Host header contains a non-localhost, non-IP hostname (e.g., charly-selkies-desktop:9222). This breaks cross-container CDP access via env_provide: BROWSER_CDP_URL.

Architecture: A cdp-proxy Python supervisord service listens on 0.0.0.0:9222 and forwards requests to Chrome on 127.0.0.1:9223. Chrome binds only to the internal loopback address.

Host header rewrite: Incoming requests with container hostnames (e.g., Host: charly-selkies-desktop:9222) are rewritten to Host: localhost:9223 before forwarding to Chrome, satisfying Chrome’s localhost-only check.

Response URL rewrite: Chrome’s DevTools HTTP API returns WebSocket URLs containing ws://localhost:9223/... in JSON responses. The proxy rewrites these to ws://<client-host>:9222/... (using the original Host header from the client request) so that cross-container WebSocket connections route back through the proxy.

Content-Length correction: After URL rewriting, response body sizes change. The proxy recalculates and corrects the Content-Length header to match the rewritten body.

The chrome candy depends on supervisord (not socat) to run cdp-proxy as a managed service.

The wrapper cleans ShaderCache and GpuCache directories from the Chrome profile on startup. Corrupted caches (from pkill -9 or hard container stops) cause secondary SIGILL crashes even with correct GPU flags. Cleaning on every launch prevents this.

The browser-open helper (set as BROWSER env var) handles three states:

  1. Chrome running + CDP responsive: Reuses the existing instance, opens the URL in a new tab.
  2. Chrome running + CDP unresponsive: Waits for CDP to become available (avoids double-launching).
  3. Chrome not running: Launches Chrome via swaymsg exec chrome-wrapper with SWAYSOCK discovery (glob on /tmp/sway-ipc.*.sock, newest first).

Uses pgrep to detect Chrome process state before deciding whether to launch. This is used by OAuth flows (BROWSER=browser-open causes OAuth URLs to auto-open in Chrome via CDP).

The chrome-restart script kills any running Chrome and relaunches it via chrome-wrapper with all proxy, CDP, and user-agent settings intact. Used by waybar’s custom/chrome button (see /charly-selkies:waybar) for one-click Chrome restart.

Compositor detection: Tries swaymsg exec first (sway — needs IPC context for proper window management), falls back to direct chrome-wrapper & (labwc). Uses the same SWAYSOCK discovery pattern as browser-open and waybar-wrapper (ls -t /tmp/sway-ipc.*.sock).

Kill sequence: SIGTERM first, then SIGKILL after 1 second for stubborn processes. Matches Chrome by chrome.*remote-debugging-port pattern (same as browser-open).

Terminal window
# Manual usage (inside container)
chrome-restart
# Via charly shell
charly shell selkies-desktop -i 45.39.130.177 -c "chrome-restart"

When chrome-restart is not enough: Chrome uses memfd_create / anonymous shared memory extensively (IPC buffers, Skia texture pools, compositor tiles). These memfds are reference-counted — when a Chrome child dies (OOM kill or SIGKILL), any surviving peer that still has the memfd mapped keeps the pages pinned. The browser process and zygote inherit memfds across renderer crashes. Over multiple crash cycles this accumulates as “orphan shmem” inside the container’s cgroup, and chrome-restart cannot release it because the container (and therefore the memory namespace) persists. The only way to clear it is to restart the whole container, which tears down the cgroup:

Terminal window
systemctl --user restart charly-selkies-desktop.service
# or per-instance
systemctl --user restart charly-selkies-desktop-192.241.92.221.service

This container restart is manual (or via charly update); see “Chrome supervision (selkies-core)” below for how the [program:chrome] service relaunches Chrome on ordinary exits.

The chrome candy ships cgroup caps in its security: block:

Directive Value Purpose
memory_max 6g Hard OOM threshold — cgroup-scoped, no host impact.
memory_high 5g Soft limit — reclaim pressure kicks in before OOM so Chrome can shed caches instead of being hard-killed.
memory_swap_max 2g Caps swap usage so a runaway tab can’t drag the host into swap thrash.
shm_size 1g Existing /dev/shm sizing — unchanged.

Override per box or per instance via charly config flags (see /charly-core:charly-config “Resource Caps”). Merging follows the smallest-wins rule.

On the selkies streaming desktop, Chrome is launched + supervised by a [program:chrome] supervisord service declared in the selkies-core candy (/charly-selkies:selkies-core) — shared by both selkies flavors (labwc via selkies-desktop, KDE Plasma via selkies-kde-desktop). sway-browser-vnc launches Chrome via the separate chrome-sway candy and is not supervised this way. The selkies-core service:

  • restart: always (autorestart) — relaunches Chrome on any exit, including the clean self-exit a Chrome started during the nested compositor’s startup-race produces (the relaunch lands post-settle, where Chrome stays up).
  • autostart defaults true and is self-synchronizing: chrome-wrapper polls for the wayland-0 client socket itself, so no per-flavor supervisorctl start handoff is needed.
  • start_secs: 5 + start_retries: 3 — a run that lasts longer than 5 s resets the retry budget, so the single startup-race self-exit never trips FATAL.

A genuinely wedged crash loop (orphan memfd shmem accumulation) is cleared by restarting the whole container, which tears down the cgroup — see “When chrome-restart is not enough” above.

Chrome launches with a Windows User-Agent and loads the chrome-windows extension to present a consistent Windows identity:

  • User-Agent header: Overridden to Mozilla/5.0 (Windows NT 10.0; Win64; x64)... via --user-agent flag
  • navigator.platform: Spoofed to "Win32" via content script (chrome-windows/content.js)
  • navigator.userAgentData.platform: Spoofed to "Windows"
  • Sec-CH-UA-Platform header: Set to "Windows" via declarativeNetRequest rules (chrome-windows/rules.json)

This reduces the chance of Google flagging the sign-in as suspicious (Linux + automation fingerprint).

Most boxes use gles2 on NVIDIA headless — Chrome gets full GPU acceleration. VNC boxes (sway-desktop-vnc) use pixman instead — Chrome falls back to software rendering (chrome-wrapper auto-detects WLR_RENDERER=pixman and strips NVIDIA EGL vars). No special handling needed — the chrome-wrapper adapts automatically to both renderers.

Chrome 147 introduced a breaking change to the CDP HTTP API:

  • /json/new requires PUTGET /json/new?<url> returns 405 Method Not Allowed. Use PUT /json/new?<url> instead. This affects programmatic tab creation via curl or HTTP clients.
  • Host header validation (since Chrome 146) — Non-localhost Host headers are rejected. The cdp-proxy rewrites headers to satisfy this check (see CDP Proxy section above).
Terminal window
# Chrome 147+: create new tab (PUT required)
curl -s -X PUT "http://localhost:9222/json/new?https://example.com"
# List tabs (GET still works)
curl -s "http://localhost:9222/json/list"

The cdp: check verb now shows diagnostics on connection failure: checks Chrome process, cdp-proxy status, and port binding. Hints point to relaunching Chrome with a wl: exec step running chrome-wrapper (not charly shell with bare swaymsg) for manual Chrome restart.

  • Part of chrome-sway / sway-desktop composition (used in sway-browser-vnc)
  • /charly-check:cdp — Chrome DevTools Protocol automation (click, type, check, screenshot)
  • /charly-core:shell — Interactive shell to access Chrome
  • /charly-check:vnc — VNC automation (delivers the pointer for cdp:-located clicks; a cdp: coords step reports the desktop X/Y, then a vnc: click step delivers the click there)
  • /charly-check:wl — Wayland automation (delivers the pointer for cdp:-located clicks via wlrctl; a cdp: coords step reports the desktop X/Y, then a wl: click step delivers the click there)
  • /charly-core:charly-config — Proxy deployment, normalizeNoProxy() auto-conversion, sep:"none" env handling
  • /charly-build:charly-mcp-cmd — the auto-included chrome-devtools-mcp sub-candy exposes 29 tools via Streamable HTTP on port 9224; run the declarative 2-check suite via charly check live <image> --filter mcp

Use when the user asks about:

  • Google Chrome in containers
  • Chrome DevTools Protocol (CDP) on port 9222
  • Browser automation or browser-open
  • The chrome candy, CHROME_FLAGS, or shm_size
  • /charly-image:layer — candy authoring reference (charly.yml schema, task verbs, service declarations)
  • /charly-check:check — declarative testing (check: block, charly check box, charly check live)