Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Chrome processes remain in a Docker-based Laravel Dusk run when one of three owners is left running: the WebDriver browser session, the ChromeDriver server, or Docker’s child-process tree. Fix the owner that started each process instead of issuing a broad pkill chrome. With the normal Dusk setup, let Dusk close browser sessions and stop its tracked ChromeDriver. If a CI image or Selenium service starts the driver, disable Dusk’s startup and stop the driver in that same service’s teardown. At the container boundary, make the main process manage children and consider Docker’s --init for reaping processes when the container exits.

Identify who owns each process

Before changing cleanup, inspect the process tree and every place that can launch Chrome or ChromeDriver:

  • tests/DuskTestCase.php and the installed Dusk package.
  • Your Dockerfile and entrypoint script.
  • CI job scripts and service definitions.
  • Any separate Selenium or ChromeDriver container.

Find every chromedriver, php artisan dusk, Selenium startup, and background shell command. Running Dusk’s automatic driver and a second driver from the image commonly leaves one process outside the cleanup path. Process ownership determines the fix:

Owner Starts Must clean up Typical configuration
Dusk Standalone ChromeDriver and browser sessions Dusk class teardown and session closure Keep static::startChromeDriver()
CI image, entrypoint, or Selenium service ChromeDriver, often before tests That same job or service Comment out Dusk startup and use the matching endpoint
Docker runtime The container’s process tree Main-process supervision and child reaping on exit Use an appropriate entrypoint; consider --init

The lifecycle details cited below are from Laravel Dusk’s 8.x source. The current official documentation page describes Laravel 13.x, so inspect your composer.lock, generated tests/DuskTestCase.php, PHP/PHPUnit versions, and Chrome/ChromeDriver pairing before copying a hook.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How Dusk-managed cleanup works

ChromeDriver is a separate server process

In the standard setup, Dusk starts a standalone ChromeDriver process. In Dusk 8.x, startChromeDriver() stores the Symfony Process object and registers stopChromeDriver() as an after-class callback. That gives the driver a known owner and a class-level shutdown path. Read the implementation in Dusk’s SupportsChrome.php when verifying behavior for that branch.

Do not replace this with a detached shell command such as chromedriver & unless the shell or supervisor also owns and stops it. A process that Dusk did not start or track cannot be stopped by Dusk’s callback.

Browser sessions are a different cleanup layer

Stopping ChromeDriver does not equal closing an active WebDriver session. Dusk 8.x closes active browser sessions during class teardown and closes additional browsers after a browse() callback. The session behavior is implemented in ProvidesBrowser.php.

Use the normal Dusk path first:

public function test_checkout(): void
{
    $this->browse(function ($browser) {
        $browser->visit('/checkout')
                ->assertSee('Checkout');
    });
}

If your test, helper, or package creates a custom RemoteWebDriver session, Dusk cannot infer its lifetime. Put quit() beside that creation and guarantee it runs when an assertion or navigation throws:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
$driver = RemoteWebDriver::create($url, $capabilities);

try {
    // Run browser actions.
} finally {
    $driver->quit();
}

quit() ends the browser session; it does not necessarily stop a separately managed ChromeDriver server. Both operations need an owner.

Keep Dusk as the ChromeDriver owner

  1. Remove duplicate driver startup from the Docker entrypoint, CI script, or image unless it is deliberately the owner.
  2. Keep the generated Dusk startup call and use the standard browse() lifecycle.
  3. Do not background or detach Dusk’s launch through another shell.
  4. Ensure custom sessions use a finally block with quit().
  5. Run the test command and inspect process parentage if anything remains.

This is the simplest option when one php artisan dusk command owns the browser work. Dusk’s documented driver-management guidance is at the Laravel Dusk documentation.

When ChromeDriver is managed outside Dusk

An external owner is appropriate when a Selenium service must outlive one test command, or when a CI image deliberately starts ChromeDriver. In that arrangement:

  1. Comment out static::startChromeDriver() in tests/DuskTestCase.php, as the Laravel documentation describes.
  2. Configure Dusk’s driver() connection URL and port to the externally managed endpoint.
  3. Start exactly one driver in the image, job, or service that owns it.
  4. Stop that driver in the same owner’s teardown, including failure and interrupt handling supported by your CI system.

A third-party image, chilio/laravel-dusk-ci, documents explicit stop-chromedriver and start commands. Treat those commands as image-specific examples, not universal Docker syntax; check the process name, binary path, and startup method in your own image before adapting them.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Example CI ownership pattern

# Pseudocode: use the commands documented by your image
start-your-chromedriver
trap 'stop-your-chromedriver' EXIT INT TERM
php artisan dusk

The important property is not the command spelling. It is that the same job that starts the driver also stops it, and that the trap runs when tests fail or are interrupted.

Use Docker’s process model correctly

