Apache JMeter 5.6.3 with the Plugins Manager and common jp@gc plugins baked in.
Two roles:
- Workers run the
jmeter-workerimage, which startsjmeter-serveron1099/tcpand50000/tcp. - The main controller runs the
jmeter-baseimage, publishes60000/tcp, and launches the distributed test against the workers.
Plugins are installed at build time by the PluginsManagerCMD.sh install … line in the Dockerfile — a comma-separated list of plugin IDs from the jp@gc catalog.
- Change the bundled set: edit that
installlist in the Dockerfile (useid=versionto pin), then rebuild.jmeter-workerbuilds onjmeter-base, so both get the same plugins. - Find plugin IDs: browse the catalog, or run
PluginsManagerCMD.sh status/availablein a container (see the CLI docs). - Add plugins without rebuilding: in your own Dockerfile,
FROM jmeter-docker:jmeter-base-5.6.3, then asrootrun"${JMETER_HOME}/bin/PluginsManagerCMD.sh" install your-id(andchown -R jmeter:jmeter "${JMETER_HOME}"), then switch back toUSER jmeter.
export DOCKER_BUILDKIT=1
docker compose build
# …or with Bake:
docker buildx bake
# …or a single target:
docker build --pull --target jmeter-base -t bsmithio/jmeter-base:latest .
docker build --pull --target jmeter-worker -t bsmithio/jmeter-worker:latest .Compose and Bake tag the images jmeter-docker:jmeter-base-<version> and jmeter-docker:jmeter-worker-<version> — the tags used in the examples below.
To change the JMeter version, update JMETER_VERSION in each of:
- docker-compose.yml (
x-jmeter-version) - docker-bake.hcl (
variable "JMETER_VERSION") - Dockerfile (
ARG JMETER_VERSIONdefault) - .github/workflows/docker-image.yml (
build-args/ tags)
Also refresh the .sha512 file when the archive version changes.
docker build --pull --target jmeter-base \
--build-arg JMETER_VERSION=5.6.3 \
-t my/jmeter-base:5.6.3 .docker run --detach --name main-controller \
-p 60000:60000 \
jmeter-docker:jmeter-base-5.6.3Set LOCALIP to the address the controller uses to reach each worker.
docker run --detach \
--name worker1 \
-e LOCALIP='192.168.1.101' \
-p 1099:1099 -p 50000:50000 \
jmeter-docker:jmeter-worker-5.6.3
docker run --detach \
--name worker2 \
-e LOCALIP='192.168.1.102' \
-p 1099:1099 -p 50000:50000 \
jmeter-docker:jmeter-worker-5.6.3docker container ls --format '{{.Names}}\t{{.Status}}'${JMETER_HOME} is /opt/apache-jmeter-5.6.3 inside the image.
docker exec -i main-controller sh -c 'cat > "${JMETER_HOME}/bin/test.jmx"' < test.jmxSet CONTROLLER_IP, CLIENT_PORT, and the worker IPs for your environment.
docker exec -it main-controller "${JMETER_HOME}/bin/jmeter" \
-n \
-t "${JMETER_HOME}/bin/test.jmx" \
-Djava.rmi.server.hostname="${CONTROLLER_IP}" \
-Dclient.rmi.localport="${CLIENT_PORT}" \
-R "${WORKER1_IP},${WORKER2_IP}"docker exec -it main-controller bash # controller
docker exec -it worker1 bash # worker
⚠️ By default, workers require no authentication and use no encryption. They run any test plan they receive, and test plans can run arbitrary code (JSR223/Groovy, OS Process samplers). Anyone who can reach1099/tcpor50000/tcpcan run code inside the worker. Never expose these ports to the public internet — never0.0.0.0/0. Restrict them to the controller and workers, prefer a private network (VPC/VPN), and setLOCALIPto a private address. To require authentication and encryption, enable RMI SSL.
Open only what you need, scoped to trusted sources:
- 22/tcp — SSH, restricted to your admin IPs
- 1099/tcp & 50000/tcp — workers, restricted to the controller and workers
- 60000/tcp — controller (
-Dclient.rmi.localport), restricted to the workers
docker run --detach --name main-controller \
--restart unless-stopped \
-p 60000:60000 \
jmeter-docker:jmeter-base-5.6.3The lookup below sets LOCALIP to the host's public IP — use it only if the controller reaches workers over public addresses. On a private network, use the private IP and bind ports to that interface (e.g. -p 10.0.0.5:1099:1099).
docker run --detach \
--name worker1 \
--restart unless-stopped \
-e LOCALIP="$(curl -fs https://checkip.amazonaws.com)" \
-p 1099:1099 -p 50000:50000 \
jmeter-docker:jmeter-worker-5.6.3Copy in test.jmx and run the test the same way as on your laptop.
To require authentication and encryption, give the controller and every worker the same keystore — a peer without it can't connect, which closes the code-execution risk noted above.
Generate it once and copy the same file to every host:
docker run --rm -v "$(pwd):/out" -w /out \
jmeter-docker:jmeter-base-5.6.3 \
sh -c '"${JMETER_HOME}/bin/create-rmi-keystore.sh" && cp rmi_keystore.jks /out/'This writes rmi_keystore.jks to the current directory; note the password you set (JMeter's default is changeit). The same file and password must go to the controller and all workers — a per-host keystore won't work.
Mount it read-only and set RMI_KEYSTORE and RMI_KEYSTORE_PASSWORD:
docker run --detach \
--name worker1 \
--restart unless-stopped \
-e LOCALIP='192.168.1.101' \
-e RMI_KEYSTORE=/rmi_keystore.jks \
-e RMI_KEYSTORE_PASSWORD='changeit' \
-v "$(pwd)/rmi_keystore.jks:/rmi_keystore.jks:ro" \
-p 1099:1099 -p 50000:50000 \
jmeter-docker:jmeter-worker-5.6.3Env vars: RMI_KEYSTORE (path in the container; setting it enables SSL), RMI_KEYSTORE_PASSWORD (required), RMI_KEYSTORE_TYPE (optional, default JKS). The password is read from a private file, not the command line, so it never appears in the process list.
Mount the keystore and pass the SSL properties on the run:
docker run --detach --name main-controller \
--restart unless-stopped \
-p 60000:60000 \
-v "$(pwd)/rmi_keystore.jks:/rmi_keystore.jks:ro" \
jmeter-docker:jmeter-base-5.6.3
docker exec -it main-controller "${JMETER_HOME}/bin/jmeter" \
-n -t "${JMETER_HOME}/bin/test.jmx" \
-Djava.rmi.server.hostname="${CONTROLLER_IP}" \
-Dclient.rmi.localport="${CLIENT_PORT}" \
-Jserver.rmi.ssl.disable=false \
-Jserver.rmi.ssl.keystore.file=/rmi_keystore.jks \
-Jserver.rmi.ssl.keystore.password='changeit' \
-Jserver.rmi.ssl.truststore.file=/rmi_keystore.jks \
-Jserver.rmi.ssl.truststore.password='changeit' \
-R "${WORKER1_IP},${WORKER2_IP}"Keep the firewall rules even with SSL on. To rotate the keystore, regenerate it and redistribute to every host at once.