skip to content

Why does a `k6/browser` script fail on grafana/k6 but run on grafana/k6:master-with-browser?

level: middleimportance: should knowfreq 46%

answer

  1. same binary, different neighbours
  2. the import is never the problem
  3. it dies at browser launch
  4. the tag installs chromium and presets env

basics

~20 s

Both tags carry the identical k6 binary, so the import compiles either way. Only the with-browser image installs Chromium, so on the plain image the run dies at browser launch, reporting that k6 could not detect a chromium-supported browser.

solid answer

~40 s

The browser tag is not a different build of k6 - the image is built as a second stage on top of the first and copies the same `/usr/bin/k6` binary, then adds `chromium` and `chromium-swiftshader`. So `import { browser } from 'k6/browser'` compiles on both images, and the difference only shows up when the script actually launches a browser: on plain `grafana/k6` the executable search finds nothing on `PATH` and the run fails with *k6 couldn't detect google chrome or a chromium-supported browser on this system*. The browser stage also presets two environment variables the plain image does not have, `K6_BROWSER_HEADLESS=true` and `K6_BROWSER_ARGS=no-sandbox`, so a browser test needs no extra configuration there.

code

bash · 7 lines
bash
# Plain image: compiles, then fails when the browser is launched
docker run --rm -i grafana/k6 run - < browser-test.js

# Browser image: chromium is installed, headless and no-sandbox are preset
docker run --rm -i \
  -v "$PWD:/home/k6/screenshots" \
  grafana/k6:master-with-browser run - < browser-test.js

go deeper

for a junior

Remember there are two published images and only one has a browser in it. A test that imports k6/browser needs grafana/k6:master-with-browser; anything protocol-only runs on plain grafana/k6.

for a middle

Explain why the failure arrives late. The binary is identical, so the import succeeds and the run only breaks when k6 searches PATH for a browser executable and finds none on the plain image.

for a senior

Show you choose the tag per job rather than defaulting to the heavier image everywhere, and that you recognise the executable-not-found message as an image problem rather than a script or module problem.

for a principal

The tradeoff is pull cost and attack surface against uniformity. One image for everything is simpler to govern; splitting protocol and browser jobs keeps the common path fast. Decide it deliberately and make it the same on every runner.

## Two images, one binary The k6 Dockerfile publishes two runtime targets. The first, `release`, is the plain image behind `grafana/k6`: Alpine, the `k6` binary at `/usr/bin/k6`, UID 12345, `WORKDIR /home/k6`, `ENTRYPOINT ["k6"]`. The second, `with-browser`, is built **on top of that stage**, copies the very same `/usr/bin/k6` across, and then installs `chromium` and `chromium-swiftshader`. It is published under tags such as `grafana/k6:master-with-browser`. The consequence is the one people get wrong: the two images do not differ in their k6. The binary is byte-identical. Everything `k6 version` reports, every module the binary exposes, every flag it accepts is the same. What differs is whether a browser is installed beside it. ## What actually fails, and when Because the binary is the same, the browser module is compiled in on both images: - `import { browser } from 'k6/browser';` **compiles on the plain image.** There is no import-time error, no missing-module message, and no warning. - The failure happens later, when the script asks for a browser and k6 goes looking for an executable. With no explicit path configured it searches `PATH` for names like `chromium`, `chromium-browser`, `google-chrome` and `google-chrome-stable`. - On plain `grafana/k6` none of those exist, so k6 returns *k6 couldn't detect google chrome or a chromium-supported browser on this system*, wrapped as a failure to find the browser executable. That timing is why the symptom is confusing. The script starts, the init stage looks healthy, and the run dies at the first browser call - so the instinct is to suspect the script rather than the image tag. ## What the browser tag brings | | `grafana/k6` | `grafana/k6:master-with-browser` | |---|---|---| | the `k6` binary | `/usr/bin/k6` | the same binary, copied from the plain stage | | `k6/browser` importable | yes | yes | | Chromium installed | no | `chromium` plus `chromium-swiftshader` | | `K6_BROWSER_HEADLESS` | unset | preset to `true` | | `K6_BROWSER_ARGS` | unset | preset to `no-sandbox` | | image size | small | substantially larger, because a browser is in it | `chromium-swiftshader` is a software renderer, which is what lets Chromium draw without a GPU in a container. The two preset environment variables are what make the tag usable with no further configuration: the browser starts headless, and it starts with the sandbox opt-out the Alpine base needs. ## Choosing the tag in a CI job The pull is the cost, so the tag is worth choosing per job rather than globally: 1. **Protocol-only jobs** - anything that talks HTTP, gRPC or WebSocket and never imports `k6/browser` - stay on plain `grafana/k6`. There is nothing to gain from shipping a browser to a runner that will not start one. 2. **Browser jobs** pin the browser tag. Both variants exist for the same releases, published side by side, so a job can move between them without changing anything but the tag. 3. **Mixed suites** are usually better split into two steps than run wholesale on the heavier image, which keeps the common path fast. Everything else about the invocation is unchanged: the entrypoint is still `k6`, so the arguments still begin with `run`, and the script still arrives either on standard input or through a mount. ## Where the run's files land A browser test usually wants to write something back, and the browser tag inherits `WORKDIR /home/k6` from the stage below it. That is why the documented invocation mounts the host directory at `/home/k6/screenshots` rather than somewhere arbitrary: the container's own working directory is where relative paths resolve, and anything written to a path that is not mounted disappears with the container. The mount also has to be writable by UID 12345, which the browser stage keeps - it drops back to that user after installing the browser packages as root at build time. ## Checks and misreadings - **Do not read the plain image's failure as a missing module.** `k6/browser` is present; the browser is not. - **Do not expect `k6 version` to tell them apart.** It is the same binary and reports the same version on both. - **Do not install a browser into the plain image at run time.** The container runs as UID 12345 and has no package manager privileges; the browser tag exists precisely so you do not have to. - **Remember the browser tag's presets are environment, not defaults in the binary.** Run the same script with plain `k6` on a laptop and neither variable is set for you.

  • Does the browser image contain a different build of k6?
    No. Its stage is built from the plain image's stage and copies the identical `/usr/bin/k6` across before installing Chromium. Both images report the same version and expose the same modules and flags; only the presence of a browser and two preset environment variables differ.
  • How does k6 locate the browser inside the image?
    It searches `PATH` for known executable names such as `chromium`, `chromium-browser`, `google-chrome` and `google-chrome-stable`, unless `K6_BROWSER_EXECUTABLE_PATH` names an absolute path, which takes precedence. The browser image satisfies the search by installing `chromium`.
  • Why does the browser tag also install `chromium-swiftshader`?
    It provides software rendering, so Chromium can rasterise without a GPU. A CI runner or container host normally exposes no graphics hardware, and the browser tag is meant to work there with no extra configuration.

saying these in an interview costs you the question

  • Says k6/browser cannot be imported on the plain image
  • Thinks the browser tag ships a different k6 build
  • Expects k6 version to distinguish the two images
  • Tries to install chromium into the running plain container
  • Assumes headless mode is a binary default rather than image env