WebDesigner: Configuring a node to run containerized Chrome Dockerfile Dockerfile.linux-linux Dockerfile.macos-linux Dockerfile.windows Dockerfile.windows-linux Dockerfile.windows-windows  Status: Development / validation. Do not enable for production until the acceptance checklist in the implementation progress log ( documentation/BrowserManager/design/containerized-browser-pool-implementation-progress.md) is complete. Overview DSServer can run Chrome sessions inside disposable Docker containers instead of a local browser profile. Each session leases a container from a pool; when the session ends the container's Chrome process and lease directory are cleaned up and the container is reused. Local browser profiles keep working through the existing manager path — containerized Chrome is an additional profile ( ChromeContainerized), not a replacement. Source lives in DSServer/container-browser/ (Dockerfiles, shared browser-supervisor.py, baked extensions/, host guides under guides/, and global.json examples under config/). Installed AIQ packages use the release-matched container-browser bundle shipped with that release (see Installed AIQ package vs. source checkout). Key concepts The container OS selects the image, not the host OS. A Mac or a Windows 11 machine in Linux-container mode runs the Linux image. There is no native macOS container mode. Two images: Linux ( appvance/ds-chrome:local, Debian bookworm + Google Chrome stable, linux/amd64) and Windows ( appvance/ds-chrome-windows:local, Windows Server 2025 LTSC + Chrome for Testing + embeddable Python). Tags are distinct, so building one never overwrites the other. Headless only. Neither image starts a visible desktop browser window. A new manager per session; only the container pool registry is shared. No persistent state. No bind mounts, no persistent profiles. Linux uses tmpfs on a read-only root filesystem; Windows uses the container's disposable writable layer. Idle containers run no Chrome process. DSServer and Docker on the same host. Every recipe assumes this. A remote Docker daemon, rootless Docker, or a custom network needs a separate address and CDP routing review. Docker access is root/admin-equivalent. The account DSServer runs as must be able to use the Docker CLI against the configured context; restrict that access to trusted operators. Validation status Scenario Status Windows 11 / Docker Desktop, Linux containers VALIDATED Live DSServer MCP sessions (navigation, click, page queries, proxy injection, shutdown, container reuse); 2–10 container stress test, 11th-session rejection, 4 replacements Windows 11 / Docker Desktop, Windows containers VALIDATED Live Apple flow, 10-container stress, 4 same-container Chrome PID replacements, 20-second exhaustion retry macOS Apple Silicon / Docker Desktop, Linux containers (amd64 emulation) VALIDATED Sep 29, 2026, Chrome 154.0.8037.57: image load/inspect, standalone supervisor checks, pool startup, proxy probe, MCP discovery, navigation, stop and reuse; 10 concurrent sessions on 10 sites; replacement batches at 10–12 concurrency; 30 starts/navigations and 32 stops. Not a completed 100-session soak. Windows Server 2025, Windows containers RECIPE ONLY Not yet build/live tested Native Linux, Docker Engine RECIPE ONLY Not yet build/live tested Broader action parity / production rollout PENDING The macOS concurrency run exposed a race between explicit stop and the browser watcher: both removed the same JVM shutdown hook, throwing a NullPointerException and leaving stale session inventory. The lifecycle code now claims and clears the hook atomically; the post-fix run was clean. Evidence: documentation/BrowserManager/evidence/containerized-chrome-apple-2026-09-23/ and containerized-chrome-windows-2026-09-24/. Step 1 — Pick the recipe for your node DSServer host Docker engine mode Dockerfile global.json example Docker host key Guide Windows 11 Linux containers Dockerfile.windows-linux config/windows11-dual.json dockerHosts.desktop guides/windows-11.md Windows 11 Windows containers Dockerfile.windows-windows config/windows11-dual.json dockerHosts.desktop guides/windows-11.md Windows Server 2025 Windows containers Dockerfile.windows-windows config/windows-server-2025-windows.json dockerHosts.server guides/windows-server-2025.md macOS Linux containers Dockerfile.macos-linux config/macos-linux.json dockerHosts.desktop guides/macos.md Linux Linux containers Dockerfile.linux-linux config/linux-linux.json dockerHosts.engine guides/linux.md The named Dockerfiles make each host/engine combination explicit. Dockerfile (Linux) and Dockerfile.windows remain canonical; the three Linux recipes produce the same appvance/ds-chrome:local image, and Dockerfile.windows-windows mirrors Dockerfile.windows. If you change a canonical file, keep the named copies aligned. Installed AIQ package vs. source checkout Source checkout (development): build from the repository root with DSServer/container-browser as the build context, and edit DSServer/config/global.json. Installed, released AIQ package: do not rebuild DSServer, replace its JAR, run Maven, or mix container-browser files from another AIQ version. Use the container-browser bundle shipped with the installed release (Dockerfiles, browser-supervisor.py, config/*.json, and helper scripts such as verify-macos.sh). Edit /config/global.json and restart with the package's normal launcher. If the bundle wasn't included, get it from the same release's artifacts, not an arbitrary checkout. Step 2 — Common configuration (global.json) Each example file in config/ contains a complete root-level containerBrowsers object. Copy only that object into global.json, replacing any previous containerBrowsers object and keeping every other property. If the installed configuration already has the object, update it in place. Required edits before starting DSServer: ownerId — stable across restarts and unique to one DSServer deployment. On startup DSServer removes labeled containers belonging to this owner and pool before accepting leases; two running servers sharing an ownerId would delete each other's containers. dockerHosts..context — set to the output of docker context show (check docker context ls too). The adapter uses the configured context explicitly for every Docker call and does not inherit the shell's current context. Common values: default, desktop-linux (Docker Desktop on Mac). network.dsProxyHost — the address containers use to reach DSServer's HTTP proxy (port typically 9090): Docker Desktop (Windows 11, macOS): host.docker.internal Linux Docker Engine: the bridge network gateway (replaces REPLACE-WITH-DOCKER-BRIDGE-GATEWAY-IPV4) Windows Server 2025: the nat network gateway (replaces REPLACE-WITH-HOST-NAT-IPV4). Native Docker on Windows Server has no host.docker.internal. Pool size ( minSize / maxSize) — examples start 2 warm containers and allow up to 20 (Linux) or 10 (Windows). The maximum is a ceiling, not a recommendation; size it for host memory and CPU. Configuration reference Setting Notes enabled false (or omitting containerBrowsers) disables the feature explicitly. ownerId Stable, unique per deployment. dockerHosts..context or explicit endpoint Choose one. An explicit TCP endpoint requires tls with CA, client certificate, and key paths and is rejected without them. apiVersion "auto" uses normal Docker CLI/server negotiation. A numeric value sets DOCKER_API_VERSION only for commands this adapter launches. network.dsProxyHost Container-reachable address of DSServer's HTTP proxy (see above). runtime.platform Linux pools: keep linux/amd64 — the image uses Google's amd64 Chrome package. Do not simply change it on arm64 hosts. runtime.isolation (Windows pool) process by default; change to hyperv if the host needs Hyper-V isolation (change the image build flag too). minSize / maxSize Warm containers / ceiling. Image / allowUnpinnedImage The mutable :local tag with allowUnpinnedImage: true is development only. Production must pin an approved image digest. acquireRetryIntervalMs / acquireRetryCount Example 5000 / 4 → a session start waits up to 20 s for a free lease. A released lease wakes waiters immediately. On deadline, startup fails with POOL_EXHAUSTED and no session is retained. acquireTimeoutMs Legacy, still supported, cannot be combined with the retry settings above. cdpAdvertisedHost Linux pool: host DSServer uses to reach the published CDP port; must be reachable from DSServer. cdpContainerPort Supervisor CDP relay port inside the container (9222). Chrome itself listens on the next port (9223), bound to container loopback. extensionList (profile option) Comma-separated IDs of extensions baked into the image under extensions//. Host extension directories are not copied at session time. Step 3 — Host-specific setup Linux host (Docker Engine, Linux containers) Assumes rootful Docker Engine on a Linux/amd64 host, the standard bridge network, and DSServer on the same host. Install Docker Engine per the official instructions for your distribution and start the daemon. Give the DSServer service account access to the daemon (socket/group access is root-equivalent — restrict to trusted operators). Confirm: docker info --format '{{.OSType}}'     # must print: linux docker context show Build from the repository root: docker build --platform linux/amd64 -f DSServer/container-browser/Dockerfile.linux-linux -t appvance/ds-chrome:local DSServer/container-browser Find the bridge gateway and subnet: docker network inspect bridge --format '{{(index .IPAM.Config 0).Gateway}}' docker network inspect bridge --format '{{(index .IPAM.Config 0).Subnet}}' Copy containerBrowsers from config/linux-linux.json; set ownerId, dockerHosts.engine.context, and replace REPLACE-WITH-DOCKER-BRIDGE-GATEWAY-IPV4 with the gateway. Make DSServer's HTTP proxy listen on an interface reachable through that gateway (typically 9090) and allow inbound traffic only from the bridge subnet. 127.0.0.1 inside a container is not the host. Start DSServer; verify one session navigates, close it, start a replacement. CDP is published only to host loopback, so DSServer must run on this host. arm64 Linux hosts: don't just change runtime.platform. Use a compatible, validated browser image or explicitly accept and test amd64 emulation. Windows 11 (Docker Desktop, Linux and/or Windows containers) Docker Desktop runs one engine mode at a time. config/windows11-dual.json defines both pools; DSServer registers only the one matching the active engine at startup, so switching modes requires a DSServer restart. Prepare Docker Desktop. Supported Windows 11 build with hardware virtualization enabled. Install Docker Desktop for all users — the per-user install does not support Windows containers. Windows container mode requires a Pro/Enterprise edition. For the ltsc2025 image with process isolation the host build must be compatible with Windows Server 2025 images (check Microsoft's version compatibility matrix). Build the Linux image. Select Linux containers in the Docker tray menu, confirm docker info --format '{{.OSType}}' prints linux, then: docker build --platform linux/amd64 -f DSServer/container-browser/Dockerfile.windows-linux -t appvance/ds-chrome:local DSServer/container-browser Build the Windows image. Switch to Windows containers, confirm the OS type prints windows, test host/image compatibility first, then build: docker run --rm --isolation=process mcr.microsoft.com/windows/server:ltsc2025 cmd /c ver docker build --isolation=process -f DSServer/container-browser/Dockerfile.windows-windows -t appvance/ds-chrome-windows:local DSServer/container-browser If process isolation isn't supported, review Hyper-V isolation requirements and change both the build flag and the Windows pool's runtime.isolation to hyperv. For build-only DNS problems add --build-arg BUILD_DNS=. Configure DSServer. Copy containerBrowsers from config/windows11-dual.json; set ownerId; set dockerHosts.desktop.context (change from default if needed). The DSServer process account must be able to run the Docker CLI — typically membership in docker-users, followed by sign-out/sign-in. Proxy. DSServer's proxy must be reachable at host.docker.internal:9090 from containers: listen on a container-reachable interface and add a Windows Firewall rule allowing only the Docker private subnet to that port. Start DSServer after selecting the engine mode. Verify one session, stop it, start a second to check reuse before increasing load. Lower minSize/ maxSize on a smaller workstation. Windows Server 2025 (native Docker, Windows containers) Use a Windows Server 2025 host. Docker Desktop is not supported on Windows Server; install a native Windows-container runtime (Docker CE/Moby). This recipe provides no Linux containers — those need a separate Linux Docker host and a reviewed remote-network design. Install updates; make sure the host can reach Microsoft Container Registry, Chrome for Testing downloads, and Python downloads during the image build. Reserve disk and memory for the Windows base image and the pool. Set up the container host per Microsoft's Windows Server container-host guide. In elevated PowerShell (review the script and your org policy first — it enables container features and may reboot): New-Item -ItemType Directory -Force C:\setup | Out-Null Invoke-WebRequest -UseBasicParsing https://raw.githubusercontent.com/microsoft/Windows-Containers/Main/helpful_tools/Install-DockerCE/install-docker-ce.ps1 -OutFile C:\setup\install-docker-ce.ps1 powershell.exe -ExecutionPolicy Bypass -File C:\setup\install-docker-ce.ps1 After reboot, verify: Get-Service docker docker context show docker info --format '{{.OSType}}'     # must print: windows docker run --rm --isolation=process mcr.microsoft.com/windows/server:ltsc2025 cmd /c ver Build from the repository root: docker build --isolation=process -f DSServer/container-browser/Dockerfile.windows-windows -t appvance/ds-chrome-windows:local DSServer/container-browser Copy containerBrowsers from config/windows-server-2025-windows.json; set ownerId and dockerHosts.server.context (if not default). Run DSServer under an account authorized for the Docker daemon with docker on PATH. Find the NAT gateway and subnet: (docker network inspect nat | ConvertFrom-Json)[0].IPAM.Config | Select-Object Subnet,Gateway Replace REPLACE-WITH-HOST-NAT-IPV4 in network.dsProxyHost with the Gateway. DSServer's proxy must listen on that interface, and Windows Firewall must allow the Subnet to the proxy port (typically 9090). Do not open it publicly. Start DSServer; verify one session, stop, start a replacement before growing the pool. CDP is only on each container's private NAT IPv4 and is never published. macOS (Docker Desktop, Linux containers) Docker Desktop runs Linux containers in a VM. On Apple Silicon the linux/amd64 image runs under emulation — slower and heavier than an x86-64 Linux host. This procedure targets an installed AIQ package with its release-matched container-browser bundle. Verify Docker Desktop. docker context show                                   # record it, commonly desktop-linux docker info --format '{{.OSType}}/{{.Architecture}}' # OS must be linux docker version uname -m                                             # arm64 on Apple Silicon (expected) On Apple Silicon, install Rosetta if arch -x86_64 /usr/bin/true fails ( softwareupdate --install-rosetta), then in Docker Desktop Settings → General select the Apple Virtualization framework and enable Use Rosetta for x86_64/amd64 emulation on Apple Silicon. Build and inspect. cd docker build --platform linux/amd64 -f Dockerfile.macos-linux -t appvance/ds-chrome:local . docker image inspect appvance/ds-chrome:local --format '{{.Os}}/{{.Architecture}}'   # must print linux/amd64 Smoke-test the image before involving DSServer: docker run -d --name ds-chrome-macos-smoke --platform linux/amd64 -p 127.0.0.1::9222 appvance/ds-chrome:local docker exec ds-chrome-macos-smoke python3 /opt/appvance/browser-supervisor.py status     # idle docker exec ds-chrome-macos-smoke python3 /opt/appvance/browser-supervisor.py manifest   # JSON: protocol, /usr/bin/google-chrome, Chrome version docker rm -f ds-chrome-macos-smoke Configure. Merge containerBrowsers from the bundle's config/macos-linux.json into /config/global.json. Set ownerId and dockerHosts.desktop.context; keep runtime.platform: linux/amd64 and dsProxyHost: host.docker.internal. On Apple Silicon start small (e.g. minSize: 2, maxSize: 10) and grow only after measuring memory, CPU, launch latency, and CDP responsiveness. Restart AIQ. Start Docker Desktop first, stop AIQ with the installation's normal procedure, then start it again (e.g. cd && ./startds.sh). Do not run Maven or a replacement JAR. Verify using Step 4, including verify-macos.sh. Step 4 — Verify Startup logs In OutputLogs/ds3output0.txt (installed package), confirm Container pool engine ready for the matching pool, Container pool warmed for the warm containers, and that the ChromeContainerized profile is loaded. A configured pool for the other engine OS being hidden is expected (e.g. the Windows pool on a Linux engine). Proxy and pool connectivity curl -i --max-time 5 http://127.0.0.1:9090/           # any HTTP response (even 404) = proxy listening docker ps --filter 'label=appvance.ds.container-pool=chrome-linux' docker exec python3 /opt/appvance/browser-supervisor.py status         # warm containers: idle docker exec python3 /opt/appvance/browser-supervisor.py probe-proxy host.docker.internal 9090   # must print: reachable On Linux / Windows Server use your dsProxyHost (bridge or NAT gateway) instead of host.docker.internal. The commands use Linux image paths; adjust for the Windows image. Lease, cleanup and reuse Select ChromeContainerized, start a session, navigate to a simple page. The leased container's status must start with running:. Stop the session; the container returns to idle. Start a replacement session and navigate again — this proves cleanup and reuse, not just first launch. For concurrency testing, keep one or two spare pool slots and replace sessions in small batches (start replacements first, then stop the old ones) to avoid pool exhaustion. Use bounded navigation retries for external sites and record session readiness separately from page success. macOS packaged verifier (verify-macos.sh) Shipped in the release bundle; it does not change AIQ configuration. It creates one disposable container with DSServer's settings, checks both network directions, and removes it on exit. cd chmod +x verify-macos.sh ./verify-macos.sh --aiq-install                         # infrastructure checks ./verify-macos.sh --aiq-install --observe-session 180   # start ChromeContainerized from the UI when prompted ./verify-macos.sh --aiq-install --mcp                   # only if loopback MCP is already enabled AIQ_MCP_TOKEN='' ./verify-macos.sh --aiq-install --mcp   # bearer token kept off the command line Observed-session checks walk the full route: container → host.docker.internal:9090; Chrome launched with --proxy-server; Chrome CDP on 9223 inside the container; supervisor relay on 9222; 9222 published to a dynamic host-loopback port; host reaches /json/version through it. The script never enables MCP or edits mcp.json. Troubleshooting Session closes unexpectedly Closes immediately → usually a proxy or Chrome-start failure. Closes after ~130 seconds → CDP route problem: DSServer closes a browser when its native connector can't attach within that window. First verifier failure → likely fault First failure Likely fault DSServer proxy is not accepting host connections Proxy disabled, not listening, or on a different port. container cannot reach DSServer proxy Docker can't route dsProxyHost to the proxy, or a firewall is blocking it. Chrome CDP does not respond ... 9223 Chrome failed or exited inside the image. container CDP relay does not respond ... 9222 Supervisor/relay missing, incompatible, or failed. Docker did not publish ... 9222 Pool container created without the dynamic loopback mapping, or AIQ is using a different Docker context. host cannot reach Chrome CDP through the published port Docker Desktop host-loopback forwarding failed or was blocked. If every route check passes but sessions still close, compare the installed DSServer and container-browser bundle versions and inspect the lifecycle errors from OutputLogs. Send support the full verifier output starting at the first FAIL line. GPU process launch failed: error_code=1002 (macOS) Emitted after Chrome starts when a GPU child-process launch fails. It is not a DSServer proxy error. On Apple Silicon, first check the amd64 emulation path: cd ./diagnose-macos-gpu.sh --aiq-install > macos-gpu-diagnostic.txt 2>&1 The redacted report records host, Docker Desktop version, virtualization backend, Rosetta status, context, engine resources, image identity, Chrome version, and a three-mode Chrome launch matrix. Attach it with the verify-macos.sh output. rosetta_host_translation and docker_vmm_rosetta should both be true; otherwise fix Rosetta (macOS step 1), restart Docker Desktop, then restart AIQ so pool containers are recreated. If current fails but no-software-rasterizer succeeds, the failure is in Chrome's graphics path. As a temporary diagnostic only, add the options below to the ChromeContainerized profile, restart AIQ, and retry one session. This disables WebGL/software GPU rendering and can change application behaviour — not a production default. If every mode shows qemu, SIGTRAP, zygote, or network-service crashes, fix Docker Desktop's amd64 emulation, not AIQ proxy settings. {"name": "disable-gpu", "value": "true"}, {"name": "experimentalDS3", "value": "--disable-software-rasterizer"} Reference working Apple Silicon host (comparison values, not minimums) Other common issues Symptom Likely cause / fix ChromeContainerized profile missing Docker unreachable at startup, wrong context, or pool OS doesn't match engine mode. Check docker context show, fix config, restart DSServer. POOL_EXHAUSTED on session start All leases busy for the full retry window (~20 s default). Increase pool size (if memory allows) or retry settings; keep spare slots during batch replacement. Windows build fails downloading Chrome/Python --build-arg BUILD_DNS=. Windows container won't start with process isolation Host build incompatible with ltsc2025 — check Microsoft's matrix, or switch build flag and runtime.isolation to hyperv. Windows containers unavailable on Windows 11 Docker Desktop installed per-user, or Home edition. Reinstall for all users on Pro/Enterprise. DSServer can't run Docker on Windows 11 Service account not in docker-users (sign out/in after adding). Config rejected for TCP endpoint tls CA/cert/key paths missing. Config rejected for acquire settings acquireTimeoutMs combined with the retry settings — use one style. Containers disappear when another server starts Two DSServer deployments share an ownerId. Production checklist (not yet complete) Pin an approved image digest; set allowUnpinnedImage to false Size the pool for host memory and CPU Unique, stable ownerId per deployment Docker daemon access restricted to the DSServer account and trusted operators Proxy firewall rules limited to the Docker private subnet; CDP never public; mTLS and a private CDP route for any remote Docker host Release-matched container-browser bundle for installed packages Acceptance checklist in the implementation progress log complete References DSServer/container-browser/README.md and guides/ ( linux.md, macos.md, windows-11.md, windows-server-2025.md) Config examples: DSServer/container-browser/config/ Implementation progress log: documentation/BrowserManager/design/containerized-browser-pool-implementation-progress.md Install Docker Engine Linux post-installation steps for Docker Engine Install Docker Desktop on Windows Install Docker Desktop on Mac Change your Docker Desktop settings Explore networking how-tos on Docker Desktop Prepare Windows operating system containers Windows container version compatibility Windows container networking