From 839a1a98632c3734b5b135580c16f97ca5c0fe91 Mon Sep 17 00:00:00 2001 From: smithbr <1495361+smithbr@users.noreply.github.com> Date: Fri, 29 May 2026 12:11:48 -0700 Subject: [PATCH 1/7] Replace legacy Dockerfiles with multi-stage JMeter 5.6 build Consolidate base and worker images into one Dockerfile on Eclipse Temurin 21, verify Apache archives with sha512, install jp@gc plugins at build time, and add a worker entrypoint that runs jmeter-server with LOCALIP. --- .dockerignore | 5 ++ Dockerfile | 70 ++++++++++++++++++++++ jmeter-base/Dockerfile | 40 ------------- jmeter-master/Dockerfile | 12 ---- jmeter-slave/Dockerfile | 9 --- scripts/docker-entrypoint-jmeter-worker.sh | 12 ++++ 6 files changed, 87 insertions(+), 61 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile delete mode 100644 jmeter-base/Dockerfile delete mode 100644 jmeter-master/Dockerfile delete mode 100644 jmeter-slave/Dockerfile create mode 100755 scripts/docker-entrypoint-jmeter-worker.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f67132a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +.gitignore +*.md +test.jmx +docker-compose*.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..822d66c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,70 @@ +# syntax=docker/dockerfile:1 + +# Pin digest = multi-arch manifest list for reproducible builds (bump when retagging base). +# Resolve: docker buildx imagetools inspect eclipse-temurin:21-jre-jammy --format '{{json .Manifest}}' +FROM eclipse-temurin:21-jre-jammy@sha256:199aebeb3adcde4910695cdebfe782ada38dadb6cc8013159b58d3724451befd AS jmeter-base + +ARG JMETER_VERSION=5.6.3 +ARG JMETER_UID=10001 +ARG JMETER_GID=10001 + +LABEL org.opencontainers.image.title="Apache JMeter — base image" +LABEL org.opencontainers.image.description="JMeter 5.x with JMeter Plugins Manager and common jp@gc plugins (load shaping, PerfMon, JTL graphs, merge)" +LABEL org.opencontainers.image.licenses="Apache-2.0" +LABEL org.opencontainers.image.url="https://jmeter.apache.org/" + +ENV JMETER_HOME=/opt/apache-jmeter-${JMETER_VERSION} \ + PATH=${JMETER_HOME}/bin:${PATH} + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ + apt-get update \ + && apt-get install -y --no-install-recommends bash ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* + +RUN set -eux; \ + base="https://archive.apache.org/dist/jmeter/binaries"; \ + tgz="${base}/apache-jmeter-${JMETER_VERSION}.tgz"; \ + curl -fSL --retry 3 -o "/tmp/apache-jmeter-${JMETER_VERSION}.tgz" "${tgz}"; \ + curl -fSL --retry 3 -o "/tmp/apache-jmeter-${JMETER_VERSION}.tgz.sha512" "${tgz}.sha512"; \ + (cd /tmp && sha512sum -c "/tmp/apache-jmeter-${JMETER_VERSION}.tgz.sha512"); \ + tar -xzf "/tmp/apache-jmeter-${JMETER_VERSION}.tgz" -C /opt \ + && rm "/tmp/apache-jmeter-${JMETER_VERSION}.tgz" "/tmp/apache-jmeter-${JMETER_VERSION}.tgz.sha512"; \ + rm -rf "${JMETER_HOME}/printable_docs" "${JMETER_HOME}/docs" + +# JMeter Plugins Manager + cmdrunner (required installer dependency) +RUN set -eux; \ + curl -fSL --retry 3 -o "${JMETER_HOME}/lib/ext/plugins-manager.jar" \ + "https://jmeter-plugins.org/get/"; \ + curl -fSL --retry 3 -o "${JMETER_HOME}/lib/cmdrunner-2.3.jar" \ + "https://repo1.maven.org/maven2/kg/apc/cmdrunner/2.3/cmdrunner-2.3.jar"; \ + java -cp "${JMETER_HOME}/lib/ext/plugins-manager.jar" \ + org.jmeterplugins.repository.PluginManagerCMDInstaller + +# Plugin IDs match https://jmeter-plugins.org/repo/ (see PluginsManagerCMD install). +RUN "${JMETER_HOME}/bin/PluginsManagerCMD.sh" install \ + jpgc-autostop,jpgc-casutg,jpgc-cmd,jpgc-csl,jpgc-dummy,jpgc-ffw,jpgc-functions,jpgc-graphs-dist,jpgc-json,jpgc-mergeresults,jpgc-perfmon,jpgc-tst \ + && "${JMETER_HOME}/bin/PluginsManagerCMD.sh" status + +RUN groupadd --gid "${JMETER_GID}" jmeter \ + && useradd --uid "${JMETER_UID}" --gid jmeter --create-home --shell /bin/bash jmeter \ + && mkdir -p /jmeter-work \ + && chown -R jmeter:jmeter "${JMETER_HOME}" /jmeter-work + +WORKDIR /jmeter-work +USER jmeter + +# --- Worker (remote execution engine via jmeter-server) --------------------- +FROM jmeter-base AS jmeter-worker + +USER root + +COPY scripts/docker-entrypoint-jmeter-worker.sh /usr/local/bin/docker-entrypoint-jmeter-worker.sh +RUN chmod +x /usr/local/bin/docker-entrypoint-jmeter-worker.sh \ + && chown root:root /usr/local/bin/docker-entrypoint-jmeter-worker.sh + +USER jmeter + +EXPOSE 1099 50000 + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint-jmeter-worker.sh"] diff --git a/jmeter-base/Dockerfile b/jmeter-base/Dockerfile deleted file mode 100644 index da84cae..0000000 --- a/jmeter-base/Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -FROM ubuntu -LABEL maintainer="Brandon Smith " - -RUN apt-get update && \ - apt-get install -y \ - default-jre \ - curl - -RUN useradd -ms /bin/bash jmeter -USER jmeter -WORKDIR /home/jmeter - -RUN curl https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-3.2.tgz > \ - /home/jmeter/apache-jmeter-3.2.tgz && \ - tar -xvzf /home/jmeter/apache-jmeter-3.2.tgz && \ - rm /home/jmeter/apache-jmeter-3.2.tgz - -ENV JMETER_HOME /home/jmeter/apache-jmeter-3.2/ -ENV PATH $JMETER_HOME/bin:$PATH - -RUN curl -L https://jmeter-plugins.org/get/ > \ - $JMETER_HOME/lib/ext/plugins-manager.jar && \ - curl -L http://search.maven.org/remotecontent?filepath=kg/apc/cmdrunner/2.0/cmdrunner-2.0.jar > \ - $JMETER_HOME/lib/cmdrunner-2.0.jar && \ - java -cp $JMETER_HOME/lib/ext/plugins-manager.jar \ - org.jmeterplugins.repository.PluginManagerCMDInstaller - -# jpgc-casutg = https://jmeter-plugins.org/wiki/ConcurrencyThreadGroup/ -# jpgc-tst = https://jmeter-plugins.org/wiki/ThroughputShapingTimer/ -# jpgc-ffw = https://jmeter-plugins.org/wiki/FlexibleFileWriter/ -# jpgc-csl = https://jmeter-plugins.org/wiki/ConsoleStatusLogger/ -# jpgc-autostop = https://jmeter-plugins.org/wiki/AutoStop/ -# jpgc-functions = https://jmeter-plugins.org/wiki/Functions/ -# jpgc-dummy = https://jmeter-plugins.org/wiki/DummySampler/ -# jpgc-json = https://jmeter-plugins.org/wiki/JSONPathExtractor/ -# jpgc-sense = https://github.com/Blazemeter/jmeter-bzm-plugins/blob/master/sense-uploader/BlazemeterPlugin.md -RUN $JMETER_HOME/bin/PluginsManagerCMD.sh install \ - jpgc-casutg,jpgc-tst,jpgc-ffw,jpgc-csl,jpgc-autostop,jpgc-functions,jpgc-dummy,jpgc-json,jpgc-sense - -RUN $JMETER_HOME/bin/PluginsManagerCMD.sh status diff --git a/jmeter-master/Dockerfile b/jmeter-master/Dockerfile deleted file mode 100644 index f1322da..0000000 --- a/jmeter-master/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM bsmithio/jmeter-base -LABEL maintainer="Brandon Smith " - -RUN curl http://archive.apache.org/dist/maven/maven-3/3.5.0/binaries/apache-maven-3.5.0-bin.tar.gz > \ - /home/jmeter/apache-maven-3.5.0-bin.tar.gz && \ - tar -xvzf /home/jmeter/apache-maven-3.5.0-bin.tar.gz && \ - rm /home/jmeter/apache-maven-3.5.0-bin.tar.gz - -ENV M2_HOME=/home/jmeter/apache-maven-3.5.0/ -ENV PATH $M2_HOME/bin:$PATH - -EXPOSE 60000 diff --git a/jmeter-slave/Dockerfile b/jmeter-slave/Dockerfile deleted file mode 100644 index fc0434d..0000000 --- a/jmeter-slave/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM bsmithio/jmeter-base -LABEL maintainer="Brandon Smith " - -EXPOSE 1099 50000 - -ENTRYPOINT $JMETER_HOME/bin/jmeter-server \ - -Dserver.rmi.localport=50000 \ - -Dserver_port=1099 \ - -Djava.rmi.server.hostname=$LOCALIP diff --git a/scripts/docker-entrypoint-jmeter-worker.sh b/scripts/docker-entrypoint-jmeter-worker.sh new file mode 100755 index 0000000..0936d26 --- /dev/null +++ b/scripts/docker-entrypoint-jmeter-worker.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -eu + +if [ -z "${LOCALIP:-}" ]; then + printf '%s\n' "LOCALIP must be set to the address other hosts use to reach this container (typically the host LAN/WAN IP that maps to TCP 1099/50000)." >&2 + exit 1 +fi + +exec "${JMETER_HOME}/bin/jmeter-server" \ + -Dserver.rmi.localport=50000 \ + -Dserver_port=1099 \ + "-Djava.rmi.server.hostname=${LOCALIP}" From c6d2af437453dff529b11a22e247b39b77359f07 Mon Sep 17 00:00:00 2001 From: smithbr <1495361+smithbr@users.noreply.github.com> Date: Sun, 7 Jun 2026 16:49:12 -0700 Subject: [PATCH 2/7] Pin and checksum Plugins Manager and cmdrunner jars Fetch both from Maven Central (immutable, versioned) instead of the rolling jmeter-plugins.org/get/ "latest" endpoint, so each jar can be SHA512-verified at build time like the JMeter archive. --- Dockerfile | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 822d66c..e238d5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,16 @@ ARG JMETER_VERSION=5.6.3 ARG JMETER_UID=10001 ARG JMETER_GID=10001 +# Plugins Manager + its required cmdrunner dependency. Pulled from Maven Central (immutable, +# versioned) so both can be pinned + verified like the JMeter archive — unlike the rolling +# jmeter-plugins.org/get/ "latest" endpoint, which can't be checksummed. +# On bump, recompute: curl -fsSL | sha512sum +# (Maven Central only publishes .sha1/.md5, so derive the SHA512 from the jar itself.) +ARG PLUGINS_MGR_VERSION=1.12 +ARG PLUGINS_MGR_SHA512=b65c919512858a438b7be5f93f60888cfab4c59def843c2e987242bce36a9b09b3644e59c4f7f8e15a132b1fda2e39c16d93db8c48cc5d497b5c899f0b44300f +ARG CMDRUNNER_VERSION=2.3 +ARG CMDRUNNER_SHA512=7f71fe42f4ead4ccddd68148e97a46b9262bdb05fe5e590a725331513549122dc64d4cb524635b2f0e3e7d3ee4bb3c2807738cd3c7e2d0d7a503ea78234dab51 + LABEL org.opencontainers.image.title="Apache JMeter — base image" LABEL org.opencontainers.image.description="JMeter 5.x with JMeter Plugins Manager and common jp@gc plugins (load shaping, PerfMon, JTL graphs, merge)" LABEL org.opencontainers.image.licenses="Apache-2.0" @@ -34,10 +44,13 @@ RUN set -eux; \ # JMeter Plugins Manager + cmdrunner (required installer dependency) RUN set -eux; \ + maven="https://repo1.maven.org/maven2"; \ curl -fSL --retry 3 -o "${JMETER_HOME}/lib/ext/plugins-manager.jar" \ - "https://jmeter-plugins.org/get/"; \ - curl -fSL --retry 3 -o "${JMETER_HOME}/lib/cmdrunner-2.3.jar" \ - "https://repo1.maven.org/maven2/kg/apc/cmdrunner/2.3/cmdrunner-2.3.jar"; \ + "${maven}/kg/apc/jmeter-plugins-manager/${PLUGINS_MGR_VERSION}/jmeter-plugins-manager-${PLUGINS_MGR_VERSION}.jar"; \ + printf '%s %s\n' "${PLUGINS_MGR_SHA512}" "${JMETER_HOME}/lib/ext/plugins-manager.jar" | sha512sum -c -; \ + curl -fSL --retry 3 -o "${JMETER_HOME}/lib/cmdrunner-${CMDRUNNER_VERSION}.jar" \ + "${maven}/kg/apc/cmdrunner/${CMDRUNNER_VERSION}/cmdrunner-${CMDRUNNER_VERSION}.jar"; \ + printf '%s %s\n' "${CMDRUNNER_SHA512}" "${JMETER_HOME}/lib/cmdrunner-${CMDRUNNER_VERSION}.jar" | sha512sum -c -; \ java -cp "${JMETER_HOME}/lib/ext/plugins-manager.jar" \ org.jmeterplugins.repository.PluginManagerCMDInstaller From 8e7c279bef14df68fe8b0b5655df11e383639faf Mon Sep 17 00:00:00 2001 From: smithbr <1495361+smithbr@users.noreply.github.com> Date: Sun, 7 Jun 2026 16:49:16 -0700 Subject: [PATCH 3/7] Add Compose, Bake, and CI build definitions Define both image targets once with JMETER_VERSION centralized per file, and add a GitHub Actions matrix that builds each target and smoke-checks JMeter. --- .github/workflows/docker-image.yml | 41 ++++++++++++++++++++++++++++++ docker-bake.hcl | 32 +++++++++++++++++++++++ docker-compose.yml | 25 ++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 .github/workflows/docker-image.yml create mode 100644 docker-bake.hcl create mode 100644 docker-compose.yml diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..0559dde --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,41 @@ +name: Docker image + +on: + push: + branches: [master, main] + pull_request: + branches: [master, main] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + target: [jmeter-base, jmeter-worker] + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build ${{ matrix.target }} + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + target: ${{ matrix.target }} + push: false + load: true + tags: jmeter-docker:${{ matrix.target }}-ci + build-args: | + JMETER_VERSION=5.6.3 + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Smoke check JMeter + if: matrix.target == 'jmeter-base' + run: docker run --rm jmeter-docker:jmeter-base-ci jmeter --version diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 0000000..4cb1ac2 --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,32 @@ +// Central build definitions (align JMETER_VERSION with docker-compose.yml x-jmeter-version). +variable "JMETER_VERSION" { + default = "5.6.3" +} + +variable "IMAGE_PREFIX" { + default = "jmeter-docker" +} + +group "default" { + targets = ["jmeter-base", "jmeter-worker"] +} + +target "jmeter-base" { + context = "." + dockerfile = "Dockerfile" + target = "jmeter-base" + args = { + JMETER_VERSION = JMETER_VERSION + } + tags = ["${IMAGE_PREFIX}:jmeter-base-${JMETER_VERSION}"] +} + +target "jmeter-worker" { + context = "." + dockerfile = "Dockerfile" + target = "jmeter-worker" + args = { + JMETER_VERSION = JMETER_VERSION + } + tags = ["${IMAGE_PREFIX}:jmeter-worker-${JMETER_VERSION}"] +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..390dd92 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +# Compose wrapper around `docker build` targets. JMETER_VERSION is defined once (x-anchors). + +x-jmeter-version: &jmeter-version + JMETER_VERSION: "5.6.3" + +x-build-defaults: &build-defaults + context: . + dockerfile: Dockerfile + +services: + jmeter-base: + build: + <<: *build-defaults + target: jmeter-base + args: + <<: *jmeter-version + image: jmeter-docker:jmeter-base-5.6.3 + + jmeter-worker: + build: + <<: *build-defaults + target: jmeter-worker + args: + <<: *jmeter-version + image: jmeter-docker:jmeter-worker-5.6.3 From dfee85b10db43c43b05dcf954221ea5cf1392996 Mon Sep 17 00:00:00 2001 From: smithbr <1495361+smithbr@users.noreply.github.com> Date: Sun, 7 Jun 2026 16:49:21 -0700 Subject: [PATCH 4/7] Add optional RMI SSL to worker entrypoint Enable authenticated, encrypted distributed testing when a shared keystore is mounted via RMI_KEYSTORE. SSL settings are written to a 0600 properties file so the keystore password never appears in the process list. Without a keystore the engine runs plaintext as before, now with an explicit warning. --- scripts/docker-entrypoint-jmeter-worker.sh | 55 ++++++++++++++++++++-- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/scripts/docker-entrypoint-jmeter-worker.sh b/scripts/docker-entrypoint-jmeter-worker.sh index 0936d26..cc1849e 100755 --- a/scripts/docker-entrypoint-jmeter-worker.sh +++ b/scripts/docker-entrypoint-jmeter-worker.sh @@ -6,7 +6,56 @@ if [ -z "${LOCALIP:-}" ]; then exit 1 fi -exec "${JMETER_HOME}/bin/jmeter-server" \ - -Dserver.rmi.localport=50000 \ - -Dserver_port=1099 \ +# Base RMI engine args. Build them with set -- so paths/values with spaces stay intact. +set -- \ + "-Dserver.rmi.localport=50000" \ + "-Dserver_port=1099" \ "-Djava.rmi.server.hostname=${LOCALIP}" + +# --- Optional RMI SSL (authenticated + encrypted distributed testing) -------- +# Opt in by mounting the SHARED keystore — the SAME rmi_keystore.jks the controller +# and every worker use — and pointing RMI_KEYSTORE at it. Generate it once with +# "${JMETER_HOME}/bin/create-rmi-keystore.sh" and distribute the identical file. +# +# docker run ... \ +# -v /path/to/rmi_keystore.jks:/rmi_keystore.jks:ro \ +# -e RMI_KEYSTORE=/rmi_keystore.jks \ +# -e RMI_KEYSTORE_PASSWORD=... \ +# ... +# +# Without RMI_KEYSTORE the engine runs in PLAINTEXT with NO authentication — only +# safe on a trusted network with 1099/50000 firewalled to known hosts. +if [ -n "${RMI_KEYSTORE:-}" ]; then + if [ ! -r "${RMI_KEYSTORE}" ]; then + printf '%s\n' "RMI_KEYSTORE is set to '${RMI_KEYSTORE}' but that file is missing or unreadable inside the container (check the volume mount and file permissions for the jmeter user)." >&2 + exit 1 + fi + if [ -z "${RMI_KEYSTORE_PASSWORD:-}" ]; then + printf '%s\n' "RMI_KEYSTORE is set but RMI_KEYSTORE_PASSWORD is empty; the keystore cannot be opened." >&2 + exit 1 + fi + + ks_type="${RMI_KEYSTORE_TYPE:-JKS}" + + # Write the SSL settings to a properties file (loaded via -q) instead of passing + # them as -D flags, so the keystore password never appears in the process list. + props="$(mktemp)" + chmod 600 "${props}" + { + printf '%s\n' "server.rmi.ssl.disable=false" + printf '%s\n' "server.rmi.ssl.keystore.file=${RMI_KEYSTORE}" + printf '%s\n' "server.rmi.ssl.keystore.type=${ks_type}" + printf '%s\n' "server.rmi.ssl.keystore.password=${RMI_KEYSTORE_PASSWORD}" + printf '%s\n' "server.rmi.ssl.truststore.file=${RMI_KEYSTORE}" + printf '%s\n' "server.rmi.ssl.truststore.type=${ks_type}" + printf '%s\n' "server.rmi.ssl.truststore.password=${RMI_KEYSTORE_PASSWORD}" + } > "${props}" + + set -- "$@" -q "${props}" + printf '%s\n' "RMI SSL enabled (keystore: ${RMI_KEYSTORE})." >&2 +else + printf '%s\n' "WARNING: RMI SSL is disabled — jmeter-server has NO authentication or encryption. Anyone who can reach TCP 1099/50000 can execute arbitrary code in this container. Set RMI_KEYSTORE (+ RMI_KEYSTORE_PASSWORD) to enable SSL, and keep these ports firewalled to trusted hosts only." >&2 + set -- "$@" "-Dserver.rmi.ssl.disable=true" +fi + +exec "${JMETER_HOME}/bin/jmeter-server" "$@" From 37b696e02b2943d5bf8064ab7d1d6a933813d27d Mon Sep 17 00:00:00 2001 From: smithbr <1495361+smithbr@users.noreply.github.com> Date: Sun, 7 Jun 2026 16:49:25 -0700 Subject: [PATCH 5/7] Update sample test plan to JMeter 5.6.3 format --- test.jmx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.jmx b/test.jmx index 77d8f2a..dc0e83e 100644 --- a/test.jmx +++ b/test.jmx @@ -1,5 +1,5 @@ - + From ced4c0611b29c7dbaec59b76c0ade87a6e6fad5b Mon Sep 17 00:00:00 2001 From: smithbr <1495361+smithbr@users.noreply.github.com> Date: Sun, 7 Jun 2026 16:49:25 -0700 Subject: [PATCH 6/7] Rewrite README for JMeter 5.6 and RMI SSL --- README.md | 200 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 141 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 8622269..b7bb660 100644 --- a/README.md +++ b/README.md @@ -1,122 +1,204 @@ # jmeter-docker -Docker images for JMeter 3.2 master and slave configurations +Apache [JMeter](https://jmeter.apache.org/) **5.6.3** with the [Plugins Manager](https://jmeter-plugins.org/wiki/PluginsManager/) and common **jp@gc** plugins baked in. -## On a local machine +Two roles: -### Bring up slaves +- **Workers** run the `jmeter-worker` image, which starts `jmeter-server` on `1099/tcp` and `50000/tcp`. +- The **main controller** runs the `jmeter-base` image, publishes `60000/tcp`, and launches the distributed test against the workers. -```bash -sudo docker run -dit --name slave1 bsmithio/jmeter-slave:latest /bin/bash && \ -sudo docker run -dit --name slave2 bsmithio/jmeter-slave:latest /bin/bash && \ -sudo docker run -dit --name slave3 bsmithio/jmeter-slave:latest /bin/bash -``` +## Bundled jp@gc plugins -### Bring up master +Plugins are installed at build time by the `PluginsManagerCMD.sh install …` line in the [Dockerfile](Dockerfile) — a comma-separated list of plugin IDs from the [jp@gc catalog](https://jmeter-plugins.org/repo/). -```bash -sudo docker run -dit --name master bsmithio/jmeter-master:latest /bin/bash -``` +- **Change the bundled set:** edit that `install` list in the [Dockerfile](Dockerfile) (use `id=version` to pin), then rebuild. `jmeter-worker` builds on `jmeter-base`, so both get the same plugins. +- **Find plugin IDs:** browse the [catalog](https://jmeter-plugins.org/repo/), or run `PluginsManagerCMD.sh status` / `available` in a container (see the [CLI docs](https://jmeter-plugins.org/wiki/PluginsManagerAutomated/)). +- **Add plugins without rebuilding:** in your own Dockerfile, `FROM jmeter-docker:jmeter-base-5.6.3`, then as `root` run `"${JMETER_HOME}/bin/PluginsManagerCMD.sh" install your-id` (and `chown -R jmeter:jmeter "${JMETER_HOME}"`), then switch back to `USER jmeter`. -### Check if they're all running +## Build ```bash -sudo docker ps -a +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 . ``` -### Get all of their IP addresses +Compose and Bake tag the images `jmeter-docker:jmeter-base-` and `jmeter-docker:jmeter-worker-` — the tags used in the examples below. + +To change the JMeter version, update `JMETER_VERSION` in each of: + +- [docker-compose.yml](docker-compose.yml) (`x-jmeter-version`) +- [docker-bake.hcl](docker-bake.hcl) (`variable "JMETER_VERSION"`) +- [Dockerfile](Dockerfile) (`ARG JMETER_VERSION` default) +- [.github/workflows/docker-image.yml](.github/workflows/docker-image.yml) (`build-args` / tags) + +Also refresh the `.sha512` file when the archive version changes. ```bash -sudo docker inspect --format '{{ .Name }} => {{ .NetworkSettings.IPAddress }}' $(sudo docker ps -a -q) +docker build --pull --target jmeter-base \ + --build-arg JMETER_VERSION=5.6.3 \ + -t my/jmeter-base:5.6.3 . ``` -### Put a sample test plan in the master container +## On your laptop + +### Start the main controller ```bash -sudo docker exec -i master sh -c 'cat > /home/jmeter/apache-jmeter-3.2/bin/test.jmx' < test.jmx +docker run --detach --name main-controller \ + -p 60000:60000 \ + jmeter-docker:jmeter-base-5.6.3 ``` -### Go inside the master container +### Start the workers + +Set `LOCALIP` to the address the controller uses to reach each worker. ```bash -sudo docker exec -it master /bin/bash +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.3 ``` -### Run the sample test plan +### Check they're running ```bash -/home/jmeter/apache-jmeter-3.2/bin/jmeter -n -t \ -/home/jmeter/apache-jmeter-3.2/bin/test.jmx \ --Djava.rmi.server.hostname=172.17.0.5 \ --Dclient.rmi.localport=60000 \ --R172.17.0.2,172.17.0.3,172.17.0.4 +docker container ls --format '{{.Names}}\t{{.Status}}' ``` -## On AWS instances +### Copy a test plan into the controller -### Create AWS instances with the following security groups: +`${JMETER_HOME}` is `/opt/apache-jmeter-5.6.3` inside the image. ```bash -Port=22 Protocol=tcp Source=0.0.0.0/0 -Port=1099 Protocol=tcp Source=0.0.0.0/0 -Port=50000 Protocol=tcp Source=0.0.0.0/0 -Port=60000 Protocol=tcp Source=0.0.0.0/0 +docker exec -i main-controller sh -c 'cat > "${JMETER_HOME}/bin/test.jmx"' < test.jmx ``` -### Install docker on the instances +### Run the distributed test + +Set `CONTROLLER_IP`, `CLIENT_PORT`, and the worker IPs for your environment. ```bash -sudo su - -apt-get update -apt-get install docker.io +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}" ``` -### Get out of `root` user +### Shell into a container ```bash -exit +docker exec -it main-controller bash # controller +docker exec -it worker1 bash # worker ``` -### On the slave instances +## On AWS (or similar) hosts -Where `$LOCALIP` should be the public IP address of the host. +### Firewall / security groups -```bash -sudo docker run -dit -e LOCALIP='(slave1 ip)' -p 1099:1099 -p 50000:50000 bsmithio/jmeter-slave:latest /bin/bash -sudo docker run -dit -e LOCALIP='(slave2 ip)' -p 1099:1099 -p 50000:50000 bsmithio/jmeter-slave:latest /bin/bash -sudo docker run -dit -e LOCALIP='(slave3 ip)' -p 1099:1099 -p 50000:50000 bsmithio/jmeter-slave:latest /bin/bash -``` +> ⚠️ **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 reach `1099/tcp` or `50000/tcp` can run code inside the worker. **Never expose these ports to the public internet** — never `0.0.0.0/0`. Restrict them to the controller and workers, prefer a private network (VPC/VPN), and set `LOCALIP` to a private address. To require authentication and encryption, enable [RMI SSL](#enabling-rmi-ssl). + +Open only what you need, scoped to trusted sources: -### On the master instance, do +- **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 + +### Controller ```bash -sudo docker run -dit --name master -p 60000:60000 bsmithio/jmeter-master:latest /bin/bash +docker run --detach --name main-controller \ + --restart unless-stopped \ + -p 60000:60000 \ + jmeter-docker:jmeter-base-5.6.3 ``` -### Copy the test to the master container +### Workers + +The 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`). ```bash -curl https://raw.githubusercontent.com/smithbr/jmeter-docker/master/test.jmx > test.jmx +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.3 ``` -### Move the test to the container +Copy in `test.jmx` and run the test the same way as [on your laptop](#copy-a-test-plan-into-the-controller). + +## Enabling RMI SSL + +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. + +### 1. Generate one shared keystore + +Generate it once and copy the same file to every host: ```bash -sudo docker exec -i master sh -c 'cat > /home/jmeter/apache-jmeter-3.2/bin/test.jmx' < test.jmx +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/' ``` -### Enter the master container +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. + +### 2. Start workers with the keystore + +Mount it read-only and set `RMI_KEYSTORE` and `RMI_KEYSTORE_PASSWORD`: ```bash -sudo docker exec -it master /bin/bash +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.3 ``` -### And run the test +Env 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. + +### 3. Run the controller with the same keystore + +Mount the keystore and pass the SSL properties on the run: ```bash -/home/jmeter/apache-jmeter-3.2/bin/jmeter -n -t \ -/home/jmeter/apache-jmeter-3.2/bin/test.jmx \ --Djava.rmi.server.hostname=(master ip) \ --Dclient.rmi.localport=60000 \ --R(slave1 ip),(slave2 ip),(slave3 ip) +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. From 858153d0700a239f00a86727b2cee40fe82524df Mon Sep 17 00:00:00 2001 From: smithbr <1495361+smithbr@users.noreply.github.com> Date: Sun, 7 Jun 2026 16:57:05 -0700 Subject: [PATCH 7/7] Put JMeter on PATH by splitting the ENV instruction JMETER_HOME and PATH were set in one ENV, so ${JMETER_HOME} resolved to its empty pre-instruction value and the JMeter bin dir never reached PATH. Bare `jmeter` (used by the CI smoke check) failed with "not found". --- Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index e238d5d..91a61a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,8 +23,10 @@ LABEL org.opencontainers.image.description="JMeter 5.x with JMeter Plugins Manag LABEL org.opencontainers.image.licenses="Apache-2.0" LABEL org.opencontainers.image.url="https://jmeter.apache.org/" -ENV JMETER_HOME=/opt/apache-jmeter-${JMETER_VERSION} \ - PATH=${JMETER_HOME}/bin:${PATH} +# Separate ENV instructions: within one ENV, ${JMETER_HOME} would resolve to its +# pre-instruction (empty) value, leaving JMeter off PATH. +ENV JMETER_HOME=/opt/apache-jmeter-${JMETER_VERSION} +ENV PATH=${JMETER_HOME}/bin:${PATH} RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \