A self-hosted control plane for isolated mitmproxy instances. It can run as a single container or as a coordinator with horizontally scalable proxy workers, which makes it suitable for Selenium Grid and other concurrent automation environments.
- Isolated proxy instances — each active test can own an independent
mitmdumpprocess, rule set, CA, and proxy port. - Horizontal worker scaling — run a lightweight coordinator plus any number of workers. Every worker may reuse the same internal proxy port range because workers have separate container or pod addresses.
- Worker discovery — workers heartbeat their capacity to the coordinator. Stale workers are removed automatically.
- Coordinator recovery — if the coordinator loses its in-memory instance-owner map, it can rediscover ownership from registered workers.
- Advertised proxy endpoints — instance responses include
proxyHost,proxyPort, andproxyUrl, so browser nodes can connect directly to the worker that owns an instance. - Automatic TTL and expiry — abandoned proxy instances are reaped automatically.
- Request and response interception — modify headers, query parameters, bodies, status codes, and binary responses with declarative rules.
- Per-instance CA certificates — download the CA for HTTPS interception through the REST API.
- GHCR distribution — multi-architecture
linux/amd64andlinux/arm64images are published only from version tags.
The release introduced by this version is pinned as:
ghcr.io/ygrip/mitmproxy-grid:2.1.0
There is intentionally no moving latest release contract. Consumers should pin the grid version they support.
Use standalone mode when one grid process has enough capacity or when the browser and grid share the same host/network addressing model.
docker run -d \
--name mitmproxy-grid \
--restart unless-stopped \
-p 8090:8090 \
-p 10000-10100:10000-10100 \
-e GRID_MODE=standalone \
-e INSTANCE_TTL=1800 \
-v mitmproxy-grid-data:/data \
-v mitmproxy-grid-ca:/ca \
ghcr.io/ygrip/mitmproxy-grid:2.1.0Verify it:
curl http://localhost:8090/healthThe dashboard is at http://localhost:8090, Swagger UI at /docs, and ReDoc at /redoc.
git clone https://github.com/ygrip/mitmproxy-grid.git
cd mitmproxy-grid
docker compose up -ddocker-compose.yml defaults to 2.1.0. Override only when you deliberately support another release:
MITMPROXY_GRID_VERSION=2.1.0 docker compose up -dFor concurrent Selenium automation, run one coordinator and scale proxy workers on the same Docker network as the Selenium Grid nodes.
The topology is:
Jenkins / Testara
|
| REST control
v
mitmproxy-grid coordinator
|
+----------+----------+
| | |
worker-1 worker-2 worker-N
:10000... :10000... :10000...
^ ^ ^
+----------+----------+
|
Selenium Grid nodes
Workers do not publish proxy ports to the Jenkins host. Selenium nodes connect directly to the worker address returned in proxyUrl, so every worker can reuse the same internal range such as 10000-10009.
Assume the existing Selenium Grid network is named selenium-grid. If yours has another name, set AUTOMATION_NETWORK.
export AUTOMATION_NETWORK=selenium-griddocker compose -f docker-compose.scaled.yml up -d --scale mitm-worker=4Or:
make start-scaled WORKERS=4The default worker range is ten proxy slots per worker:
PORT_START=10000
PORT_END=10009
Four workers therefore provide up to forty isolated proxy instances while using the same ten internal port numbers on every worker.
curl http://localhost:8090/health
curl http://localhost:8090/workersCoordinator health includes aggregate worker capacity and worker heartbeat information.
curl -s -X POST http://localhost:8090/instances | python3 -m json.toolA distributed instance response includes both the legacy port field and the routable endpoint:
{
"instanceId": "f7c1...",
"port": 10003,
"proxyHost": "172.20.0.8",
"proxyPort": 10003,
"proxyUrl": "http://172.20.0.8:10003",
"workerId": "mitm-worker-3",
"status": "running",
"ttl": 1800,
"expiresAt": "2026-08-11T10:00:00+00:00"
}Distributed clients must use proxyUrl or proxyHost + proxyPort. The coordinator API host is not the proxy host. The legacy port field remains for backwards compatibility with standalone consumers.
The same image supports three modes:
GRID_MODE |
Purpose |
|---|---|
standalone |
API and local mitmproxy instances in one container. Default. |
coordinator |
Routes instance operations across registered workers. Does not start mitmdump. |
worker |
Starts local mitmproxy instances and registers capacity with a coordinator. |
Important environment variables:
| Variable | Default | Description |
|---|---|---|
GRID_MODE |
standalone |
Runtime mode. |
PORT_START |
10000 |
First local proxy port owned by a standalone grid or worker. |
PORT_END |
10100 |
Last local proxy port. Scaled Compose defaults workers to 10009. |
INSTANCE_TTL |
1800 |
Default instance lifespan in seconds. |
MAX_BODY_SIZE |
52428800 |
Maximum decoded bodyBase64 payload size. |
COORDINATOR_URL |
empty | Coordinator REST URL used by workers. |
PROXY_ADVERTISE_HOST |
auto | Host/IP returned to clients for worker proxy traffic. Override when automatic container/pod addressing is not routable by browser nodes. |
WORKER_API_URL |
auto | Worker REST address registered with the coordinator. |
WORKER_REGISTER_INTERVAL |
5 |
Worker heartbeat interval in seconds. |
WORKER_STALE_AFTER |
20 |
Time before the coordinator removes a silent worker. |
A worker owns a small local pool of mitmdump processes. Scale worker replicas, rather than creating one enormous process pool:
docker compose -f docker-compose.scaled.yml up -d --scale mitm-worker=8This keeps rule isolation strong and bounds the impact of a failed worker. A coordinator restart does not require Redis for recovery: workers re-register and instance ownership is rediscovered on demand by querying their active instances.
If coordinator high availability is required later, the in-memory registry can be replaced with a shared store without changing the public instance API.
curl -s -X POST http://localhost:8090/instances/{INSTANCE_ID}/rules \
-H "Content-Type: application/json" \
-d '{
"match": { "urlContains": "example.com/api" },
"action": {
"modifyResponse": {
"statusCode": 200,
"body": {"mocked": true}
}
}
}'curl -s http://localhost:8090/instances/{INSTANCE_ID}/cert -o ca.pemcurl -X DELETE 'http://localhost:8090/instances/{INSTANCE_ID}?cleanup=true'| Method | Path | Description |
|---|---|---|
GET |
/health |
Grid mode, version, capacity, and worker health. |
GET |
/workers |
Registered workers. Coordinator mode only. |
POST |
/workers/register |
Worker heartbeat/registration. Coordinator mode only. |
GET |
/instances |
List active instances. Coordinator aggregates all workers. |
POST |
/instances |
Allocate a proxy instance. |
GET |
/instances/{id} |
Get instance details. |
DELETE |
/instances/{id} |
Destroy an instance. |
POST |
/instances/{id}/renew |
Renew TTL. |
GET |
/instances/{id}/rules |
List interception rules. |
POST |
/instances/{id}/rules |
Add a rule. |
DELETE |
/instances/{id}/rules/{index} |
Delete a rule. |
PATCH |
/instances/{id}/rules/{index}/toggle |
Toggle a rule. |
GET |
/instances/{id}/cert |
Download the instance CA certificate. |
Pull requests run Python unit tests, compile the Python sources, and build the multi-architecture container without publishing it.
A GHCR image is published only when a semantic Git tag is pushed:
git tag v2.1.0
git push origin v2.1.0That tag publishes exactly:
ghcr.io/ygrip/mitmproxy-grid:2.1.0
No main, SHA, major-only, minor-only, or latest package tag is published by the release workflow. This keeps automation environments reproducible and lets clients declare the exact grid version they support.
docker build \
--build-arg GRID_VERSION=dev \
-t mitmproxy-grid:dev \
./grid├── .github/workflows/publish-ghcr.yml
├── docker-compose.yml
├── docker-compose.scaled.yml
├── Makefile
└── grid/
├── Dockerfile
├── config.py
├── main.py
├── models.py
├── instance_manager.py
├── worker_registry.py
├── rule_registry.py
├── interceptor_template.py
├── dashboard.html
└── tests/
└── test_worker_registry.py
MIT