Skip to main content

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 <AIQ-install>/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:

  1. 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.
  2. dockerHosts.<name>.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).
  3. 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.
  4. 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.<name>.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/<id>/. 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.

  1. 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
  2. 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
  3. 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}}'
  4. Copy containerBrowsers from config/linux-linux.json; set ownerId, dockerHosts.engine.context, and replace REPLACE-WITH-DOCKER-BRIDGE-GATEWAY-IPV4 with the gateway.
  5. 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.
  6. 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.

  1. 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).
  2. 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
  3. 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=<working-DNS-IP>.
  4. 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.
  5. 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.
  6. 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.

  1. 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.
  2. 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
  3. 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
  4. 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
  5. 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.
  6. 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.
  7. 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.

  1. 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.
  2. 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
  3. 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
  4. Configure. Merge containerBrowsers from the bundle's config/macos-linux.json into <AIQ-install>/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.
  5. 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.
  6. 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 <container-id> python3 /opt/appvance/browser-supervisor.py status        # warm containers: idle
docker 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

  1. Select ChromeContainerized, start a session, navigate to a simple page. The leased container's status must start with running:.
  2. Stop the session; the container returns to idle.
  3. 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 enabled
AIQ_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

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 <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_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=<working-DNS-IP>.

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