name: chrome description: | Google Chrome with DevTools on port 9222, Chrome DevTools MCP on port 9224, and browser-open helper. Use when working with Chrome, CDP, browser automation, or DevTools Protocol.
chrome -- Google Chrome with DevTools
Candy Properties
| 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 |
Environment Variables
| Variable | Value |
|---|---|
CHROME_FLAGS |
--ozone-platform=wayland --enable-features=UseOzonePlatform,VaapiVideoDecodeLinuxGL,VaapiIgnoreDriverChecks |
BROWSER |
browser-open |
PATH additions: ~/.local/bin
Environment Provides
| 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.
MCP Provides
| 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.
Environment Accepts (Proxy)
| 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).
# 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
Packages
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)
Usage
# charly.yml
my-browser:
candy:
- chrome
Usually used via the chrome-sway or sway-desktop composition candies rather than directly.
Google Sign-In
Web sign-in at accounts.google.com works via CDP + VNC hybrid automation (see /charly-check:cdp for the full recipe). All clicks use charly check cdp click --vnc (CDP selector targeting + VNC pointer delivery), and all text input uses charly check vnc type (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.
Chrome Wrapper
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.
GPU Detection
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).
CDP Proxy
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.
ShaderCache / GpuCache Cleanup
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.
browser-open On-Demand Chrome Launch
The browser-open helper (set as BROWSER env var) handles three states:
- Chrome running + CDP responsive: Reuses the existing instance, opens the URL in a new tab.
- Chrome running + CDP unresponsive: Waits for CDP to become available (avoids double-launching).
- Chrome not running: Launches Chrome via
swaymsg exec chrome-wrapperwith 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).
chrome-restart
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).
# 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:
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.
Resource Caps
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.
Chrome supervision (selkies-core)
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).autostartdefaults true and is self-synchronizing:chrome-wrapperpolls for thewayland-0client socket itself, so no per-flavorsupervisorctl starthandoff 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 tripsFATAL.
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.
Windows Platform Spoofing
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-agentflag - 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).
NVIDIA Headless Notes
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+ CDP Changes
Chrome 147 introduced a breaking change to the CDP HTTP API:
/json/newrequires PUT —GET /json/new?<url>returns405 Method Not Allowed. UsePUT /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-proxyrewrites headers to satisfy this check (see CDP Proxy section above).
# 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"
CDP Diagnostics
charly check cdp commands now show diagnostics on connection failure: checks Chrome process, cdp-proxy status, and port binding. Hints use charly check wl sway exec <image> chrome-wrapper (not charly shell with bare swaymsg) for manual Chrome restart.
Used In Boxes
- Part of
chrome-sway/sway-desktopcomposition (used insway-browser-vnc)
Related Candies
/charly-selkies:chrome-devtools-mcp— Chrome DevTools MCP server (auto-included viacandy:)/charly-infrastructure:supervisord— required dependency for cdp-proxy service/charly-hermes:hermes— consumesBROWSER_CDP_URLviaenv_acceptandchrome-devtoolsviamcp_accept/charly-selkies:selkies-desktop-layer— desktop metalayer composing chrome with labwc, pipewire, waybar, etc.
Related Commands
/charly-check:cdp— Chrome DevTools Protocol automation (click, type, check, screenshot)/charly-core:shell— Interactive shell to access Chrome/charly-check:vnc— VNC automation (used with--vncflag oncharly check cdp click)/charly-check:wl— Wayland automation (used with--wlflag oncharly check cdp click)/charly-core:charly-config— Proxy deployment,normalizeNoProxy()auto-conversion,sep:"none"env handling/charly-build:charly-mcp-cmd— the auto-includedchrome-devtools-mcpsub-candy exposes 29 tools via Streamable HTTP on port 9224; probe withcharly check mcp list-tools <image>or run the declarative 2-check suite viacharly check live <image> --filter mcp
When to Use This Skill
Use when the user asks about:
- Google Chrome in containers
- Chrome DevTools Protocol (CDP) on port 9222
- Browser automation or
browser-open - The
chromecandy,CHROME_FLAGS, orshm_size
Related
/charly-image:layer— candy authoring reference (charly.ymlschema, task verbs, service declarations)/charly-check:check— declarative testing (check:block,charly check box,charly check live)