If you’re developing a Rails app inside WSL 2 and want to run system specs with Capybara and Selenium, you’ll quickly hit a problem: WSL 2 runs on a separate virtual network from your Windows host. Your test suite runs in Linux, but Chrome lives on the Windows side. The two need to talk to each other across that network boundary.
In this post, I’ll walk you through a clean setup that wires everything together.
The Architecture
Here’s what’s happening under the hood:
- Capybara starts a Puma test server inside WSL and drives the browser via Selenium.
- ChromeDriver runs on Windows and controls a local Chrome instance.
- Capybara connects to ChromeDriver over the WSL ↔ Windows virtual network.
- Chrome connects back to the Puma test server inside WSL to load pages.
Two IPs matter here:
- Windows host IP - so WSL can reach ChromeDriver. This is the default gateway from
ip route show default. - WSL IP - so Chrome can reach the Puma test server. Available via
hostname -I.
Setting Up ChromeDriver on Windows
We need ChromeDriver installed and running on the Windows side. The following PowerShell script handles everything: it installs ChromeDriver via winget if it’s not already present, resolves the WSL IP, and launches ChromeDriver with access restricted to that IP only.
Save this as chromedriver.ps1:
param(
[string]$Version
)
# Resolve full version from a major version prefix (e.g. "145" -> "145.0.7049.85")
if ($Version) {
$match = winget show --id Chromium.ChromeDriver --versions | Select-String "^\s*($Version\.\S+)" | Select-Object -First 1
if (-not $match) {
Write-Host "ERROR: No ChromeDriver version found matching '$Version'" -ForegroundColor Red
exit 1
}
$Version = $match.Matches[0].Groups[1].Value
Write-Host "Resolved version: $Version"
}
# Uninstall if the installed version is newer than the requested one
if ($Version) {
$installed = winget list --id Chromium.ChromeDriver | Select-String "Chromium\.ChromeDriver\s+(\S+)" | ForEach-Object { $_.Matches[0].Groups[1].Value }
if ($installed -and [version]$installed -gt [version]$Version) {
Write-Host "Downgrading from $installed to $Version..."
winget uninstall --id Chromium.ChromeDriver
}
}
# Install ChromeDriver
winget install --id Chromium.ChromeDriver --version $Version --accept-source-agreements --accept-package-agreements
# Get the WSL IP to restrict access
$wslIp = (wsl hostname -I).Trim().Split()[0]
if (-not $wslIp) {
Write-Host "ERROR: Could not determine WSL IP. Is WSL running?" -ForegroundColor Red
exit 1
}
Write-Host "Starting ChromeDriver on port 9515 (allowed IP: $wslIp)"
chromedriver.exe --port=9515 --allowed-ips="$wslIp"
Run it to install the latest version and start ChromeDriver:
.\chromedriver.ps1
Or pin a specific version to match your Chrome (a major version prefix is enough):
.\chromedriver.ps1 -Version 145
Note: If this is your first time running PowerShell scripts, you may need to allow script execution first:
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
ChromeDriver version must match your installed Chrome version. You can check your Chrome version at chrome://settings/help. If Chrome auto-updates ahead of ChromeDriver (or vice versa), pass -Version to pin the matching release.
The --allowed-ips flag is important. Without it (or with an empty value), any device on your local network could connect to ChromeDriver. By restricting it to the WSL IP, only your WSL instance can make requests.
Configuring Capybara
On the Rails side, create a Capybara configuration file (e.g. test/test_helpers/capybara.rb or spec/support/capybara.rb) and make sure it’s loaded by your test helper.
WSL = File.read("/proc/version").include?("microsoft") rescue false
HEADLESS = ENV.fetch("HEADLESS", "1").in?(%w[1 y yes true t])
Capybara.app_host = "http://#{`hostname -I`.strip.split.first}" if WSL
Capybara.server_host = "0.0.0.0"
chrome_options = -> {
options = Selenium::WebDriver::Chrome::Options.new
options.add_argument("--headless=new") if HEADLESS
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options
}
Capybara.register_driver :chrome do |app|
Capybara::Selenium::Driver.new(app, browser: :chrome, options: chrome_options.call)
end
Capybara.register_driver :remote_chrome do |app|
host = `ip route show default`.match(/via\s+(\S+)/)&.captures&.first
Capybara::Selenium::Driver.new(app, browser: :remote, url: "http://#{host}:9515", capabilities: chrome_options.call)
end
A few things to note:
- WSL detection checks
/proc/versionfor themicrosoftstring, so the same config works on both WSL and native Linux/macOS. server_host = "0.0.0.0"makes the test server listen on all interfaces, so Chrome on Windows can reach it.app_hostis set to the WSL IP so Capybara tells Chrome where to find the test server. Without this, Capybara would use127.0.0.1, which inside Chrome on Windows points to Windows itself - not the WSL instance running Puma. Capybara appends the port automatically.- Two drivers are registered:
:chromefor local environments and:remote_chromefor WSL. The Windows host IP is resolved from the default gateway viaip route show default. HEADLESSis controlled via an environment variable. SetHEADLESS=0to watch the tests run
Running the Specs
-
Start ChromeDriver on Windows:
.\chromedriver.ps1 -
Run your system specs from WSL:
bundle exec rails test:system
That’s it. Capybara connects to ChromeDriver on the Windows side, ChromeDriver launches Chrome, and Chrome loads pages from the Puma server running in WSL.