---
title: "Local HA Compose Simulation"
editUrl: false
description: "Use this setup to simulate a small HA environment on one machine:"
---
Use this setup to simulate a small HA environment on one machine:

- `3` MCP replicas
- `1` shared Postgres instance
- `1` one-shot Flyway migration job
- `1` shared ZAP instance
- `1` local HTTP gateway on `localhost:7456`

## Why this override exists

The default `docker-compose.yml` is a single-node local stack:

- `mcp-server` publishes a fixed host port
- queue state defaults to `in-memory`
- queue coordination defaults to `single-node`

That is fine for local development, but it is not enough to reproduce multi-replica queue behavior.

## Start the stack

```bash
./ha.sh
```

Optional replica count:

```bash
MCP_REPLICAS=5 ./ha.sh
```

The helper script restarts the local gateway after scaling so it picks up the current replica set.
It now renders explicit upstream entries for the running `mcp-server` containers before restarting nginx, so affinity is pinned to concrete replicas rather than the Docker service alias.

## What the HA override changes

- adds a shared `postgres` service for queue state, leader election, and JWT revocation state
- runs a one-shot `db-migrate` Flyway container before MCP replicas start
- removes direct host-port publishing from `mcp-server`
- sets queue store backend to `postgres`
- sets coordinator backend to `postgres-lock`
- removes the static `ZAP_SCAN_QUEUE_COORDINATOR_NODE_ID` override so the app derives `node-id` from each container hostname
- adds `mcp-gateway` on `localhost:7456` so you can hit one stable local endpoint
- configures the gateway with client-affinity hashing so streamable MCP requests from one client stay on one replica
- renders explicit upstream entries for the current `mcp-server` containers so nginx affinity targets concrete replicas

## Files

- `docker-compose.ha.yml`
- `docker/nginx/mcp-ha.conf.tmpl`
- `docker/nginx/mcp-ha.conf` (generated by `ha.sh`, ignored by git)
- `ha.sh`

## Useful commands

Migration logs:

```bash
docker compose -f docker-compose.yml -f docker-compose.ha.yml logs db-migrate
```

Leader/follower logs:

```bash
docker compose -f docker-compose.yml -f docker-compose.ha.yml logs -f mcp-server
```

Stop the stack:

```bash
docker compose -f docker-compose.yml -f docker-compose.ha.yml down
```

Remove Postgres data too:

```bash
docker compose -f docker-compose.yml -f docker-compose.ha.yml down -v
```

## Notes

- This is a local simulation, not a production topology.
- The gateway is a lightweight local ingress for convenience, not a leader-aware router.
- The gateway is session-aware: streamable MCP over HTTP needs sticky ingress when session state lives in-memory on each replica.
- In local Compose, `ha.sh` owns the generated gateway config because explicit per-replica upstream entries are required for nginx hashing to behave correctly with scaled Docker services.
- MCP replicas assume the shared Postgres schema has already been migrated; they no longer create queue/JWT tables on startup.
- The dev override preserves the base API-key/security behavior. Only add an override when you intentionally want to test `none`, JWT, or another auth mode.