Docker states that “The container’s main process is responsible for managing all processes that it starts.” If an entrypoint launches children and then exits, or leaves them under a shell that does not forward signals, cleanup becomes unreliable. Use an exec-form entrypoint so the intended supervisor receives termination signals:

#!/bin/sh
set -eu

# Start only services this container intentionally owns.
exec php artisan dusk

For a container that must run several services, use a supervisor or a carefully written entrypoint that waits for children and forwards signals. Docker’s --init option inserts a tiny init process as PID 1 and “handles reaping of all processes when the container exits,” according to Docker’s multi-process container documentation:

docker run --init your-dusk-image php artisan dusk

--init is a container-exit safeguard. It does not replace $browser->quit(), Dusk’s class teardown, or an external owner’s driver stop command while the container remains alive.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Diagnose leftovers without broad kills

List processes and parentage

docker exec -it dusk-container sh -lc 
  'ps -eo pid,ppid,stat,etime,args | grep -E "chrome|chromedriver|php artisan dusk" | grep -v grep'

Record the PID, parent PID, elapsed time, command line, and whether the process belongs to this test container. A long-lived container may intentionally keep a service available; that is different from a leaked session.

Match symptoms to causes

Symptom Likely cause Action
Two ChromeDriver processes appear at test start Dusk startup plus image/CI startup Choose one owner; disable the other startup path.
Chrome remains after a custom helper test Session created outside browse() Call quit() in finally.
Driver remains after a failed CI job External owner lacks failure/interrupt cleanup Add the CI-native trap/finally cleanup to that owner.
Children remain only after container stop PID 1 did not reap children Use a signal-aware main process and consider --init.
Processes belong to another service Shared host or container namespace Do not kill them from the Dusk job; isolate services and fix their owner.

Avoid pkill chrome as a first response. It can terminate unrelated browser work, hide duplicate ownership, and leave the ChromeDriver server or session bookkeeping untouched.

Version and environment checks

  • Compare the Dusk version in composer.lock with the source branch you are reading; the lifecycle references above are specifically Dusk 8.x.
  • Review the generated tests/DuskTestCase.php; projects may customize startup and connection methods.
  • Pair Chrome and ChromeDriver versions supported by your image.
  • Check whether the CI runner sends TERM, kills the container immediately, or preserves it for diagnostics.
  • Confirm whether a separate Selenium service is expected to outlive one test invocation.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your goal is a clean website image rather than interactive Dusk assertions, ScreenshotNeo provides a one-request screenshot API. It accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each 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.

See the complete parameter reference in the ScreenshotNeo documentation. cURL:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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)
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}`);

Every plan includes the capture options, PDF support, custom waits and scripts, selectors, device presets, caching, signed links, asynchronous jobs, bulk capture, usage API, and OpenAPI specification. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Sign up free to try it.

Best Value
Docker Container Linux Devops Programming Coding T-Shirt
  • Docker, Docker Swarm, Docker Compose, Programmer, Developer, Coding, Programming, Software Engineer, Code, DevOps, Deploy, Deployment, Kubernetes, Salt, Puppet, Chef, Terraform, Container, AWS, Azure, Cloud, Geek, Funny, Computer, Software, Tech, IT
  • Integration, Scrum, Compile, Compilation, Science, Bug, Debug, Python, Linux, Java, Javascript, Scala, Dotnet, Kotlin
  • Lightweight, Classic fit, Double-needle sleeve and bottom hem

Frequently asked questions

Should I stop Chrome or ChromeDriver first?

End the WebDriver session first, then stop the server that owns it. This preserves the protocol’s normal shutdown instead of terminating a browser underneath an active session.

Does restarting the Docker container fix the leak?

It may remove processes when the container is destroyed, but it does not correct duplicate startup, missing session cleanup, or an external driver that is intentionally outside the container. Fix ownership so the next run is clean.

Can I use a separate Selenium container with Dusk?

Yes. Disable Dusk’s automatic ChromeDriver startup, point driver() at the Selenium endpoint, and let the Selenium service’s own lifecycle stop its processes.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why does --init not make Chrome disappear immediately?

It reaps exited children at container shutdown. It does not close a live WebDriver session or request ChromeDriver to terminate while tests are still running.

Frequently Asked Questions

Is a leftover Chrome process always a memory leak?

No. It may be an intentionally long-lived Selenium service, a custom session awaiting quit, a duplicate driver, or an unreaped child. Check PID ownership and elapsed time before labeling it a leak.

Where should custom cleanup code live?

Place it next to the code that creates the custom WebDriver session and call quit() from a finally block, while leaving Dusk-managed browse() cleanup unchanged.

The Bottom Line

Assign one clear owner to each layer: Dusk or an external service for ChromeDriver, every test for its WebDriver session, and Docker’s PID 1 for child-process handling. Once duplicate startups and out-of-band sessions are removed, Chrome processes stop as part of a predictable lifecycle instead of a blanket kill.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.