VLearnVibium

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.

By Pramod Dutta··15 min read·Verified with Vibium 26.2
▶ Animated overview · made with Remotion

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.

ConcernSelenium Grid on K8sVibium on K8s
OrchestrationHub + node Pods, service wiringKubernetes Job schedules Pods directly
Driver processchromedriver matched to browserNone — Go binary drives Chrome via BiDi
Browser installBase image with pinned browserAuto-downloaded Chrome for Testing
Version driftDriver vs browser mismatch breaks CIPinned, self-contained — no mismatch
Scaling parallelismAdd node Pods, register with hubAdd Job parallelism or shards
Waiting modelManual waits, grid session timeoutsAuto-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.0

How 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: 256Mi

Apply it and watch the run:

kubectl apply -f vibium-job.yaml
kubectl logs -f job/vibium-tests

Two 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.

ResourceRequestLimitWhy
Memory512Mi1GiOne Chrome + Python/Node runtime; headroom for heavy pages
CPU500m1000mRendering and script execution are bursty, not steady
/dev/shm256Mi256MiChrome 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:

ApproachHow it worksBest when
Indexed Job parallelismOne Job, N Pods, shard by completion indexUniform suite, want a single Job to track
Separate Jobs per shardOne Job per feature area or teamDifferent 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 typeLifetimeUse for
emptyDir + sidecarPod lifetimeSidecar uploads to S3/GCS before Pod exits
PersistentVolumeClaimSurvives the PodShared results a later job reads
Object store (S3/GCS) via CLIDurable, externalLong-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 testWhat to set BASE_URL to
Same cluster, same namespacehttp://<service> (short name)
Same cluster, other namespacehttp://<service>.<namespace>.svc.cluster.local
External / public sitehttps://staging.example.com
Local dev bridged inPort-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.

TechniqueEffect
-slim base imageSmaller starting layer, faster pull
--no-install-recommends + cache cleanupDrops tens of MB of unused packages
pip --no-cache-dir / npm ciNo cached wheels or tarballs in the layer
Single RUN for install + cleanupFewer, tighter layers; nothing stale committed
Let Vibium fetch ChromeNo 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/shm is 64Mi. Mount an emptyDir with medium: Memory at /dev/shm as 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 Deployment restarts your finished suite forever. Use a Job or CronJob for 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 install in 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 imagePullSecrets entry 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