JMeter in Docker and Kubernetes

8 min read

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

Now 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-pvc

Worker 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: rmi

Headless 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: rmi

A 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. The POD_IP field 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 .jmx file 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.

  1. Install Docker locally if not already installed.
  2. Run your existing test plan in Docker using the justb4/jmeter:5.6.3 image. 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
  3. Confirm results-docker.jtl is written to your local directory (via the volume mount).
  4. Write a minimal Dockerfile that uses justb4/jmeter:5.6.3 as the base and COPYs your test plan and data files in. Build it: docker build -t my-jmeter-tests:latest .
  5. 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 local results/ directory.
  6. Generate the HTML report from the container output: jmeter -g results/run.jtl -o results/report/. Open it in a browser.

// tip to track lessons you complete and pick up where you left off across devices.