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 |
| Docker host key | Guide |
|---|---|---|---|---|---|
Windows 11 | Linux containers |
|
|
|
|
Windows 11 | Windows containers |
|
|
|
|
Windows Server 2025 | Windows containers |
|
|
|
|
macOS | Linux containers |
|
|
|
|
Linux | Linux containers |
|
|
|
|
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-browseras the build context, and editDSServer/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 asverify-macos.sh). Edit<AIQ-install>/config/global.jsonand 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 anownerIdwould delete each other's containers.dockerHosts.<name>.context— set to the output ofdocker context show(checkdocker context lstoo). 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
bridgenetwork gateway (replacesREPLACE-WITH-DOCKER-BRIDGE-GATEWAY-IPV4) - Windows Server 2025: the
natnetwork gateway (replacesREPLACE-WITH-HOST-NAT-IPV4). Native Docker on Windows Server has nohost.docker.internal.
- Docker Desktop (Windows 11, macOS):
- 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 |
|---|---|
|
|
| Stable, unique per deployment. |
| Choose one. An explicit TCP endpoint requires |
|
|
| Container-reachable address of DSServer's HTTP proxy (see above). |
| Linux pools: keep |
|
|
| Warm containers / ceiling. |
Image / | The mutable |
| Example |
| Legacy, still supported, cannot be combined with the retry settings above. |
| Linux pool: host DSServer uses to reach the published CDP port; must be reachable from DSServer. |
| Supervisor CDP relay port inside the container (9222). Chrome itself listens on the next port (9223), bound to container loopback. |
| Comma-separated IDs of extensions baked into the image under |
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: linuxdocker 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
containerBrowsersfromconfig/linux-linux.json; setownerId,dockerHosts.engine.context, and replaceREPLACE-WITH-DOCKER-BRIDGE-GATEWAY-IPV4with 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.1inside 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
ltsc2025image 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}}'printslinux, 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 verdocker 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'sruntime.isolationtohyperv. For build-only DNS problems add--build-arg BUILD_DNS=<working-DNS-IP>. - Configure DSServer. Copy
containerBrowsersfromconfig/windows11-dual.json; setownerId; setdockerHosts.desktop.context(change fromdefaultif needed). The DSServer process account must be able to run the Docker CLI — typically membership indocker-users, followed by sign-out/sign-in. - Proxy. DSServer's proxy must be reachable at
host.docker.internal:9090from 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/maxSizeon 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-NullInvoke-WebRequest -UseBasicParsing https://raw.githubusercontent.com/microsoft/Windows-Containers/Main/helpful_tools/Install-DockerCE/install-docker-ce.ps1 -OutFile C:\setup\install-docker-ce.ps1powershell.exe -ExecutionPolicy Bypass -File C:\setup\install-docker-ce.ps1 - After reboot, verify:
Get-Servicedocker
docker context showdocker info --format '{{.OSType}}'# must print: windowsdocker 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
containerBrowsersfromconfig/windows-server-2025-windows.json; setownerIdanddockerHosts.server.context(if notdefault). Run DSServer under an account authorized for the Docker daemon withdockeronPATH. - Find the NAT gateway and subnet:
(docker network inspect nat | ConvertFrom-Json)[0].IPAM.Config | Select-Object Subnet,Gateway
ReplaceREPLACE-WITH-HOST-NAT-IPV4innetwork.dsProxyHostwith 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-linuxdocker info --format '{{.OSType}}/{{.Architecture}}'# OS must be linuxdockerversionuname -m# arm64 on Apple Silicon (expected)
On Apple Silicon, install Rosetta ifarch -x86_64 /usr/bin/truefails (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 <container-browser-bundle>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:localdocker exec ds-chrome-macos-smoke python3 /opt/appvance/browser-supervisor.py status# idledocker exec ds-chrome-macos-smoke python3 /opt/appvance/browser-supervisor.py manifest# JSON: protocol, /usr/bin/google-chrome, Chrome versiondocker rm -f ds-chrome-macos-smoke - Configure. Merge
containerBrowsersfrom the bundle'sconfig/macos-linux.jsoninto<AIQ-install>/config/global.json. SetownerIdanddockerHosts.desktop.context; keepruntime.platform: linux/amd64anddsProxyHost: 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 <AIQ-install> && ./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 listeningdocker ps --filter 'label=appvance.ds.container-pool=chrome-linux'docker exec <container-id> python3 /opt/appvance/browser-supervisor.py status # warm containers: idledocker exec <container-id> 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'sstatusmust start withrunning:. - 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 <container-browser-bundle>chmod +x verify-macos.sh./verify-macos.sh --aiq-install <AIQ-install> # infrastructure checks./verify-macos.sh --aiq-install <AIQ-install> --observe-session 180 # start ChromeContainerized from the UI when prompted./verify-macos.sh --aiq-install <AIQ-install> --mcp # only if loopback MCP is already enabledAIQ_MCP_TOKEN='<token>' ./verify-macos.sh --aiq-install <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 |
|---|---|
| Proxy disabled, not listening, or on a different port. |
| Docker can't route |
| Chrome failed or exited inside the image. |
| Supervisor/relay missing, incompatible, or failed. |
| Pool container created without the dynamic loopback mapping, or AIQ is using a different Docker context. |
| 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 <container-browser-bundle>./diagnose-macos-gpu.sh --aiq-install <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_translationanddocker_vmm_rosettashould both betrue; otherwise fix Rosetta (macOS step 1), restart Docker Desktop, then restart AIQ so pool containers are recreated.- If
currentfails butno-software-rasterizersucceeds, the failure is in Chrome's graphics path. As a temporary diagnostic only, add the options below to theChromeContainerizedprofile, 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 |
|---|---|
| Docker unreachable at startup, wrong context, or pool OS doesn't match engine mode. Check |
| 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 |
|
Windows container won't start with process isolation | Host build incompatible with |
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 |
Config rejected for TCP endpoint |
|
Config rejected for acquire settings |
|
Containers disappear when another server starts | Two DSServer deployments share an |
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.mdandguides/(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