Browse the Knowledge Hub211 resources
Kubernetes review for testing: alternatives, pricing and setup
Kubernetes is an open-source system that schedules containers across a cluster of machines and keeps them in the state you declare, restarting failures, scaling replicas and routing traffic. For testing it is the usual foundation for per-branch environments, autoscaled Selenium Grids, distributed load generation and test suites run as batch Jobs next to the services they test.
QAble has evaluated this tool and works alongside it. The factual sections are complete; our view is bounded to what we have actually seen rather than a long-term verdict.
- last reviewed
- 13 October 2026
- version covered
- Kubernetes 1.37
- licence
- Open source
At a glance
Kubernetes at a glance
- Vendor
- CNCF
- Category
- Containers
- Licence
- Open source (Apache 2.0)
- Platforms
- Cloud, Self-hosted
- Integrates with
- Docker, Helm, Selenium Grid, Jenkins, k6
- First released
- 2014
- Version reviewed
- Kubernetes 1.37
Verdict
Our verdict on Kubernetes
Strengths
- A namespace per pull request gives each branch an isolated environment, removed with one delete command
- Jobs run a suite to completion with retry limits, deadlines and automatic clean-up afterwards
- Indexed Jobs hand each pod a shard number, so a suite splits across pods without extra tooling
- The Selenium Grid Helm chart autoscales browser nodes from the session queue through KEDA
- kind runs a real cluster inside Docker, so manifests and Jobs are testable on one CI runner
- Apache 2.0 and vendor-neutral under the CNCF, with the same API on every major cloud
Weaknesses
- Networking, storage, access control and upgrades all need someone who understands them
- Each minor release gets about a year of patches, so clusters need upgrading every year
- Pods without resource requests make test timings noisy and failures look random
- Managed clusters bill for nodes, and some add a per-cluster fee that idle test clusters still incur
- Debugging spans pods, events and logs, a slower loop than reading Compose logs
Our take
For a QA team, Kubernetes matters in three places: as the platform that makes an isolated environment per branch or pull request practical, as the way to scale a Selenium Grid or a fleet of load generators beyond one machine, and as a place to run suites as Jobs beside the services they test. It is also substantial operational complexity. QAble has evaluated it in these roles rather than operated clusters long term, so treat this as a bounded take. If one machine running Docker Compose can hold your test environment, start there. Kubernetes earns its cost for testing when the application already runs on it, so test environments reuse the production manifests or Helm charts, or when parallel demand has outgrown a single host. Budget for resource requests, clean-up of stale environments and a cluster upgrade at least once a year.
We have evaluated this tool rather than maintained a long-running suite on it, so this is a bounded view rather than a long-term verdict. The rest of this page is factual.
How it works
How Kubernetes works
Desired state and controllers
You describe objects such as Deployments, Services and Jobs in YAML and send them to the API server, which stores them. Controllers then loop continuously, comparing what exists with what you declared and acting on the difference: starting pods, replacing failed ones, rescheduling work from a lost node. This is why a crashed dependency in a Kubernetes test environment usually comes back on its own. It is also why problems hide: a service that crashes every few minutes keeps getting restarted, so a test failure caused by it looks intermittent. Check restart counts before blaming the test.
Pods, nodes and the scheduler
The pod is the unit Kubernetes runs: one or more containers sharing a network address and, optionally, volumes. The scheduler places each pod on a node that has enough unreserved capacity for the pod’s resource requests. Limits are enforced on the node: a container that reaches its CPU limit is throttled, and one that exceeds its memory limit may be killed by the kernel. For test workloads this explains two classic symptoms. Browser pods without requests get packed onto busy nodes and run slowly, so timeouts fail at random; and a Chrome pod with a tight memory limit restarts mid-test with an OOMKilled reason rather than a clear error.
Namespaces as environment boundaries
A namespace groups objects under one name, and most resources, including Services, Deployments, Secrets and Jobs, live inside one. Services get DNS names that include their namespace, so the same manifests deployed into pr-123 and pr-124 produce two environments whose services find each other by short name and never cross over. Deleting a namespace deletes everything in it, which makes tearing down an environment one command. A ResourceQuota on each test namespace caps how much CPU and memory it can request, which stops one forgotten preview environment starving everyone else.
Jobs for work that finishes
Deployments keep services running forever; a Job runs pods until a set number succeed, which is the shape of a test run. The pod template may use only Never or OnFailure as its restart policy. backoffLimit, 6 by default, caps retries, and activeDeadlineSeconds fails the whole Job after a time limit, taking precedence over retries. ttlSecondsAfterFinished deletes the Job and its pods a set time after it ends. In Indexed completion mode, each pod gets an index from 0 to completions minus 1, exposed as the JOB_COMPLETION_INDEX environment variable, which a test runner can use as its shard number.
Strengths
What it does well
Per-branch environments from production manifests
When the application is already deployed with Kubernetes manifests or a Helm chart, a preview environment is the same chart installed into a new namespace with the branch’s image tag. Testers get an isolated copy of the system for each pull request, and tests run against something structurally identical to production instead of a hand-built shared environment.
Test suites as Jobs beside the system under test
Running the suite as a Job inside the cluster puts it on the same network as the services, so it reaches them by DNS name with no ingress or port forwarding, and it inherits the cluster’s secrets and configuration mechanisms. Retries, deadlines and clean-up become declared fields instead of shell script logic.
Sharding across pods
An Indexed Job with completions and parallelism set to the same number starts that many pods at once, each with its own JOB_COMPLETION_INDEX. Passing the index to the test runner’s shard option splits the suite with no coordinator. backoffLimitPerIndex, stable since Kubernetes 1.33, retries a failed shard without rerunning the others.
An autoscaled Selenium Grid
The official selenium-grid Helm chart deploys the Grid components and browser nodes. With autoscaling enabled it installs KEDA, which scales browser nodes from the length of the Grid’s session queue, by default as one-session Jobs, from zero up to a configurable maximum (24 in the chart’s defaults). Idle capacity costs nothing and a burst of parallel sessions gets fresh browsers.
Distributed load generation
Grafana’s k6 Operator adds a TestRun resource that runs one k6 script across several pods, so load generation scales past a single machine and runs inside the same network as the service under test. The test definition becomes a declared resource that can be versioned and reviewed like any other manifest.
Real clusters on a laptop or CI runner
kind runs Kubernetes nodes as Docker containers, so a full cluster runs on a laptop or a GitHub-hosted Ubuntu runner with no cloud account. It was designed for testing Kubernetes itself, and its documentation recommends stable releases for CI. It is the cheapest way to test manifests, Helm charts and Job-based test runs before they reach a shared cluster.
Pricing
Kubernetes pricing and licensing
Kubernetes is open source under Apache 2.0, a CNCF project with no paid edition. Cost comes from where it runs: the machines that act as nodes, load balancers, storage, a control plane fee on some managed services, and the people who operate and upgrade clusters. For testing, idle environments are the cost that grows quietly, because a preview namespace nobody deleted keeps its resource requests reserved.
- Licence: Apache 2.0. Every feature is in the open-source release; vendors sell hosting, support and distributions around it.
- Managed control planes: Amazon EKS charges per cluster per hour, with list prices of $0.10 under standard support and $0.60 under extended support on 13 October 2026. Azure AKS’s Free tier has free cluster management with no financially backed SLA, which Microsoft recommends for development and test clusters.
- Nodes are the main bill: virtual machines, disks, public IP addresses and load balancers, priced by the provider whatever runs on them.
- Local and CI: kind is free and runs on a single machine with Docker, including GitHub-hosted Ubuntu runners.
- Hidden costs: preview environments without a time limit, browser nodes left scaled up, and engineering time for the yearly minor version upgrades that the roughly one-year patch window requires.
Checked against the vendor’s own pages on 13 October 2026. Pricing changes; confirm on the official site before buying.
Getting started
Getting started with Kubernetes
Setup, step by step
- Install Docker, which kind uses to run cluster nodes, then install kind and kubectl.
- Create a local cluster with kind create cluster --name tests and check it with kubectl cluster-info.
- Build your test image locally and load it into the cluster with kind load docker-image.
- Apply the Job manifest below, which runs the suite once with a retry limit and a deadline.
- Wait for the Job, read its logs, then delete the cluster with kind delete cluster --name tests.
Install
# macOS with Homebrew; the kind and Kubernetes docs cover Linux and Windows
brew install kind kubectl
# or, with Go installed
go install sigs.k8s.io/[email protected]
kind create cluster --name tests
kubectl cluster-info --context kind-testsA first example
# e2e-job.yaml: run the suite once inside the cluster
apiVersion: batch/v1
kind: Job
metadata:
name: e2e
spec:
backoffLimit: 1 # the default of 6 retries hides flaky suites
activeDeadlineSeconds: 1800 # fail the Job if the run hangs
ttlSecondsAfterFinished: 3600 # delete the Job and its pods an hour after it ends
template:
spec:
restartPolicy: Never # Jobs allow only Never or OnFailure
containers:
- name: e2e
image: shop-e2e:dev # kind load docker-image shop-e2e:dev --name tests
imagePullPolicy: IfNotPresent
env:
- name: BASE_URL # a Service in the default namespace
value: http://shop.default.svc.cluster.local:3000
resources:
requests: { cpu: "1", memory: 2Gi }
limits: { memory: 2Gi }
# kubectl apply -f e2e-job.yaml
# kubectl wait --for=condition=complete job/e2e --timeout=30m
# kubectl logs job/e2eWhat trips people up first
ErrImagePull or ImagePullBackOff on an image you just built. The cluster’s nodes have their own image store, so an image in your local Docker is invisible to them, and the node tries to pull it from a registry that does not have it. With kind, run kind load docker-image with the cluster name after every build. Avoid the latest tag for local images: Kubernetes defaults to always pulling images tagged latest or untagged, so even a loaded image is ignored. kubectl describe pod shows the exact pull error in its events, which is the first place to look for any pod that will not start.
In practice
Using Kubernetes in practice
An isolated environment for every pull request
Name the namespace after the pull request, install the application’s own chart into it with the image built from that commit, and point the suite at the services inside it. Label the namespace so a scheduled job can find and delete preview environments older than a day, because the close-event workflow that should delete them will sometimes fail. Apply a ResourceQuota per namespace so a runaway environment cannot consume the cluster, and seed test data through a Job rather than by hand, so every environment starts from the same state. Helm’s --wait makes the install step block until the release is ready, which gives the pipeline a clean failure point before any test runs.
# Create or update a preview environment for one pull request
NS="pr-${PR_NUMBER}"
kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f -
kubectl label namespace "$NS" purpose=preview --overwrite
# The same chart as production, with this commit's image
helm upgrade --install shop ./chart \
--namespace "$NS" \
--set image.tag="$GIT_SHA" \
--wait --timeout 10m
# When the pull request closes: everything in the namespace goes
kubectl delete namespace "$NS" --wait=falseSharding a suite with an Indexed Job
For a suite that takes too long on one machine, run it as an Indexed Job with completions and parallelism both set to the number of shards. Each pod reads JOB_COMPLETION_INDEX, which starts at 0, and passes it to the runner’s shard option, which for Playwright counts from 1. backoffLimitPerIndex retries a failed shard once without restarting the others, and the Job is complete only when every index has succeeded. Give each pod resource requests that match what one shard really needs, so the scheduler spreads pods across nodes instead of stacking them where they slow each other down, and collect results by writing reports to shared storage or by uploading them from each pod.
apiVersion: batch/v1
kind: Job
metadata:
name: e2e-sharded
spec:
completionMode: Indexed
completions: 4 # four shards in total
parallelism: 4 # run them all at once
backoffLimitPerIndex: 1 # retry a failed shard once, alone
ttlSecondsAfterFinished: 3600
template:
spec:
restartPolicy: Never
containers:
- name: e2e
image: registry.example.com/shop-e2e:1.4.2
command: ["sh", "-c"]
# JOB_COMPLETION_INDEX is 0 to 3; Playwright shards count from 1
args: ["npx playwright test --shard=$((JOB_COMPLETION_INDEX + 1))/4"]
resources:
requests: { cpu: "2", memory: 3Gi }
limits: { memory: 3Gi }Testing manifests and Jobs in CI with kind
Before a manifest or chart reaches a shared cluster, run it in a throwaway one. The helm/kind-action creates a kind cluster on a GitHub-hosted Ubuntu runner, named chart-testing unless you set cluster_name, and deletes it when the job ends. Hosted macOS and Windows runners cannot create kind clusters because they lack Docker with Linux container support. kubectl wait for the complete condition only returns early on success, so a failed Job surfaces as a timeout: set the timeout just above activeDeadlineSeconds and print the logs whatever the outcome, so the job output contains the evidence.
# .github/workflows/k8s-e2e.yml
name: k8s-e2e
on: [pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: helm/kind-action@v1
with:
cluster_name: tests
- name: Build and load images into the cluster
run: |
docker build -t shop:ci .
docker build -t shop-e2e:dev -f Dockerfile.e2e .
kind load docker-image shop:ci shop-e2e:dev --name tests
- name: Deploy the app and run the suite as a Job
run: |
kubectl apply -f k8s/
kubectl rollout status deployment/shop --timeout=5m
kubectl apply -f e2e-job.yaml
kubectl wait --for=condition=complete job/e2e --timeout=31m
- name: Test output
if: always()
run: kubectl logs job/e2e --all-containersRunning a Selenium Grid that scales itself
The docker-selenium project publishes a Helm chart that deploys the Grid router, distributor, session queue and browser nodes. Setting autoscaling.enabled installs KEDA alongside it, or autoscaling.enableWithExistingKEDA uses one you already run. With the default scaling type of job, each browser node handles a session and is then drained, so every test gets a fresh browser, and the node count follows the session queue from zero up to autoscaling.scaledOptions.maxReplicaCount. Set browser node resource requests realistically, since Chrome under-provisioned on a crowded node is the commonest cause of timeouts that only happen on the cluster, and pin the chart version so the browser versions change only when you choose.
Debugging pods that will not start or will not finish
Work from the outside in. kubectl get pods shows status and restart counts; Pending usually means no node has room for the requests, CrashLoopBackOff means the container keeps exiting, and ImagePullBackOff means the image cannot be fetched. kubectl describe pod shows the events behind each of those, including OOMKilled as a termination reason. kubectl logs with --previous shows the output of the container before its last restart, which is where a crash’s real error usually is. Cluster events sorted by time give the order in which things went wrong across the namespace. Finally, check that kubectl itself is within one minor version of the cluster, which is the supported skew.
kubectl get pods -n pr-123 # status and restart counts
kubectl describe pod shop-7d9c-abcde -n pr-123 # events, OOMKilled, pull errors
kubectl logs shop-7d9c-abcde -n pr-123 --previous # output before the last restart
kubectl get events -n pr-123 --sort-by=.lastTimestamp
kubectl version # client and server versionsPitfalls
Common pitfalls
Shared test clusters without resource requests
Why it happens: The scheduler places pods by their requests. Without them, browser and application pods pile onto the same busy nodes, and timing-sensitive tests fail at random.
The fix: Set CPU and memory requests on every test and browser pod, set memory limits to catch leaks, and add a ResourceQuota per test namespace.
Preview environments that never get deleted
Why it happens: Deletion is triggered by a pull request closing, and that workflow occasionally fails or is skipped, so namespaces accumulate and keep their requested capacity reserved.
The fix: Label preview namespaces, run a scheduled clean-up that deletes any older than a set age, and set ttlSecondsAfterFinished on every test Job.
Leaving backoffLimit at its default for test Jobs
Why it happens: The default of 6 retries means a failing suite is rerun several times, and a flaky one eventually passes, so the Job reports success while hiding the flakiness.
The fix: Set backoffLimit to 0 or 1, or backoffLimitPerIndex for sharded Jobs, and let the test runner handle retries where it can report them.
Waiting on a Job that has already failed
Why it happens: kubectl wait --for=condition=complete waits for success only. A failed Job never meets that condition, so the pipeline sits until the timeout expires.
The fix: Set activeDeadlineSeconds and a matching wait timeout, or loop over waits for both complete and failed conditions, as the kubectl wait reference shows for pods.
Following tutorials written for older Kubernetes versions
Why it happens: Many articles predate the removal of dockershim in Kubernetes 1.24 and of PodSecurityPolicy in 1.25, so they tell you to configure Docker as the node runtime or apply PodSecurityPolicy objects that no longer exist.
The fix: Nodes now use a CRI runtime such as containerd, and images built with Docker still run unchanged. Use Pod Security Admission in place of PodSecurityPolicy, and check any tutorial against the docs for your minor version.
Chrome pods crashing with small shared memory
Why it happens: Browser containers need more /dev/shm than the container default, the same problem Docker users solve with --shm-size, but pod specs have no shm_size field.
The fix: Use the Selenium Helm chart, which mounts a memory-backed /dev/shm when dshmVolumeSizeLimit is set and otherwise passes Chrome the --disable-dev-shm-usage flag, or mount an emptyDir volume with medium: Memory at /dev/shm in your own browser pods.
Compared
Kubernetes alternatives compared
| Tool | Scope | Recovery from failure | Scaling | Local setup | Licence | Typical test role |
|---|---|---|---|---|---|---|
| Kubernetes | Cluster of many machines | Controllers restart and reschedule pods | Replicas, autoscalers and parallel Jobs | kind, or Docker Desktop’s built-in cluster | Apache 2.0 | Per-branch environments, scaled grids, load generators |
| Docker | One host | Restart policies on one host | docker compose up --scale on one host | Docker Desktop or Docker Engine | Apache 2.0 engine; Desktop by subscription | Local and CI test environments |
| Podman | One host | Restart policies on one host | No autoscaling; runs Kubernetes YAML locally | Podman or Podman Desktop | Apache 2.0 | Rootless local environments, Kubernetes YAML without a cluster |
FAQ
Frequently asked questions
What are the best Kubernetes alternatives?
The alternatives most often weighed against Kubernetes are Docker and Podman. The comparison on this page sets them side by side on scope, recovery from failure, scaling, local setup, licence and typical test role, and the verdict above says where Kubernetes is the wrong choice and which of them to pick instead.
Is Kubernetes free?
The software is free and open source under Apache 2.0, with no paid edition. Running it costs money: the machines that act as nodes, storage and load balancers, and on some managed services a per-cluster fee, such as Amazon EKS’s hourly charge. Azure AKS offers a Free tier with free cluster management for development and test. Local clusters with kind cost nothing beyond the machine.
Do testers need to learn Kubernetes?
If the application under test runs on Kubernetes, enough to read manifests, find pods and read logs and events is valuable, because test environments and failures will live there. Writing Helm charts and operating clusters is a platform skill most testers do not need. Running a test Job, debugging a pod that will not start and understanding resource requests cover most day-to-day test work.
Kubernetes or Docker Compose for test environments?
Docker Compose is simpler and enough when one machine can hold the whole environment. Kubernetes becomes worth it when environments must exist per branch on shared infrastructure, when the application already ships as Kubernetes manifests so tests can reuse them, or when a Selenium Grid or load test must spread across many machines. Both run the same container images.
Can I run Selenium Grid on Kubernetes?
Yes. The docker-selenium project publishes an official Helm chart, installed from the docker-selenium repository at selenium.dev, that deploys the Grid components and Chrome, Firefox and Edge nodes. Its autoscaling option uses KEDA to add browser nodes as sessions queue and remove them when idle, by default as single-session Jobs so each test gets a fresh browser.
Does Kubernetes still support Docker images?
Yes. Kubernetes 1.24 removed dockershim, the component that let nodes use Docker Engine as their runtime, and nodes now use CRI runtimes such as containerd or CRI-O. Images built with docker build are standard OCI images and run unchanged. What changed is only what runs containers on the nodes, which matters to cluster operators rather than to people building test images.
Which Kubernetes version is current, and how long is it supported?
Kubernetes 1.37, released on 26 August 2026, is the newest minor version, with 1.37.1 as the latest patch. The project maintains the three most recent minor releases, and each gets roughly a year of patch support, so 1.34 reaches end of life on 27 October 2026. kubectl is supported within one minor version of the cluster’s API server.
How do I run tests inside a Kubernetes cluster?
Package the suite in a container image and run it as a Job: the pod runs the test command, and the Job records success or failure. Set restartPolicy to Never, a low backoffLimit, an activeDeadlineSeconds and ttlSecondsAfterFinished. Then wait for the Job to complete and read its logs. For parallel runs, use an Indexed Job and pass JOB_COMPLETION_INDEX to the test runner’s shard option.
Alternatives
Kubernetes alternatives worth weighing
Resources
Where to go next
Picking the wrong tool costs more than picking late.
QAble advises on toolchain decisions and then implements them, with engineers who have run these tools on real delivery. Start with a free QA audit.