Run Vibium Tests on Kubernetes
Run Vibium tests on Kubernetes: build a headless image, ship it as a Job, scale parallel Pods, and pull screenshots and traces as artifacts.
To run Vibium tests on Kubernetes, package them into a headless container image, run that image as a Kubernetes Job, scale out with Pod parallelism, and write screenshots and traces to a mounted volume so they survive the Pod. Vibium ships as a single Go binary that auto-downloads its own Chrome for Testing, so a Pod needs no Selenium Grid, no driver process, and no browser-version matching — the classic sources of flaky containerized tests. You build an image that runs pip install vibium (or npm install vibium), pre-fetch Chrome at build time, set HEADLESS=true, and mount an emptyDir at /dev/shm so Chrome does not crash. Kubernetes becomes the scheduler: each Pod launches its own isolated Chrome, and you get N-way parallelism by adding N Pods rather than wiring up a hub. Because Vibium's find() auto-waits for actionability, the same suite that passes locally passes on a throttled cluster node without sleep() calls. This guide walks the full path from image to Job to artifacts.
What does the Vibium-on-Kubernetes pipeline look like?
The pipeline is a straight line: build a headless image, push it to a registry, run it as a Job, scale the Pods, then collect artifacts. There is no grid to stand up in the middle.
Each stage maps to one concrete artifact. Build image produces a Dockerfile that installs Vibium and Chrome. Push registry puts that image somewhere the cluster can pull. Run as Job is a Job manifest that runs the container to completion. Scale Pods uses Job parallelism (or separate sharded Jobs) to run many browsers at once. Collect artifacts writes screenshots and traces to a volume and ships them off-cluster. The rest of this article fills in each box with copy-paste manifests.
If you are new to the tool itself, start with what is Vibium and install Vibium before containerizing it.
Why is Vibium a clean fit for Kubernetes?
Vibium removes the three things that make browser tests painful to containerize: driver management, grid orchestration, and version drift. Older stacks needed a Selenium hub Pod, one or more node Pods, and a driver binary matched to the exact browser build — a fragile assembly that broke whenever Chrome auto-updated. Vibium collapses that into one process.
A single Go binary launches its own Chrome for Testing, pinned to a known-good version, so nothing drifts. There is no hub, no node registration, no chromedriver to align. That means your Pod spec is just "run this container," and Kubernetes' own scheduler does the fan-out that a Selenium Grid used to do by hand.
| Concern | Selenium Grid on K8s | Vibium on K8s |
|---|---|---|
| Orchestration | Hub + node Pods, service wiring | Kubernetes Job schedules Pods directly |
| Driver process | chromedriver matched to browser | None — Go binary drives Chrome via BiDi |
| Browser install | Base image with pinned browser | Auto-downloaded Chrome for Testing |
| Version drift | Driver vs browser mismatch breaks CI | Pinned, self-contained — no mismatch |
| Scaling parallelism | Add node Pods, register with hub | Add Job parallelism or shards |
| Waiting model | Manual waits, grid session timeouts | Auto-waiting find() (actionability) |
The net effect: fewer moving parts, fewer Pods, and a spec that reads like a normal batch workload. For the architecture behind the single binary, see how Vibium works.
How do I build a headless Vibium container image?
Build an image that pip-installs Vibium, adds Chrome's shared system libraries, and pre-downloads Chrome for Testing so the first test does not pay the download cost. This Dockerfile targets the Python client; the JS variant is nearly identical.
# Dockerfile
FROM python:3.12-slim
# Chrome for Testing needs these shared libraries at runtime.
RUN apt-get update && apt-get install -y --no-install-recommends \
libnss3 libatk-bridge2.0-0 libgbm1 libasound2 \
libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 \
libxrandr2 libdrm2 libpango-1.0-0 libcairo2 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install Vibium and pre-fetch Chrome during the build, so the image
# is fully self-contained and Pods start without a network download.
RUN pip install --no-cache-dir vibium pytest && vibium install
COPY tests/ ./tests/
ENV HEADLESS=true
CMD ["pytest", "-v", "tests/"]For the JavaScript client, swap the base image for node:22-slim, keep the same apt-get block, replace the install line with RUN npm install vibium, and change the entry point to your test runner. In both cases, vibium install runs at build time so the layer is cached and every Pod starts Chrome-ready.
Read headless mode from an environment variable so the identical code runs headed on your laptop and headless in the Pod:
import os
from vibium import browser_sync as browser
headless = os.getenv("HEADLESS", "false") == "true"
vibe = browser.launch(headless=headless)
vibe.go("https://example.com")
title = vibe.find("h1").text()
print("Heading:", title)
vibe.quit()The equivalent JavaScript reads the same flag before launching:
const { browser } = require('vibium/sync')
const headless = process.env.HEADLESS === 'true'
const bro = browser.launch({ headless })
const page = bro.page()
page.go('https://example.com')
console.log('Heading:', page.find('h1').text())
bro.close()Build and push the image to a registry your cluster can reach:
docker build -t registry.example.com/vibium-tests:1.0 .
docker push registry.example.com/vibium-tests:1.0How do I run Vibium as a Kubernetes Job?
Run the image as a Job, not a Deployment — tests are finite work that should run once and stop, which is exactly what a Job models. A Deployment would treat your suite as a service and restart it forever. The critical detail is mounting shared memory at /dev/shm, because containerized Chrome crashes with the default 64Mi.
# vibium-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: vibium-tests
spec:
backoffLimit: 1
template:
spec:
restartPolicy: Never
containers:
- name: vibium
image: registry.example.com/vibium-tests:1.0
env:
- name: HEADLESS
value: "true"
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
volumeMounts:
- name: dshm
mountPath: /dev/shm
volumes:
- name: dshm
emptyDir:
medium: Memory
sizeLimit: 256MiApply it and watch the run:
kubectl apply -f vibium-job.yaml
kubectl logs -f job/vibium-testsTwo settings deserve attention. restartPolicy: Never with backoffLimit: 1 means a genuinely failing suite fails the Job instead of retrying endlessly and masking a real bug. The /dev/shm emptyDir with medium: Memory gives Chrome the shared memory it needs; without it you will see renderer crashes that look like flaky tests but are pure container misconfiguration.
How much CPU and memory should each Pod request?
Start each single-Chrome Pod at a 512Mi memory request with a 1Gi limit and roughly half a CPU, then tune from observed usage. Chrome is memory-hungry, and a Pod that hits its limit gets OOM-killed mid-test, which surfaces as an unexplained failure.
| Resource | Request | Limit | Why |
|---|---|---|---|
| Memory | 512Mi | 1Gi | One Chrome + Python/Node runtime; headroom for heavy pages |
| CPU | 500m | 1000m | Rendering and script execution are bursty, not steady |
/dev/shm | 256Mi | 256Mi | Chrome shared memory; default 64Mi crashes the renderer |
These are starting points, not gospel. A suite that opens image-heavy or single-page apps may need a 2Gi limit; a suite of light form flows may run happily at 768Mi. Watch kubectl top pod during a real run and right-size from there. Keeping requests honest also lets the scheduler pack more Pods per node, which matters once you scale out. For the waiting behavior that keeps these Pods stable under CPU pressure, see how Vibium auto-waits.
One subtlety on CPU: Chrome's rendering is bursty, so a low CPU request with a higher limit usually works well — the Pod idles cheaply between actions but can grab a full core when it paints a heavy page. Setting the request too high wastes schedulable capacity and reduces how many Pods fit per node. Under Vibium's auto-waiting model, a CPU-throttled Pod simply takes a little longer inside find() rather than failing, so you can afford to run lean and let the actionability checks absorb the slack.
How do I run Vibium tests in parallel across Pods?
Scale out with Job parallelism and shard your suite so each Pod runs a distinct slice of tests. Because every Pod launches its own isolated Chrome, there is no shared browser state and no contention — N Pods give you clean N-way parallelism.
The simplest model uses an indexed Job: Kubernetes injects a JOB_COMPLETION_INDEX into each Pod, and your test command uses it to pick a shard.
# vibium-parallel-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: vibium-parallel
spec:
completions: 4
parallelism: 4
completionMode: Indexed
backoffLimit: 0
template:
spec:
restartPolicy: Never
containers:
- name: vibium
image: registry.example.com/vibium-tests:1.0
command: ["/bin/sh", "-c"]
args:
- >-
pytest -v
--shard-id=$(JOB_COMPLETION_INDEX)
--num-shards=4
env:
- name: HEADLESS
value: "true"
resources:
requests: { cpu: "500m", memory: "512Mi" }
limits: { cpu: "1", memory: "1Gi" }
volumeMounts:
- name: dshm
mountPath: /dev/shm
volumes:
- name: dshm
emptyDir: { medium: Memory, sizeLimit: 256Mi }This runs four Pods at once, each executing a quarter of the suite. Use a sharding plugin for your runner (for example pytest-shard, or a Jest shard flag) to split by index. If your runner has no native sharding, split tests into directories per shard and select by index in the command.
Two scaling styles are worth comparing:
| Approach | How it works | Best when |
|---|---|---|
| Indexed Job parallelism | One Job, N Pods, shard by completion index | Uniform suite, want a single Job to track |
| Separate Jobs per shard | One Job per feature area or team | Different resources/timeouts per group |
Either way, wall-clock time drops roughly linearly with Pod count until you saturate cluster capacity. The same sharding logic that speeds up CI applies here — see parallelizing Vibium tests for splitting strategy and structuring a Vibium suite for the fixture that centralizes headless launch.
How do I collect screenshots and traces from Pods?
Write failure screenshots and Vibium traces to a mounted volume, then ship them off the Pod before it is garbage-collected. A Pod's filesystem vanishes when the Job finishes, so anything you want to keep must leave the Pod. Vibium's screenshot() returns PNG bytes, so persisting one is a single write.
Capture on failure in a fixture and write to a shared path:
import pytest
from vibium import browser_sync as browser
ARTIFACTS = "/artifacts"
@pytest.fixture
def vibe(request):
instance = browser.launch(headless=True)
yield instance
if getattr(request.node, "rep_call", None) and request.node.rep_call.failed:
png = instance.screenshot(full_page=True)
with open(f"{ARTIFACTS}/fail-{request.node.name}.png", "wb") as f:
f.write(png)
instance.quit()You have three durable destinations for that /artifacts directory, depending on how long the data must live:
| Volume type | Lifetime | Use for |
|---|---|---|
emptyDir + sidecar | Pod lifetime | Sidecar uploads to S3/GCS before Pod exits |
PersistentVolumeClaim | Survives the Pod | Shared results a later job reads |
| Object store (S3/GCS) via CLI | Durable, external | Long-term artifact archive and CI links |
For same-Pod upload, mount an emptyDir at /artifacts on both the test container and a small sidecar that runs aws s3 cp --recursive /artifacts s3://... after tests finish. For richer debugging, record a Vibium trace with DOM snapshots, write trace.zip to the same directory, and open it in the Vibium trace viewer to scrub the run frame by frame. See taking a screenshot for the full capture options, including full-page and element shots.
How do I schedule recurring Vibium runs on Kubernetes?
Wrap the Job in a CronJob to run smoke or synthetic checks on a schedule — every fifteen minutes, hourly, or nightly. A CronJob creates a fresh Job on each tick, so every run gets clean Pods with fresh Chrome instances and no state carried over.
# vibium-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: vibium-smoke
spec:
schedule: "*/15 * * * *" # every 15 minutes
concurrencyPolicy: Forbid # skip a tick if the last run is still going
jobTemplate:
spec:
backoffLimit: 1
template:
spec:
restartPolicy: Never
containers:
- name: vibium
image: registry.example.com/vibium-tests:1.0
env:
- name: HEADLESS
value: "true"
volumeMounts:
- name: dshm
mountPath: /dev/shm
volumes:
- name: dshm
emptyDir: { medium: Memory, sizeLimit: 256Mi }concurrencyPolicy: Forbid stops a slow run from stacking on top of the next tick, which would double your cluster load and muddy the signal. This pattern turns Vibium into a lightweight synthetic-monitoring probe: if the smoke suite fails, the CronJob's Job goes red and your alerting picks it up. Because each run is a fresh Pod, a leaked browser or wedged session in one run never poisons the next.
How do Vibium Pods reach the app under test?
Point Vibium at the app the same way any Pod reaches another workload — an in-cluster Service DNS name for apps on the same cluster, or a public URL for external targets. Vibium's Chrome runs inside the Pod, so the network context is the Pod's, not your laptop's. That single fact resolves most "works locally, fails in the cluster" confusion.
If the app under test also runs in the cluster, use its Service name, which Kubernetes resolves through internal DNS. Pass it in as an environment variable so the image is not hard-coded to one environment:
import os
from vibium import browser_sync as browser
base_url = os.getenv("BASE_URL", "http://web-app.staging.svc.cluster.local")
vibe = browser.launch(headless=True)
vibe.go(f"{base_url}/login")
vibe.find("#username").type("qa@example.com")
vibe.find("#password").type("secret")
vibe.find("button[type=submit]").click()
assert "Dashboard" in vibe.find("h1").text()
vibe.quit()Set BASE_URL per environment in the Job spec, and the same image runs against staging, a preview namespace, or a public URL without a rebuild:
env:
- name: BASE_URL
value: "http://web-app.staging.svc.cluster.local"
- name: HEADLESS
value: "true"| Target of the app under test | What to set BASE_URL to |
|---|---|
| Same cluster, same namespace | http://<service> (short name) |
| Same cluster, other namespace | http://<service>.<namespace>.svc.cluster.local |
| External / public site | https://staging.example.com |
| Local dev bridged in | Port-forward or a NodePort/Ingress URL |
For a fuller login walk-through you can drop into these tests, see automating login with Vibium. Keeping URLs and credentials in environment variables and Secrets — never baked into the image — is what lets one image serve every environment. That, in turn, is what makes the page object model portable across namespaces: the objects describe what to click, and the environment supplies where.
How do I keep the Vibium image small and fast to pull?
Keep the image lean by starting from a slim base, installing only Chrome's real runtime libraries, and letting Vibium supply its own browser. A smaller image pulls faster onto every node, which directly shortens Pod startup time when you fan out to dozens of shards.
Three levers matter most. First, use a -slim base image rather than a full OS — you do not need build tools at runtime. Second, install only the shared libraries Chrome actually loads, and clean the apt cache in the same layer so it is never committed. Third, do not try to pre-bake a system Chrome or a driver; Vibium downloads a pinned Chrome for Testing itself, and duplicating it only bloats the image.
| Technique | Effect |
|---|---|
-slim base image | Smaller starting layer, faster pull |
--no-install-recommends + cache cleanup | Drops tens of MB of unused packages |
pip --no-cache-dir / npm ci | No cached wheels or tarballs in the layer |
Single RUN for install + cleanup | Fewer, tighter layers; nothing stale committed |
| Let Vibium fetch Chrome | No duplicate/mismatched system browser |
The Dockerfile earlier in this guide already applies these: one apt-get layer with --no-install-recommends and a cache wipe, pip --no-cache-dir, and vibium install baking in exactly the browser the tests run. The result is an image that is both self-contained — no network fetch at Pod start — and small enough that pulling it across a large node pool is not the bottleneck. When you later scale to many Pods, this pull time is often what separates a fast run from a slow one.
What are the common gotchas running Vibium on Kubernetes?
Most Kubernetes failures with Vibium are container-environment issues, not test bugs — and nearly all trace back to shared memory, resource limits, or artifact lifetime. Knowing the short list saves hours of chasing "flaky" tests that are really infrastructure.
- Chrome crashes on start: the default
/dev/shmis 64Mi. Mount anemptyDirwithmedium: Memoryat/dev/shmas shown above. This is the single most common cause of renderer crashes in containers. - Pods get OOM-killed mid-run: the memory limit is too low for the pages under test. Raise the limit, watch
kubectl top pod, and right-size. An OOM kill looks like a random failure but is deterministic once you see the memory ceiling. - Wrong workload type: using a
Deploymentrestarts your finished suite forever. Use aJoborCronJobfor finite test work. - Lost artifacts: screenshots written to the container filesystem disappear with the Pod. Write to a mounted volume and ship them out before the Job completes.
- Slow Pod startup: if you skip
vibium installin the image, every Pod downloads Chrome on first run. Pre-fetch at build time so the browser is baked into the image layer. - Registry pull failures on private images: add an
imagePullSecretsentry to the Pod spec so nodes can pull your test image.
Avoid these six and the rest is ordinary batch scheduling. None of them are Vibium-specific quirks — they are the standard tax of running any headless browser in a container, and Vibium's single-binary design keeps the list this short.
Next steps
Frequently asked questions
How do I run Vibium tests on Kubernetes?
Build a container image that pip-installs vibium and pre-downloads Chrome, then run it as a Kubernetes Job with HEADLESS=true. Vibium is a single Go binary that fetches its own Chrome for Testing, so the Pod only needs Chrome's shared libraries and enough shared memory.
How much memory does a Vibium Pod need?
Start with a 512Mi request and a 1Gi limit per Pod for a single Chrome instance, then adjust from real usage. The critical fix is shared memory: mount an emptyDir with medium Memory at /dev/shm, because Chrome crashes with the default 64Mi /dev/shm in a container.
Do I need a Selenium Grid or hub to run Vibium on Kubernetes?
No. Vibium has no hub, node, or driver process to orchestrate. Each Pod runs a self-contained Go binary that launches its own Chrome, so Kubernetes itself is the scheduler — you scale by adding Pods with Job parallelism or shards, not by wiring up a grid.
How do I run Vibium tests in parallel on Kubernetes?
Set parallelism on a Kubernetes Job and shard your test suite across the completions, or launch separate Jobs per shard. Each Pod runs an isolated Chrome, so N Pods give you N-way parallelism with no shared browser state and no contention between shards.
How do I get screenshots and traces out of a Kubernetes Pod?
Write failure screenshots and Vibium traces to a mounted volume — an emptyDir for same-Pod sidecar upload, or a PersistentVolume or object-store bucket for durable artifacts. A sidecar or post-run step then ships them to S3, GCS, or your CI so they outlive the Pod.
Should I use a Kubernetes Job or a Deployment for Vibium tests?
Use a Job (or CronJob for scheduled runs). Tests are finite work that should run to completion and stop, which is exactly a Job. A Deployment is for long-lived services and would restart your test container forever, so it is the wrong primitive for a test suite.
Vibium is created by Jason Huggins. This is an independent tutorial — see the official Vibium site and GitHub repo for canonical docs.
Related guides
Vibium Best Practices: The Complete Guide
Vibium best practices for reliable browser automation: semantic locators, actionability waits, page objects, isolation, CI, and AI checks.
13 min read→Best PracticesA Complete Vibium CI/CD Pipeline
Build a complete Vibium CI/CD pipeline: install, headless run, parallel shards, artifact capture, and quality gates that block bad merges on every push.
12 min read→Best PracticesData-Driven Testing with Vibium
Data-driven testing with Vibium: feed one browser test many rows from arrays, CSV, or JSON, loop over cases, and keep the automation logic in one place.
15 min read→Best PracticesRun Vibium with Docker Compose
Run Vibium with Docker Compose: orchestrate a headless test service, an app-under-test, mounted artifact volumes, healthchecks, and parallel workers.
15 min read→