Running JMeter in Docker and Kubernetes solves three problems that plague on-premise distributed testing: environment consistency, scalability on demand, and CI/CD integration. The same container image that runs a smoke test on your laptop runs a 1,000-user load test in a CI/CD pipeline — same JMeter version, same plugins, same configuration.
Why containerise JMeter
Consistency. JMeter is a Java application with version-specific behaviour and plugin dependencies. A test plan built against JMeter 5.6.3 with the Custom Thread Groups plugin may behave differently on 5.5.0 without the plugin. A Docker image locks the exact version, plugins, and Java runtime into a reproducible unit.
Scalability. Spinning up five JMeter worker containers on demand and tearing them down after the test is cheaper and faster than maintaining five dedicated VMs. Kubernetes handles the scheduling automatically.
CI/CD native. Every CI system that runs Docker can run JMeter tests without installing Java or JMeter locally. The pipeline pulls the image and runs the test.
Docker for single-node tests
The community-maintained justb4/jmeter image is the most widely used. It accepts the same CLI flags as a local JMeter installation:
docker run --rm \
-v $(pwd)/test-plans:/jmeter/test-plans \
-v $(pwd)/data:/jmeter/data \
-v $(pwd)/results:/jmeter/results \
justb4/jmeter:5.6.3 \
-n \
-t /jmeter/test-plans/load-test.jmx \
-l /jmeter/results/results.jtl \
-e -o /jmeter/results/report \
-JbaseUrl=https://api.example.com-v mounts local directories into the container. The test plan and data files go in; the results come out. The container exits when the test completes and is removed with --rm.
Custom Docker image
For team use, bake the test plans and data into the image rather than mounting volumes at run time:
FROM justb4/jmeter:5.6.3
# Copy all test artefacts into the image
COPY test-plans/ /jmeter/test-plans/
COPY data/ /jmeter/data/
COPY plugins/ /jmeter/plugins/
# Install custom plugins (drop JARs into lib/ext/)
RUN cp /jmeter/plugins/*.jar /opt/apache-jmeter-5.6.3/lib/ext/
# Default command — override via docker run arguments
ENTRYPOINT ["/entrypoint.sh"]Build and push to your registry:
docker build -t company/jmeter-tests:1.2.0 .
docker push company/jmeter-tests:1.2.0Now any team member or CI system runs the pinned version of your tests with a single docker run command, no local setup required.
Kubernetes for distributed testing
Step 1 of 6
Trigger the test
A CI/CD pipeline or operator creates a Kubernetes Job for the JMeter master and a Deployment for the worker pods. The master image contains the test plan; workers run the jmeter-server binary.
Master Kubernetes Job
apiVersion: batch/v1
kind: Job
metadata:
name: jmeter-master
spec:
template:
spec:
restartPolicy: Never
containers:
- name: jmeter-master
image: company/jmeter-tests:1.2.0
args:
- "-n"
- "-t"
- "/jmeter/test-plans/load-test.jmx"
- "-l"
- "/results/results.jtl"
- "-e"
- "-o"
- "/results/report"
- "-R"
- "$(WORKER_IPS)" # injected by init container or env
- "-X"
env:
- name: BASE_URL
value: "https://api.staging.example.com"
volumeMounts:
- name: results-volume
mountPath: /results
volumes:
- name: results-volume
persistentVolumeClaim:
claimName: jmeter-results-pvcWorker Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: jmeter-workers
spec:
replicas: 5 # 5 workers × Thread Group threads = total VUs
selector:
matchLabels:
app: jmeter-worker
template:
spec:
containers:
- name: jmeter-worker
image: company/jmeter-tests:1.2.0
command: ["/opt/apache-jmeter-5.6.3/bin/jmeter-server"]
args:
- "-Djava.rmi.server.hostname=$(POD_IP)"
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
ports:
- containerPort: 1099
name: rmiHeadless Service for worker discovery
apiVersion: v1
kind: Service
metadata:
name: jmeter-workers
spec:
clusterIP: None # headless — returns individual pod IPs
selector:
app: jmeter-worker
ports:
- port: 1099
name: rmiA headless Service (clusterIP: None) returns the IP addresses of all matching pods on DNS lookup. The master can resolve jmeter-workers.default.svc.cluster.local to get all worker IPs.
Results storage
Test results need to survive beyond pod lifecycle. Options:
PersistentVolumeClaim — a shared volume that all pods can mount. The master writes results.jtl here; it persists after pods terminate.
Object storage (S3/GCS) — a JSR223 Post-Processor or a post-test script uploads the .jtl and HTML report to a storage bucket at test end. Teams access results via a storage browser URL.
InfluxDB + Grafana — a Backend Listener streams metrics to InfluxDB during the test. Results are stored in InfluxDB and visualised in Grafana. Pod termination does not affect stored metrics.
GitHub Actions pipeline example
name: Load Test
on:
workflow_dispatch:
inputs:
environment:
type: choice
options: [staging, production]
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run JMeter
run: |
docker run --rm \
-v ${{ github.workspace }}/test-plans:/jmeter/test-plans \
-v ${{ github.workspace }}/data:/jmeter/data \
-v ${{ github.workspace }}/results:/jmeter/results \
justb4/jmeter:5.6.3 \
-n \
-t /jmeter/test-plans/load-test.jmx \
-l /jmeter/results/results.jtl \
-e -o /jmeter/results/report \
-JbaseUrl=${{ vars[format('{0}_BASE_URL', inputs.environment)] }}
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: jmeter-report
path: results/report/⚠️ Common mistakes
- Not setting
-Djava.rmi.server.hostname=$(POD_IP)on worker pods. Kubernetes pods have internal IPs that change on restart. Without explicitly binding the RMI server to the pod's IP, the worker registers the wrong hostname with the RMI registry and the master cannot complete the callback connection. ThePOD_IPfield reference via the Downward API is the standard fix. - Using a regular Kubernetes Service (with ClusterIP) instead of a headless Service for workers. A regular Service load-balances traffic to one pod at a time. The master needs to connect to each worker individually for RMI. A headless Service returns all pod IPs in DNS, allowing the master to connect to each worker directly.
- Forgetting that workers need the same test plan files. When the master pod contains the
.jmxfile but the worker pods do not, the master sends the test plan over the RMI connection — but referenced files (CSV data, external scripts) are not automatically transferred. Either bake all files into the image, use a shared volume, or use an init container to copy files from the master before the test starts.
🎯 Practice task
Containerise your JMeter test and run it via Docker.
- Install Docker locally if not already installed.
- Run your existing test plan in Docker using the
justb4/jmeter:5.6.3image. Mount your local directory and run the test:docker run --rm \ -v $(pwd):/jmeter \ justb4/jmeter:5.6.3 \ -n -t /jmeter/test.jmx \ -l /jmeter/results-docker.jtl - Confirm
results-docker.jtlis written to your local directory (via the volume mount). - Write a minimal
Dockerfilethat usesjustb4/jmeter:5.6.3as the base andCOPYs your test plan and data files in. Build it:docker build -t my-jmeter-tests:latest . - Run the custom image:
docker run --rm -v $(pwd)/results:/results my-jmeter-tests:latest -n -t /jmeter/test.jmx -l /results/run.jtl. Confirm the results appear in your localresults/directory. - Generate the HTML report from the container output:
jmeter -g results/run.jtl -o results/report/. Open it in a browser.