Skip to content

Private Runner

Private Runner lets VisualRunner monitor environments our capture servers can't reach on their own — a VPN-only staging site, an internal admin tool, a service bound to localhost on your CI box. You run a small Docker container inside your own network; it pulls capture jobs from VisualRunner over an outbound-only connection and sends back the screenshots.

Private Runner is an Enterprise feature, enabled per workspace. It isn't self-serve — contact us (support@visualrunner.com or your account contact) to have it switched on.

How it works

  • The runner makes only outbound HTTPS connections to VisualRunner. You don't open any inbound port or poke a hole in your firewall.
  • It runs inside the network that already has access, so it never needs your VPN credentials. If an environment also sits behind a login, the cookie / header / basic-auth credentials you configured on it are sent to your runner (over its own outbound connection) and applied in the browser for that capture — your encryption key never leaves VisualRunner.
  • It only ever does one thing: run the screenshot-capture jobs you've already configured (pages × environments × languages × viewports). It can't be used for anything else.
  • It uses the same browser engine as our own capture servers, pinned to the same version, so a screenshot taken by your runner matches one taken by us.

What it covers

A Private Runner is set per environment (Environments tab → Capture delivery), and it only changes where the screenshot capture for that environment runs:

Runs on your runner Still runs on VisualRunner's platform (egress IP 157.230.2.191)
Manual captures for that environment Uptime checks — there is no runner path for uptime at all
Scheduled screenshot Monitors for that environment (each run's capture + all its QA checks) Discovery scans
The environment connectivity test on the Environments tab

A monitor on a runner-delivered environment is created and enabled normally — VisualRunner skips the platform-side connectivity check that would otherwise fail for a host our servers can't reach, because your runner captures it instead. As a safety net, if a runner-delivered monitor's scheduled runs capture nothing three times in a row (runner offline, or it can't reach the site), VisualRunner auto-pauses that monitor with a message telling you to check the runner. Fix the runner and re-enable the monitor.

So a VPN-only staging site can be captured and screenshot-monitored through a runner with nothing to allowlist — but uptime monitoring on that same site still needs VisualRunner's egress IP allowlisted to reach it directly.

Setup

  1. We enable the feature on your workspace.
  2. In Workspace settings → Runners, click Register a runner. You'll get a token (vr_runner_…) shown once — copy it now; you can't see it again (rotate to get a new one).
  3. Run the container on a host inside your network that can reach both https://visualrunner.com/api and the internal sites you want captured:

bash / zsh (Linux, macOS, WSL):

docker pull visualrunner/runner:latest

docker run -d --restart unless-stopped --name visualrunner-runner \
  -e PLATFORM_URL=https://visualrunner.com/api \
  -e RUNNER_TOKEN=vr_runner_xxxxxxxxxxxxxxxx \
  visualrunner/runner:latest

Single line:

docker run -d --restart unless-stopped --name visualrunner-runner -e PLATFORM_URL=https://visualrunner.com/api -e RUNNER_TOKEN=vr_runner_xxxxxxxxxxxxxxxx visualrunner/runner:latest

PowerShell (Windows):

docker pull visualrunner/runner:latest

docker run -d --restart unless-stopped --name visualrunner-runner `
  -e PLATFORM_URL=https://visualrunner.com/api `
  -e RUNNER_TOKEN=vr_runner_xxxxxxxxxxxxxxxx `
  visualrunner/runner:latest

Single line:

docker run -d --restart unless-stopped --name visualrunner-runner -e PLATFORM_URL=https://visualrunner.com/api -e RUNNER_TOKEN=vr_runner_xxxxxxxxxxxxxxxx visualrunner/runner:latest

The container is outbound-only — it opens no ports and needs no inbound firewall rule. To update it later: docker pull visualrunner/runner:latest then recreate the container. Pin to a fixed version (visualrunner/runner:v0.1.0) if you'd rather update on your own schedule — we'll tell you the current version.

Optional environment variables: RUNNER_CONCURRENCY (1–16, default 1 — see Host requirements & concurrency), RUNNER_NAV_TIMEOUT_MS (default 30000), RUNNER_HEARTBEAT_MS (default 60000).

If your team can't pull from Docker Hub, ask us for an image tarball (docker load < visualrunner-runner.tar.gz) instead.

  1. In your project's Environments tab, set the environment's Capture delivery to your runner instead of "Platform worker". Monitors and manual captures for that environment now run on your runner.

Once the container is running and has sent its first heartbeat, the runner shows as online in Workspace settings.

Host requirements & concurrency

The runner image is a headless-Chromium (Playwright) container. Each concurrent capture slot is a fresh browser context that spikes one CPU core while a page renders and holds ~2 GB of RAM. Size the host to the number of captures you want to run at once — plus ~5 GB free disk (the image is ~2 GB, plus working space) — and to whether the vCPUs are dedicated or shared.

Dedicated CPU

AWS c7/c6, DigitalOcean CPU-Optimized, GCP c2, Hetzner CCX — you get the whole core, so budget ~1 vCPU per slot plus one for the OS and the agent.

RUNNER_CONCURRENCY Suggested vCPU Suggested RAM Captures at once ~100 captures
1 (default) 2 vCPU 4 GB 1 at a time ~50 min
2 3 vCPU 6 GB 2 at once ~25 min
4 5 vCPU 10 GB 4 at once ~13 min
8 9 vCPU 18 GB 8 at once ~7 min
16 (max) 17 vCPU 34 GB 16 at once ~3 min

Shared / burstable CPU

AWS t3/t4g, DigitalOcean Basic, GCP e2, Hetzner CX/CPX — each vCPU is a scheduled slice of a core with a burst-credit budget. Roughly double the vCPU count, and treat the times as best-case: a long backlog burns through the credits and then throttles.

RUNNER_CONCURRENCY Suggested vCPU Suggested RAM Captures at once ~100 captures
1 (default) 2 vCPU 4 GB 1 at a time ~50 min
2 4 vCPU 6 GB 2 at once ~25 min
4 8 vCPU 10 GB 4 at once ~13 min
8 16 vCPU 18 GB 8 at once ~7 min
16 (max) 32 vCPU 34 GB 16 at once ~3 min

RAM is the same in both tables — it tracks the number of slots, not the core type. Heavy pages (large images, video, huge DOMs) need 3–4 GB per slot instead of 2.

One job vs. many

One slot runs captures one after another. RUNNER_CONCURRENCY = N runs N at once, so a backlog clears roughly N× faster. A single capture is typically 10–40 seconds of wall-clock time depending on page weight and RUNNER_NAV_TIMEOUT_MS (default 30 s). The ~100 captures column is that backlog at the ~30 s midpoint, run RUNNER_CONCURRENCY-wide (100 / N × 30 s) — planning estimates, not guarantees.

Running more than one job

Concurrency is entirely under your control. To go from one job to several, add -e RUNNER_CONCURRENCY=<n> (1–16) to the docker run command and restart the container — the agent then runs <n> independent poll → capture → upload workers. Nothing has to be requested, granted, or reconfigured on the VisualRunner side, and concurrency is not metered or billed: the Private Runner feature flag only governs whether runner delivery is available at all. The runner reports its maximum concurrency to VisualRunner on every heartbeat, and the platform never hands it more work in parallel than that.

Scaling past one host

The per-runner cap is 16. Beyond that, run a second runner container on another host and point different environments at different runners (Environments tab → Capture delivery). One environment is served by exactly one runner today.

If the host is undersized you'll see capture timeouts, Chromium processes killed for memory, or slow lease turnaround — lower RUNNER_CONCURRENCY or add CPU/RAM.

What's supported today

  • Authenticated environments work. Cookie, header, and HTTP basic-auth credentials you set on an environment are delivered to your runner over its outbound-only connection and applied in the browser, so VPN-only sites that also sit behind a login are captured normally. Your runner never receives your encryption key — VisualRunner decrypts the credentials just before handing out each job.
  • Runner captures count toward your plan's automated-capture allowance and storage, exactly like captures run on our servers — a private runner is a different place the capture runs, not a separate quota.
  • Full QA checks. Runner captures produce the screenshot, visual comparison, extracted text for search, and the same text-length, accessibility, console-error, brand, and missing-translation checks as server-run captures.
  • If a runner goes offline mid-job, VisualRunner automatically retries the capture on your next online runner (up to 3 attempts) before marking it failed. A monitor whose runner has gone away pauses itself with a clear reason rather than failing every check.

Managing runners

  • Rotate token — issues a new token and invalidates the old one immediately. Update the container's RUNNER_TOKEN and restart it.
  • Revoke — permanently disables the runner. Any job it was holding is released and retried elsewhere.
  • A runner is a workspace resource — it keeps working if the person who registered it leaves the workspace.

See also