Skip to content

Commit f19e49b

Browse files
committed
feat(docker): opt-in GPU acceleration via CLOAKBROWSER_GPU_ACCEL (#189)
1 parent ad4d946 commit f19e49b

8 files changed

Lines changed: 197 additions & 9 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,7 @@ Access the original un-patched Playwright page at `page._original` if you need r
571571
| `CLOAKBROWSER_AUTO_UPDATE` | `true` | Set to `false` to disable background update checks |
572572
| `CLOAKBROWSER_SKIP_CHECKSUM` | `false` | Set to `true` to skip SHA-256 verification after download |
573573
| `CLOAKBROWSER_GEOIP_TIMEOUT_SECONDS` | `5` | Max seconds for GeoIP resolution before continuing without it |
574+
| `CLOAKBROWSER_GPU_ACCEL` || Set to `1` to opt in to Docker GPU acceleration flags |
574575

575576
## Fingerprint Management
576577

@@ -848,6 +849,12 @@ services:
848849
start_period: 10s
849850
```
850851
852+
### GPU acceleration in Docker
853+
854+
GPU acceleration is opt-in. Set `CLOAKBROWSER_GPU_ACCEL=1` to add Chromium's EGL, GPU rasterization, GPU blocklist bypass, and Linux Vaapi video decode flags. If you use NVIDIA GPUs, install the NVIDIA Container Toolkit on the Docker host and add a GPU device reservation to your Compose service.
855+
856+
See [`examples/docker-compose.gpu.yml`](examples/docker-compose.gpu.yml) for a complete Compose example. If you run CloakBrowser inside your own multi-container stack, mirror the environment variable and GPU reservation in that service.
857+
851858
**Per-connection fingerprint seeds** — run multiple browser identities from a single container. Each unique seed spawns a separate Chrome process with its own fingerprint:
852859
853860
```python

cloakbrowser/browser.py

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import logging
1818
import os
19+
import sys
1920
from typing import Any, Literal, TypedDict
2021
from urllib.parse import quote, unquote, urlparse, urlunparse
2122

@@ -64,6 +65,7 @@ def launch(
6465
humanize: bool = False,
6566
human_preset: HumanPreset = "default",
6667
human_config: HumanConfigOverrides | None = None,
68+
gpu_accel: bool = False,
6769
**kwargs: Any,
6870
) -> Any:
6971
"""Launch stealth Chromium browser. Returns a Playwright Browser object.
@@ -90,6 +92,8 @@ def launch(
9092
humanize: Enable human-like mouse, keyboard, scroll behavior (default False).
9193
human_preset: Humanize preset — 'default' or 'careful' (default 'default').
9294
human_config: Custom humanize config mapping to override preset values.
95+
gpu_accel: Enable Docker GPU acceleration flags (default False).
96+
Can also be enabled with CLOAKBROWSER_GPU_ACCEL=1.
9397
**kwargs: Passed directly to playwright.chromium.launch().
9498
9599
Returns:
@@ -112,7 +116,14 @@ def launch(
112116
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
113117
args = list(args or [])
114118
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
115-
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless)
119+
chrome_args = build_args(
120+
stealth_args,
121+
(args or []) + proxy_extra_args,
122+
timezone=timezone,
123+
locale=locale,
124+
headless=headless,
125+
gpu_accel=gpu_accel,
126+
)
116127

117128
logger.debug("Launching stealth Chromium (headless=%s, args=%d)", headless, len(chrome_args))
118129

@@ -159,6 +170,7 @@ async def launch_async( # noqa: C901
159170
humanize: bool = False,
160171
human_preset: HumanPreset = "default",
161172
human_config: HumanConfigOverrides | None = None,
173+
gpu_accel: bool = False,
162174
**kwargs: Any,
163175
) -> Any:
164176
"""Async version of launch(). Returns a Playwright Browser object.
@@ -175,6 +187,8 @@ async def launch_async( # noqa: C901
175187
humanize: Enable human-like mouse, keyboard, scroll behavior (default False).
176188
human_preset: Humanize preset — 'default' or 'careful' (default 'default').
177189
human_config: Custom humanize config mapping to override preset values.
190+
gpu_accel: Enable Docker GPU acceleration flags (default False).
191+
Can also be enabled with CLOAKBROWSER_GPU_ACCEL=1.
178192
**kwargs: Passed directly to playwright.chromium.launch().
179193
180194
Returns:
@@ -202,7 +216,14 @@ async def launch_async( # noqa: C901
202216
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
203217
args = list(args or [])
204218
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
205-
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless)
219+
chrome_args = build_args(
220+
stealth_args,
221+
(args or []) + proxy_extra_args,
222+
timezone=timezone,
223+
locale=locale,
224+
headless=headless,
225+
gpu_accel=gpu_accel,
226+
)
206227

207228
logger.debug("Launching stealth Chromium async (headless=%s, args=%d)", headless, len(chrome_args))
208229

@@ -253,6 +274,7 @@ def launch_persistent_context(
253274
humanize: bool = False,
254275
human_preset: HumanPreset = "default",
255276
human_config: HumanConfigOverrides | None = None,
277+
gpu_accel: bool = False,
256278
**kwargs: Any,
257279
) -> Any:
258280
"""Launch stealth browser with a persistent profile and return a BrowserContext.
@@ -282,6 +304,8 @@ def launch_persistent_context(
282304
humanize: Enable human-like mouse, keyboard, scroll behavior (default False).
283305
human_preset: Humanize preset — 'default' or 'careful' (default 'default').
284306
human_config: Custom humanize config mapping to override preset values.
307+
gpu_accel: Enable Docker GPU acceleration flags (default False).
308+
Can also be enabled with CLOAKBROWSER_GPU_ACCEL=1.
285309
**kwargs: Passed directly to playwright.chromium.launch_persistent_context().
286310
287311
Returns:
@@ -306,7 +330,14 @@ def launch_persistent_context(
306330
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
307331
args = list(args or [])
308332
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
309-
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless)
333+
chrome_args = build_args(
334+
stealth_args,
335+
(args or []) + proxy_extra_args,
336+
timezone=timezone,
337+
locale=locale,
338+
headless=headless,
339+
gpu_accel=gpu_accel,
340+
)
310341

311342
logger.debug(
312343
"Launching persistent stealth Chromium (headless=%s, user_data_dir=%s)",
@@ -377,6 +408,7 @@ async def launch_persistent_context_async(
377408
humanize: bool = False,
378409
human_preset: HumanPreset = "default",
379410
human_config: HumanConfigOverrides | None = None,
411+
gpu_accel: bool = False,
380412
**kwargs: Any,
381413
) -> Any:
382414
"""Async version of launch_persistent_context().
@@ -403,6 +435,8 @@ async def launch_persistent_context_async(
403435
humanize: Enable human-like mouse, keyboard, scroll behavior (default False).
404436
human_preset: Humanize preset — 'default' or 'careful' (default 'default').
405437
human_config: Custom humanize config mapping to override preset values.
438+
gpu_accel: Enable Docker GPU acceleration flags (default False).
439+
Can also be enabled with CLOAKBROWSER_GPU_ACCEL=1.
406440
**kwargs: Passed directly to playwright.chromium.launch_persistent_context().
407441
408442
Returns:
@@ -432,7 +466,14 @@ async def launch_persistent_context_async(
432466
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
433467
args = list(args or [])
434468
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
435-
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless)
469+
chrome_args = build_args(
470+
stealth_args,
471+
(args or []) + proxy_extra_args,
472+
timezone=timezone,
473+
locale=locale,
474+
headless=headless,
475+
gpu_accel=gpu_accel,
476+
)
436477

437478
logger.debug(
438479
"Launching persistent stealth Chromium async (headless=%s, user_data_dir=%s)",
@@ -957,6 +998,7 @@ def build_args(
957998
timezone: str | None = None,
958999
locale: str | None = None,
9591000
headless: bool = True,
1001+
gpu_accel: bool = False,
9601002
) -> list[str]:
9611003
"""Combine stealth args with user-provided args and locale flags.
9621004
@@ -979,6 +1021,13 @@ def build_args(
9791021
if not headless or _platform.system() == "Windows":
9801022
seen["--ignore-gpu-blocklist"] = "--ignore-gpu-blocklist"
9811023

1024+
if gpu_accel or os.environ.get("CLOAKBROWSER_GPU_ACCEL"):
1025+
seen["--use-gl"] = "--use-gl=egl"
1026+
seen["--enable-gpu-rasterization"] = "--enable-gpu-rasterization"
1027+
seen["--ignore-gpu-blocklist"] = "--ignore-gpu-blocklist"
1028+
if sys.platform.startswith("linux"):
1029+
seen["--enable-features"] = "--enable-features=VaapiVideoDecoder"
1030+
9821031
if extra_args:
9831032
for arg in extra_args:
9841033
key = arg.split("=", 1)[0]

examples/docker-compose.gpu.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
services:
2+
cloakbrowser:
3+
image: cloakhq/cloakbrowser
4+
command: cloakserve
5+
restart: unless-stopped
6+
environment:
7+
CLOAKBROWSER_GPU_ACCEL: "1"
8+
ports:
9+
- "127.0.0.1:9222:9222"
10+
healthcheck:
11+
test: ["CMD", "curl", "-f", "http://localhost:9222/json/version"]
12+
interval: 30s
13+
timeout: 5s
14+
retries: 3
15+
start_period: 10s
16+
deploy:
17+
resources:
18+
reservations:
19+
devices:
20+
- driver: nvidia
21+
count: all
22+
capabilities: [gpu]

js/src/args.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ export function buildArgs(options: LaunchOptions): string[] {
2929
if (options.headless === false || process.platform === "win32") {
3030
seen.set("--ignore-gpu-blocklist", "--ignore-gpu-blocklist");
3131
}
32+
if (options.gpuAccel || process.env.CLOAKBROWSER_GPU_ACCEL) {
33+
seen.set("--use-gl", "--use-gl=egl");
34+
seen.set("--enable-gpu-rasterization", "--enable-gpu-rasterization");
35+
seen.set("--ignore-gpu-blocklist", "--ignore-gpu-blocklist");
36+
if (process.platform === "linux") {
37+
seen.set("--enable-features", "--enable-features=VaapiVideoDecoder");
38+
}
39+
}
3240
if (options.args) {
3341
for (const arg of options.args) {
3442
const key = arg.split("=")[0];

js/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export interface LaunchOptions {
2525
locale?: string;
2626
/** Auto-detect timezone/locale from proxy IP (requires: npm install mmdb-lib). */
2727
geoip?: boolean;
28+
/** Enable Docker GPU acceleration flags. Can also be enabled with CLOAKBROWSER_GPU_ACCEL=1. */
29+
gpuAccel?: boolean;
2830
/** Raw options passed directly to playwright/puppeteer launch(). */
2931
launchOptions?: Record<string, unknown>;
3032
/** Enable human-like mouse, keyboard, and scroll behavior. */

js/tests/config.test.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect } from "vitest";
1+
import { afterEach, describe, it, expect } from "vitest";
22
import {
33
CHROMIUM_VERSION,
44
getArchiveExt,
@@ -11,6 +11,22 @@ import {
1111
} from "../src/config.js";
1212
import { _buildArgsForTest, resolveTimezone } from "../src/playwright.js";
1313

14+
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform")!;
15+
const originalGpuAccel = process.env.CLOAKBROWSER_GPU_ACCEL;
16+
17+
afterEach(() => {
18+
Object.defineProperty(process, "platform", originalPlatform);
19+
if (originalGpuAccel === undefined) {
20+
delete process.env.CLOAKBROWSER_GPU_ACCEL;
21+
} else {
22+
process.env.CLOAKBROWSER_GPU_ACCEL = originalGpuAccel;
23+
}
24+
});
25+
26+
function setPlatform(platform: NodeJS.Platform): void {
27+
Object.defineProperty(process, "platform", { ...originalPlatform, value: platform });
28+
}
29+
1430
describe("config", () => {
1531
it("CHROMIUM_VERSION matches expected format", () => {
1632
expect(CHROMIUM_VERSION).toMatch(/^\d+\.\d+\.\d+\.\d+(\.\d+)?$/);
@@ -183,6 +199,45 @@ describe("buildArgs deduplication", () => {
183199
});
184200
});
185201

202+
describe("buildArgs GPU acceleration", () => {
203+
it("leaves GPU acceleration flags off by default", () => {
204+
delete process.env.CLOAKBROWSER_GPU_ACCEL;
205+
const args = _buildArgsForTest({});
206+
expect(args).not.toContain("--use-gl=egl");
207+
expect(args).not.toContain("--enable-gpu-rasterization");
208+
expect(args).not.toContain("--enable-features=VaapiVideoDecoder");
209+
});
210+
211+
it("CLOAKBROWSER_GPU_ACCEL adds Linux GPU flags", () => {
212+
process.env.CLOAKBROWSER_GPU_ACCEL = "1";
213+
setPlatform("linux");
214+
const args = _buildArgsForTest({});
215+
expect(args).toContain("--use-gl=egl");
216+
expect(args).toContain("--enable-gpu-rasterization");
217+
expect(args).toContain("--ignore-gpu-blocklist");
218+
expect(args).toContain("--enable-features=VaapiVideoDecoder");
219+
});
220+
221+
it("skips VaapiVideoDecoder off Linux", () => {
222+
process.env.CLOAKBROWSER_GPU_ACCEL = "1";
223+
setPlatform("darwin");
224+
const args = _buildArgsForTest({});
225+
expect(args).toContain("--use-gl=egl");
226+
expect(args).toContain("--enable-gpu-rasterization");
227+
expect(args).not.toContain("--enable-features=VaapiVideoDecoder");
228+
});
229+
230+
it("gpuAccel option adds GPU flags without the environment variable", () => {
231+
delete process.env.CLOAKBROWSER_GPU_ACCEL;
232+
setPlatform("linux");
233+
const args = _buildArgsForTest({ gpuAccel: true });
234+
expect(args).toContain("--use-gl=egl");
235+
expect(args).toContain("--enable-gpu-rasterization");
236+
expect(args).toContain("--ignore-gpu-blocklist");
237+
expect(args).toContain("--enable-features=VaapiVideoDecoder");
238+
});
239+
});
240+
186241
describe("buildArgs webrtc IP", () => {
187242
it("passes --fingerprint-webrtc-ip from args", () => {
188243
const args = _buildArgsForTest({ args: ["--fingerprint-webrtc-ip=1.2.3.4"] });

tests/conftest.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
"""Shared test fixtures."""
22

3-
import os
4-
53
import pytest
64

75

86
@pytest.fixture(autouse=True)
9-
def _clean_backend_env(monkeypatch):
10-
"""Ensure CLOAKBROWSER_BACKEND doesn't leak into tests from the host environment."""
7+
def _clean_env(monkeypatch):
8+
"""Ensure host-level wrapper env vars don't leak into tests."""
119
monkeypatch.delenv("CLOAKBROWSER_BACKEND", raising=False)
10+
monkeypatch.delenv("CLOAKBROWSER_GPU_ACCEL", raising=False)

tests/test_build_args.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Unit tests for build_args timezone/locale injection and timezone alias."""
22

3+
import sys
4+
35
from cloakbrowser.browser import build_args, _resolve_timezone
46

57

@@ -151,6 +153,50 @@ def test_non_value_flags_preserved():
151153
assert "--no-sandbox" in args
152154

153155

156+
# --- GPU acceleration ---
157+
158+
159+
def test_gpu_accel_default_off(monkeypatch):
160+
"""GPU acceleration flags should stay off unless explicitly enabled."""
161+
monkeypatch.delenv("CLOAKBROWSER_GPU_ACCEL", raising=False)
162+
args = build_args(stealth_args=True, extra_args=None)
163+
assert "--use-gl=egl" not in args
164+
assert "--enable-gpu-rasterization" not in args
165+
assert "--enable-features=VaapiVideoDecoder" not in args
166+
167+
168+
def test_gpu_accel_env_adds_linux_flags(monkeypatch):
169+
"""CLOAKBROWSER_GPU_ACCEL enables GPU flags, including Vaapi on Linux."""
170+
monkeypatch.setenv("CLOAKBROWSER_GPU_ACCEL", "1")
171+
monkeypatch.setattr(sys, "platform", "linux")
172+
args = build_args(stealth_args=True, extra_args=None)
173+
assert "--use-gl=egl" in args
174+
assert "--enable-gpu-rasterization" in args
175+
assert "--ignore-gpu-blocklist" in args
176+
assert "--enable-features=VaapiVideoDecoder" in args
177+
178+
179+
def test_gpu_accel_skips_vaapi_off_linux(monkeypatch):
180+
"""VaapiVideoDecoder is Linux-only."""
181+
monkeypatch.setenv("CLOAKBROWSER_GPU_ACCEL", "1")
182+
monkeypatch.setattr(sys, "platform", "darwin")
183+
args = build_args(stealth_args=True, extra_args=None)
184+
assert "--use-gl=egl" in args
185+
assert "--enable-gpu-rasterization" in args
186+
assert "--enable-features=VaapiVideoDecoder" not in args
187+
188+
189+
def test_gpu_accel_kwarg_adds_flags(monkeypatch):
190+
"""gpu_accel=True enables GPU flags without the environment variable."""
191+
monkeypatch.delenv("CLOAKBROWSER_GPU_ACCEL", raising=False)
192+
monkeypatch.setattr(sys, "platform", "linux")
193+
args = build_args(stealth_args=True, extra_args=None, gpu_accel=True)
194+
assert "--use-gl=egl" in args
195+
assert "--enable-gpu-rasterization" in args
196+
assert "--ignore-gpu-blocklist" in args
197+
assert "--enable-features=VaapiVideoDecoder" in args
198+
199+
154200
def test_override_logs_debug(caplog):
155201
"""Should log debug message when an override happens."""
156202
import logging

0 commit comments

Comments
 (0)