This folder contains everything needed to run PerfPilot Hub in containers on
your local machine. Each MCP server ships as its own independent Docker image,
each sub-folder ships its own Dockerfile + docker-compose.yml + .env.example,
and two top-level compose files (docker-compose-full-{mac,windows}.yaml) bring
the whole stack up together.
📌 Looking for the pre-refactor monolithic Docker setup?
The Phase 1 refactor (per-MCP images, baked-in configs,
DEPLOYMENT_MODEtoggle) replaces the earlier monolithic gateway container. If you prefer the older single-image layout — or you need a stable checkpoint to compare against — use the v1.1.0 release (FastMCP 3.4.x, last stable before the Docker refactor):git clone --branch v1.1.0 https://github.com/canyonlabz/perfpilot-hub.gitAll releases live at github.com/canyonlabz/perfpilot-hub/releases.
PerfPilot Hub is a collection of Model Context Protocol (MCP) servers plus an AI-agent framework that automates performance-testing workflows (JMeter runs, BlazeMeter cloud tests, Datadog observability, PerfAnalysis, PerfReport, Confluence publishing, and a PostgreSQL-backed memory layer).
Design principles applied throughout docker/:
- Per-MCP images. No monolithic image. Each MCP (
blazemeter,datadog,jmeter,perfanalysis,perfreport,confluence,perfmemory,github,playwright) plus the gateway, agent backend, and UI has its own Dockerfile. - Baked-in configs. All YAML / JSON configs are
COPY'd into each image at build time. There are zero./config/*:robind mounts in any compose file — configs never travel over a bind mount at runtime. DEPLOYMENT_MODEtoggle. A single env var flips human-readable console logs (local) into OTel-shaped JSON to stdout (cloud). Same image, both modes. No dual Dockerfiles.- Independence Contract. Each Dockerfile builds from its own sub-folder
plus the MCP source tree. Only three tightly-scoped cross-MCP config
COPYs are allowed, all documented in §13.
Local development runs Docker Compose from this folder. Cloud deployment (Azure / Aspire) is a Phase 2 concern — same images, different orchestration.
New to Docker? Here's the shortest path to a working stack.
Pick one — both are confirmed working with this repo:
| Engine | Notes |
|---|---|
| 🐳 Docker Desktop | Original, most common. Free for personal / small-business use; check licensing for enterprise. |
| 🐄 Rancher Desktop | Open-source alternative, no licensing fees. Uses containerd or dockerd under the hood. Same docker and docker compose commands. |
Both must ship Docker Engine 20.10+ and Docker Compose v2 (the docker compose command, not the older docker-compose script).
Verify after install:
docker --version # → Docker version 24.x or newer
docker compose version # → Docker Compose version v2.x.x
docker info # → succeeds without errorsThe full stack runs 14 containers. Recommended minimums for a smooth experience:
- CPU: 4 cores (2 minimum, but startup will be slow)
- RAM: 8 GB assigned to the Docker VM (6 GB minimum). Set in Docker Desktop → Settings → Resources, or Rancher Desktop → Preferences → Virtual Machine.
- Disk: ~15 GB free after images build. The JMeter image alone is ~1.5 GB (Java + JMeter + plugins).
| OS | Status | Notes |
|---|---|---|
| 🪟 Windows 10/11 | ✅ Confirmed | Use docker-compose-full-windows.yaml. Docker Desktop with the WSL2 backend is the standard setup. |
| 🍎 macOS 13+ (Intel or Apple Silicon) | ✅ Confirmed | Use docker-compose-full-mac.yaml. The Mac file sets user: "999:999" on the database to work around a Docker Desktop VirtioFS quirk (§9.2). PGDATA is set in both compose files (PG18+ convention, host-OS agnostic). |
| 🐧 Linux | Should work with the Windows compose file, but has not been validated against this repo. |
You'll need git to clone this repo. On Windows install Git for Windows;
on macOS xcode-select --install or brew install git.
Not everything is required for every workflow, but if you plan to bring the full stack up you'll want most of these:
- 🔑 BlazeMeter API key + secret + account ID + workspace ID
- 🔑 Datadog API key + application key
- 🔑 Confluence — either a Cloud API token or an on-prem PAT
- 🔑 OpenAI or Azure OpenAI or Ollama access (used by both the agent chat model and the PerfMemory embedding model)
- 🔑 GitHub personal access token
- 🔒 PostgreSQL password (you pick this — used for the local
perfmem-pgvector-agecontainer) - 🏢 (optional) Corporate CA bundle (PEM) if your network runs an HTTPS-intercepting proxy (Zscaler, Norton 360, BlueCoat, etc.). See §9.
Every secret goes into docker/.env (copied from docker/.env.example).
Nothing is baked into images.
Every container, image, and compose service uses one of the following
canonical names. There is no legacy perf-* or perfmemory-db after Phase 1.
| Component | Image / Container name | Sub-folder |
|---|---|---|
| MCP — BlazeMeter | perfpilot-mcp-blazemeter |
docker/blazemeter-mcp/ |
| MCP — Datadog | perfpilot-mcp-datadog |
docker/datadog-mcp/ |
| MCP — JMeter | perfpilot-mcp-jmeter |
docker/jmeter-mcp/ |
| MCP — PerfAnalysis | perfpilot-mcp-perfanalysis |
docker/perfanalysis-mcp/ |
| MCP — PerfReport | perfpilot-mcp-perfreport |
docker/perfreport-mcp/ |
| MCP — Confluence | perfpilot-mcp-confluence |
docker/confluence-mcp/ |
| MCP — PerfMemory | perfpilot-mcp-perfmemory |
docker/perfmemory-mcp/ |
| MCP — GitHub | perfpilot-mcp-github |
docker/github-mcp/ |
| MCP — Playwright (vendor) | perfpilot-mcp-playwright |
docker/playwright-mcp/ |
| Gateway (aggregator) | perfpilot-mcp-gateway |
docker/gateway-mcp/ |
| Agent backend — A2A | perfpilot-a2a |
docker/agent-backend/ |
| Agent backend — AG-UI | perfpilot-agui |
docker/agent-backend/ (same Dockerfile, two image tags) |
| Frontend | perfpilot-ui |
docker/agent-frontend/ |
| Database | perfmem-pgvector-age |
docker/postgresql/ |
Compose service names match image names, so docker compose ps output and
image tags always align.
MCPs are grouped in the 81xx range for easy firewall rules. Agent backend on
810x, UI on 8080, database on the standard 5432.
| Service | Port | Endpoint path |
|---|---|---|
perfmem-pgvector-age |
5432 |
(raw postgres protocol) |
perfpilot-ui |
8080 |
/ |
perfpilot-a2a |
8101 |
/ (FastAPI + SSE) |
perfpilot-agui |
8102 |
/ (FastAPI + CopilotKit bridge) |
perfpilot-mcp-blazemeter |
8110 |
/perfpilot-mcp-blazemeter/mcp |
perfpilot-mcp-datadog |
8111 |
/perfpilot-mcp-datadog/mcp |
perfpilot-mcp-jmeter |
8112 |
/perfpilot-mcp-jmeter/mcp |
perfpilot-mcp-perfanalysis |
8113 |
/perfpilot-mcp-perfanalysis/mcp |
perfpilot-mcp-perfreport |
8114 |
/perfpilot-mcp-perfreport/mcp |
perfpilot-mcp-confluence |
8115 |
/perfpilot-mcp-confluence/mcp |
perfpilot-mcp-perfmemory |
8116 |
/perfpilot-mcp-perfmemory/mcp |
perfpilot-mcp-playwright |
8117 |
/mcp (vendor image, no prefix) |
perfpilot-mcp-github |
8118 |
/perfpilot-mcp-github/mcp |
perfpilot-mcp-gateway |
8125 |
/perfpilot-mcp-gateway/mcp (aggregates the 8 MCPs above) |
Cursor / Claude Desktop / any MCP client should point at
http://localhost:8125/perfpilot-mcp-gateway/mcp — the gateway exposes every
mounted MCP under its own namespace (jmeter.*, blazemeter.*, etc.).
docker/
├── README.md (this file)
├── docker-compose-full-mac.yaml (macOS full-stack: adds `user:` VirtioFS workaround)
├── docker-compose-full-windows.yaml (Windows/WSL2 full-stack)
├── .env.example (union of all secrets; copy to .env)
│
├── certs/ (all optional; empty by default)
│ ├── corporate/ (CA PEM bundles for HTTPS-intercepting proxy)
│ ├── jmeter/ (JMeter-only: .jks client keystores)
│ └── playwright/ (Playwright-only: .p12 / .pem test-user certs)
│
├── data/ (gitignored; local postgres data lives here)
│ └── pgvectordb/ (bind-mounted into perfmem-pgvector-age)
│
├── postgresql/ (perfmem-pgvector-age image)
├── gateway-mcp/ (perfpilot-mcp-gateway image)
├── playwright-mcp/ (perfpilot-mcp-playwright image, vendor)
├── agent-backend/ (perfpilot-a2a + perfpilot-agui images)
├── agent-frontend/ (perfpilot-ui image)
│
├── blazemeter-mcp/ (perfpilot-mcp-blazemeter image)
├── datadog-mcp/ (perfpilot-mcp-datadog image)
├── jmeter-mcp/ (perfpilot-mcp-jmeter image)
├── perfanalysis-mcp/ (perfpilot-mcp-perfanalysis image)
├── perfreport-mcp/ (perfpilot-mcp-perfreport image)
├── confluence-mcp/ (perfpilot-mcp-confluence image)
├── perfmemory-mcp/ (perfpilot-mcp-perfmemory image)
└── github-mcp/ (perfpilot-mcp-github image)
Every sub-folder contains at minimum a Dockerfile, a docker-compose.yml,
and a .env.example. Most also contain a config/ folder (baked into the
image) and a README.md.
Each FastMCP-based MCP reads three env vars at startup:
MCP_TRANSPORT=http— alwayshttpin Docker (stdio is the local-dev default)HTTP_PORT=<port>— the listen port (see §4)MCP_HTTP_PREFIX=/perfpilot-mcp-<name>— the URL path prefix
And it calls:
mcp.run(
transport="http",
host="0.0.0.0",
port=int(os.environ["HTTP_PORT"]),
path=os.environ["MCP_HTTP_PREFIX"] + "/mcp",
)Endpoint = http://<host>:<port>/perfpilot-mcp-<name>/mcp.
Playwright is the sole exception. It wraps Microsoft's vendor image which
does not accept a path prefix — its endpoint is /mcp only. Agents call it
directly at http://perfpilot-mcp-playwright:8117/mcp, bypassing the gateway.
The gateway dials each mounted MCP via MCP_URL_<NAME> env vars, which
default to the compose-network DNS values baked into docker/gateway-mcp/config/config.yaml:
MCP_URL_BLAZEMETER → http://perfpilot-mcp-blazemeter:8110/perfpilot-mcp-blazemeter/mcp
MCP_URL_DATADOG → http://perfpilot-mcp-datadog:8111/perfpilot-mcp-datadog/mcp
... etc.
Override any of them in docker/.env for split-mode / debugging setups.
A single env var switches log format and secret source. Same image, both modes.
- Human-readable console output via structlog's
ConsoleRenderer. - Secrets read from
docker/.env(copied fromdocker/.env.example). - Used for local development and manual smoke-testing.
Example log line:
2026-09-10 23:22:00 [info ] connected-to-blazemeter workspace_id=12345
- OTel-shaped JSON emitted to stdout (fields per OpenTelemetry Logs Semantic
Conventions:
timestamp,severity_text,severity_number,body,trace_id,span_id,attributes,resource.service.name). - Secrets read from injected env vars (Vault / Azure Key Vault / Aspire user-secrets).
No
.envfile is loaded in cloud mode. - Used by the Phase 2 Aspire deployment on Azure.
Example log line:
{"timestamp":"2026-09-10T23:22:00.123Z","severity_text":"INFO","severity_number":9,"body":"connected-to-blazemeter","trace_id":"","span_id":"","attributes":{},"resource.service.name":"perfpilot-mcp-blazemeter","workspace_id":12345}Every sub-folder ships its own docker-compose.yml and .env.example for
isolated testing. The pattern is always the same:
cd docker/<component>/
cp .env.example .env # then edit .env with real values
docker compose up --build -d- Windows PowerShell:
Copy-Item .env.example .envinstead ofcp. - The build context is
../..(repo root), so the compose file canCOPYfrom bothdocker/<component>/andmcp-perf-suite/<mcp>/.
Standalone gateway — brings up perfpilot-mcp-gateway alone. The 8 MCP
URLs default to host.docker.internal in the standalone .env.example, so
you can bring up each MCP in its own compose and dial them from the gateway
container without shared networking.
Each sub-folder README (see §14) documents its own healthcheck endpoint,
required secrets, and any component-specific quirks (JMeter's JKS mount,
Playwright's --config flags, etc.).
Two OS-specific compose files bring the entire 14-container stack up at once.
cd docker\
Copy-Item .env.example .env
# Edit .env with your secrets
docker compose -f docker-compose-full-windows.yaml up --buildcd docker/
cp .env.example .env
# Edit .env with your secrets
docker compose -f docker-compose-full-mac.yaml up --buildThe Mac file adds one setting to perfmem-pgvector-age that the Windows file
does not need:
user: "999:999"— forces the container process to run as UID 999 (the image's postgres user), so it can chown the bind-mounted data folder (Docker Desktop for Mac's VirtioFS requires this; WSL2 doesn't).
Both compose files set PGDATA: /var/lib/postgresql/18/docker/pgdata — that is
the PostgreSQL 18+ version-specific data-cluster layout and is host-OS agnostic.
Without the user: setting, PostgreSQL will fail to initialize on macOS.
- Every image ships a Dockerfile
HEALTHCHECKthat probes its own endpoint —/healthGET viawgetfor all 9 FastMCP MCPs,/mcpviawget --spider(accepting HTTP 200 or 406) for Playwright,/healthviacurl -ffor the agent backend,/viawget --spiderfor the frontend, andpg_isreadyfor PostgreSQL. - Gateway
depends_onall 8 mounted MCPs withcondition: service_healthy, so it does not start until every backing MCP responds to a probe. - Agents (
perfpilot-a2a,perfpilot-agui)depends_onthe gateway and the database (service_healthy) plus Playwright (service_started, since Playwright's health probe uses a non-standard endpoint). - UI (
perfpilot-ui)depends_onthe agent backend.
Full-stack cold-start typically takes 60–90 seconds. Use docker compose ps
to watch health status; use docker compose logs -f <service> to trace a
specific container.
docker compose -f docker-compose-full-<os>.yaml downPostgres data survives because it lives on the host bind mount
(docker/data/pgvectordb/). To reset the database entirely, add -v (removes
volumes) and delete docker/data/pgvectordb/ from the host.
If your machine runs a TLS-intercepting proxy (Zscaler, Norton 360, BlueCoat) or you're inside a corporate network with a private CA, every image needs the corporate CA installed into its trust store.
Enable with two steps:
- Place your CA bundle (PEM format, may be a concatenation of multiple certs)
at
docker/certs/corporate/ca-bundle.pem. - Set
ENABLE_CORP_CA=trueindocker/.env. - Rebuild every image:
docker compose ... up --build.
Every Python image installs the CA into:
- The OS trust store (
update-ca-certificates). SSL_CERT_FILEandREQUESTS_CA_BUNDLEenv vars.
The Playwright image additionally installs the CA into:
- The Chromium NSS database (
certutil). - Node.js's
NODE_EXTRA_CA_CERTS. - Adds
--ignore-https-errorsas a belt-and-suspenders fallback.
The JMeter image additionally imports the CA into Java's cacerts
keystore (via keytool).
Nothing about ENABLE_CORP_CA=false (the default) installs any corporate cert
— you can safely ignore this whole section if you're not behind a proxy.
Three sub-folders under docker/certs/ are recognized. Every image sees only
the cert store(s) it needs:
| Folder | Consumed by | File types | Purpose |
|---|---|---|---|
docker/certs/corporate/ |
Every image (when ENABLE_CORP_CA=true) |
*.pem |
Corporate CA bundles for HTTPS-intercepting proxies. |
docker/certs/jmeter/ |
perfpilot-mcp-jmeter only |
*.jks |
JMeter TLS client keystores (JKS format). File name + password go into .env via JMETER_JKS_FILE / JMETER_JKS_PWD. |
docker/certs/playwright/ |
perfpilot-mcp-playwright only |
*.p12, *.pem |
Test-user browser digital certs. Filename + passphrase go into .env via PLAYWRIGHT_CERT_FILE / PLAYWRIGHT_CERT_PASSPHRASE. Optional PLAYWRIGHT_CERT_AUTO_SELECT_PATTERN (defaults to wildcard *) narrows the Chromium AutoSelectCertificateForUrls policy scope. |
All three are optional at build time — the folders are committed as empty
directories (each has a .gitkeep). JMeter does not read .p12/.pem;
Playwright does not read .jks. Corporate CA is unrelated to either
authentication store.
Anyone who cloned or forked this repo before Phase 1's Docker restructure will find their local workflows broken until they update to the new names. Here's the mapping:
| Old (pre-Phase 1) | New (Phase 1+) |
|---|---|
perf-gateway:8888 (monolithic image) |
perfpilot-mcp-gateway:8125 (aggregator) + 8 per-MCP images (8110–8118) |
perfmemory-db:5432 (compose service) |
perfmem-pgvector-age:5432 |
playwright-mcp:8931 |
perfpilot-mcp-playwright:8117 |
agent-a2a:8001 |
perfpilot-a2a:8101 |
agent-agui:8002 |
perfpilot-agui:8102 |
frontend:3000 |
perfpilot-ui:8080 |
docker/.env.gateway (operator file) |
docker/.env |
docker/.env.gateway.example |
docker/.env.example (union of all secrets) |
docker/Dockerfile.gateway.example (monolithic) |
docker/<mcp>/Dockerfile × 9 |
docker/entrypoint.sh (shared) |
docker/<mcp>/entrypoint.sh × per-MCP |
docker/config/<mcp>/config.yaml (bind-mounted) |
docker/<mcp>/config/config.yaml (baked into image) |
MCP endpoint /mcp (single path) |
/perfpilot-mcp-<name>/mcp (namespaced per MCP) |
Cursor / MCP clients: update the server URL from
http://localhost:8888/mcp → http://localhost:8125/perfpilot-mcp-gateway/mcp.
Every Dockerfile builds from its own sub-folder plus the MCP source tree
(mcp-perf-suite/<mcp>/ or agent-framework/) plus the optional cert drop
points. There are exactly three exceptions — cross-MCP config file COPYs
that must land at a specific sibling path inside the consumer image:
| Consumer image | Shared file | Destination inside image |
|---|---|---|
perfpilot-mcp-perfanalysis |
docker/datadog-mcp/config/environments.json |
/app/datadog-mcp/environments.json |
perfpilot-mcp-perfreport |
docker/datadog-mcp/config/environments.json |
/app/datadog-mcp/environments.json |
perfpilot-mcp-confluence |
docker/perfreport-mcp/config/chart_schema.yaml |
/app/perfreport-mcp/chart_schema.yaml |
These three are the only permitted cross-folder COPYs. Everything else
must live inside the sub-folder that owns the Dockerfile.
Manual checklist when adding or changing a Dockerfile in this folder:
- No
./config/*:robind mounts anywhere in the compose file. - Every
docker/<mcp>/config/folder has a correspondingCOPY docker/<mcp>/config/...line in its sibling Dockerfile (configs are baked, not mounted). - The
utils/logging_config.pymodule emits console output whenDEPLOYMENT_MODE=localand OTel-shaped JSON whenDEPLOYMENT_MODE=cloud. - Any cross-folder
COPYmust appear in the three-row table above; otherwise it violates the contract.
- Per-component READMEs live inside each sub-folder — start there for image
size, secrets required, healthcheck endpoints, and any quirks specific to
that MCP:
postgresql/README.mdgateway-mcp/README.mdplaywright-mcp/README.mdagent-backend/README.mdagent-frontend/README.mdblazemeter-mcp/README.mddatadog-mcp/README.mdjmeter-mcp/README.mdperfanalysis-mcp/README.mdperfreport-mcp/README.mdconfluence-mcp/README.mdperfmemory-mcp/README.mdgithub-mcp/README.md
- Repository-level documentation:
- Repository root README — overall project overview.
mcp-perf-suite/README.md— MCP source tree.agent-framework/README.md— agent backend + UI source tree.