Short answer: Express.js can start and monitor a Selenium job, but it does not record the browser stream. A Selenium WebDriver client connects to a remote Grid, the Grid runs the browser, and a Docker Selenium video-recorder container captures the display. Enable recording in the deployment that runs the browser, mount or upload the resulting file, and always call driver.quit() so recording can stop cleanly.
What runs where
A reliable setup has three separate responsibilities:
- Express.js (optional coordinator): accepts an API request, queues work, starts a job, and returns a result or artifact location.
- Node.js Selenium client: sends WebDriver commands with the
selenium-webdriverpackage. - Remote Selenium Grid and browser node: create the browser session and execute those commands. A Docker Selenium recorder captures the browser display alongside the node.
You can run the same WebDriver code from a test runner or a plain Node.js script; Express is not a recording requirement. The default Grid endpoint in Selenium’s quick start is http://localhost:4444. The JavaScript binding supports both usingServer() and the SELENIUM_REMOTE_URL environment variable (see the Selenium JavaScript API).
Choose a Docker Selenium topology first
Recorder containers and output paths differ by deployment. Identify the topology before copying configuration.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Fix the driver behind crashes, sound loss and screen glitches3Repair Windows errors before they cause bigger problems#1 Best Overall
Standalone container
A standalone browser container runs Grid, the browser and (in common Docker Selenium examples) a paired video container. The recorder watches the session and writes a file to a shared or host-mounted directory such as /videos.
Hub and node
The Hub routes commands to one or more browser nodes. Attach a recorder to each node according to the Docker Selenium compose example, and ensure every recorder has access to the same output volume. Use unique names when sessions can run in parallel.
Dynamic Grid
Dynamic Grid creates browser and recorder containers for each session. Its documented control is the session capability se:recordVideo. Assets are normally exposed through the host-mounted assets directory configured for that deployment. Do not assume standalone environment variables or paths apply unchanged to Dynamic Grid.
Docker Selenium’s current documentation also shows Rclone-based uploads to S3 and GCS-compatible destinations. Image tags and variable names change; pin mutually compatible versions and verify the current docker-selenium README before deploying. A README search result dated September 5, 2026 showed Selenium image tag 4.48.0-20260905 and video image selenium/video:ffmpeg-8.1-20260905; treat those as point-in-time examples, not permanent defaults.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Prerequisites and security
- Node.js 22 or later for the current Selenium JavaScript binding.
- A reachable Selenium Grid URL and a display-capable browser node. Docker Selenium documents that video recording for headless browsers is unsupported.
- Docker and a shared volume or an object-storage destination for recordings.
- Firewall rules restricting Grid port 4444 to trusted systems. Selenium’s Grid guide states: “Selenium Grid must be protected from external access using appropriate firewall permissions.” An exposed, unauthenticated Grid can allow access to internal sites, files and arbitrary binaries.
Keep S3/GCS credentials in Docker secrets or your deployment’s secret manager, never in an Express route or committed source file.
Connect Node.js to the remote Grid
Install the client in the project that will run the automation:
Rank #2
npm install selenium-webdriver express
The following script uses Chrome on a Grid at http://localhost:4444. Replace the URL with the private address of your Grid. The finally block is essential: the recorder listens for session closure and can then finalize (and, if configured, upload) the video.
import express from 'express';
import {Builder, By, Browser} from 'selenium-webdriver';
const app = express();
app.use(express.json());
const gridUrl = process.env.SELENIUM_REMOTE_URL || 'http://localhost:4444';
async function runBrowserJob(targetUrl) {
const driver = await new Builder()
.forBrowser(Browser.CHROME)
.usingServer(gridUrl)
.build();
try {
await driver.get(targetUrl);
await driver.manage().setTimeouts({implicit: 5000, pageLoad: 60000, script: 30000});
const title = await driver.getTitle();
const heading = await driver.findElement(By.css('h1')).getText().catch(() => '');
return {title, heading};
} finally {
await driver.quit();
}
}
app.post('/jobs', async (req, res) => {
const {url} = req.body;
if (typeof url !== 'string' || !/^https?:///i.test(url)) {
return res.status(400).json({error: 'url must be an http(s) URL'});
}
try {
const result = await runBrowserJob(url);
res.json({status: 'complete', result});
} catch (error) {
console.error(error);
res.status(502).json({status: 'failed', error: 'remote browser job failed'});
}
});
app.listen(process.env.PORT || 3000);
For a direct script, remove the Express route and call runBrowserJob(). The same client configuration works either way. Long jobs should normally be queued rather than held open in an HTTP request; return a job ID, enforce a request timeout, cap concurrency, and persist the final video path or object-storage key.
Recommended Free Tools
Enable recording in Docker Selenium
Recording is configured on the browser/Grid side, not by Express. Follow the compose file for your selected topology and pair each browser with its compatible recorder image. A typical arrangement has:
- A browser container connected to the Grid network.
- A video container that can observe session-created and session-closed events.
- A shared volume, for example a host directory mounted at
/videos, or an assets directory used by Dynamic Grid. - Unique or automatically generated file names for parallel sessions.
In Dynamic Grid, request recording with the documented capability:
const driver = await new Builder()
.forBrowser(Browser.CHROME)
.usingServer(process.env.SELENIUM_REMOTE_URL)
.setChromeOptions(/* your display-capable options */)
.withCapabilities({'se:recordVideo': true})
.build();
Use the capability only where your Dynamic Grid version supports it. Other topologies may start recording through their compose environment instead. A browser launched in headless mode will not produce a supported Docker Selenium recording.
Where the video file appears
Mounted file output
With a host-mounted volume, the recorder writes inside its container (commonly /videos) and Docker exposes that directory on the host. The exact filename convention is deployment-specific. Inspect the mounted directory after driver.quit(); do not read it before the recorder has observed session closure.
Rank #3
Object-storage upload
The recorder image includes Rclone and Docker Selenium documents S3 and GCS-backed configurations. Upload is useful in CI because artifacts survive container deletion and can be accessed by a controlled artifact service. Give the recorder only the bucket permissions it needs and keep credentials outside source control. Store the returned object key with your job record.
| Approach | Operational simplicity | Persistence | Access control | CI retrieval |
|---|---|---|---|---|
| Shared host volume | Lowest setup overhead | Lost if the host volume is removed | Host filesystem permissions | Collect as a workspace artifact |
| Rclone to S3/GCS | More deployment configuration | Persists beyond containers | Bucket IAM and scoped credentials | Download by object key or signed workflow |
Neither option is universally best. Use a volume for local debugging and a controlled object store when jobs are ephemeral or distributed.
Run a complete recording job
- Start the Grid, browser node and recorder with the documented compose or Dynamic Grid configuration for your pinned image versions.
- Confirm the Grid health endpoint is reachable from the Node.js process at the private Grid URL.
- Start Node.js with
SELENIUM_REMOTE_URL=http://grid-host:4444(or set the URL in code). - Open the target page, perform the actions you need, and wait for the page state that matters. Avoid arbitrary sleeps when an element or network condition can be awaited.
- Call
await driver.quit()in afinallyblock, including error paths. - Wait for the recorder to finalize, then read the mounted file or verify the object-storage upload.
- Associate the filename or object key with the Express job ID and apply a retention policy.
Performance, parallelism and reliability
CPU and capacity
Docker Selenium advises planning approximately one CPU for each video container and one CPU for each browser container. This is a project guideline, not a benchmark; measure your own pages, codecs and concurrency. Video encoding competes with page rendering, so reduce parallel sessions or allocate more CPU when recordings show dropped frames or browser timeouts.
Parallel sessions
Give every recorder a unique filename or use the automatic naming supported by the deployment. Sharing one fixed path can cause overwrites or incomplete artifacts. Keep browser, recorder and Grid image versions compatible.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →Express job design
Do not let an HTTP request depend on a multi-minute browser session without a timeout strategy. A production route should validate URLs, enqueue work, return 202 Accepted with an ID, and expose a status endpoint. On worker failure, call quit() if a driver exists, mark the job failed, and retain logs separately from the video.
Troubleshooting
No video file is created
Check that the recorder container is running, the browser is not headless, the output volume is mounted on both sides, and the session actually closed. Verify Dynamic Grid sessions include se:recordVideo where required.
Rank #4
Node.js cannot connect
Confirm the URL is reachable from the Node.js container or host, port 4444 is allowed on the private network, and the Grid is ready before creating a session. Set SELENIUM_REMOTE_URL to the Grid address, not the recorder address.
Recording stops abruptly or is empty
Look for browser or recorder CPU pressure, container restarts, and a missing shared display. Allocate capacity according to the project’s CPU guidance, then test one session before increasing concurrency.
Files overwrite each other
Replace a fixed filename with the recorder’s automatic naming or a session-specific name. This is especially important when multiple video containers write to one host directory.
Upload fails
Check bucket permissions, region and endpoint settings in the Rclone configuration, and verify that secrets are injected into the recorder container. A successful browser session does not guarantee a successful upload.
The route hangs after the browser exits
Ensure your promise resolves after driver.quit() and that the recorder finalization is handled by the deployment rather than awaited forever in Express. For longer workflows, move execution to a worker queue.
Or skip the browser setup
If what you need is a clean image or PDF of a page rather than a time-based browser recording, ScreenshotNeo provides a single HTTP request. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be disabled. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients. The Free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000. See the ScreenshotNeo API documentation for all options.
Free tools Windows power users keep installed
One-click scans. No signup required.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const body = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', body));
Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.
Best Value
Frequently asked questions
Is Express.js required to record Selenium video?
No. It is an optional API and job-coordination layer; a Node.js script or test runner can connect to the same remote Grid.
Can I record a browser running on another machine?
Yes. The WebDriver client sends commands to the Grid URL while the browser and recorder run on the remote deployment. Keep that Grid endpoint private and reachable from the client.
Does driver.quit() delete the recording?
No. It signals session closure so the recorder can finalize the file or begin its configured upload. The resulting location depends on your mounted volume or storage configuration.
Can Docker Selenium record headless Chrome?
The documented Docker Selenium setup does not support video recording for headless browsers; use a display-capable browser configuration.
The Bottom Line
Use Node.js Selenium to control the remote Grid, configure Docker Selenium’s recorder beside the browser, store the artifact on a mounted volume or through Rclone, and close every session explicitly. Express coordinates the work; it is not the recorder.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

