> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mcphub.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Multiple Replicas

> Shared PostgreSQL configuration with explicit session and failover boundaries

Replica mode is opt-in (`MCPHUB_REPLICA_MODE=true`). Identical MCPHub instances share PostgreSQL behind a load balancer. Modern, stateless MCP calls can reach any ready replica. Legacy MCP sessions, `perSessionClient` clients and OpenAPI cookie sessions remain in one process: use affinity and reinitialize after node loss. Active calls are not migrated or replayed.

## Prepare once, then serve

Build the same application version for setup and all replicas. Set `DB_URL`, `USE_DB=true`, a persistent `JWT_SECRET` of at least 32 bytes, and `MCPHUB_CREDENTIAL_ENCRYPTION_KEY` (32 bytes encoded as base64). Every replica must use the same secrets and external installation/OAuth URLs. Set a unique `MCPHUB_NODE_ID` on each replica; the default is its hostname. Set `ADMIN_PASSWORD` when initializing a new deployment.

Run one schema preparation job before starting or upgrading replicas:

```sh theme={null}
DB_SYNCHRONIZE=true MCPHUB_REPLICA_MODE=false pnpm db:setup
```

This uses the existing database initialization and migration path, seeds the administrator if necessary, and installs transactional configuration revision tracking. Back up an existing database before schema preparation. Apply declarative `MCPHUB_SETTINGS_SYNC` in this job only, then unset it on serving nodes. Do not allow multiple setup jobs to run together.

Start each serving replica with schema synchronization disabled:

```sh theme={null}
DB_SYNCHRONIZE=false MCPHUB_REPLICA_MODE=true MCPHUB_NODE_ID=a pnpm start
```

Serving replicas do not create extensions, synchronize tables or rebuild vector indexes at startup. A missing schema, unseeded database, missing persistent secrets, or serving-node `MCPHUB_SETTINGS_SYNC` is a startup error. Keep local binaries, environment variables and files consistent when using stdio servers; their child-process and local filesystem state is not shared.

## Deploy Docker images with Compose

The repository includes a deployment example in [`examples/replicas/`](https://github.com/samanhappy/mcphub/tree/main/examples/replicas): two MCPHub containers, a persistent PostgreSQL database, a one-off setup job, and Nginx. It requires Docker Compose v2 with `--wait` support and uses the official `samanhappy/mcphub` image. The example uses `latest`; pin `MCPHUB_IMAGE` to a released version or digest for production.

```sh theme={null}
# From the repository root
cd examples/replicas

# Run only for a NEW installation; retain this file and back up its secrets.
umask 077
cat > .env <<EOF
MCPHUB_IMAGE=samanhappy/mcphub:latest
POSTGRES_PASSWORD=$(openssl rand -hex 24)
JWT_SECRET=$(openssl rand -hex 32)
MCPHUB_CREDENTIAL_ENCRYPTION_KEY=$(openssl rand -base64 32)
ADMIN_PASSWORD=$(openssl rand -hex 24)
INSTALL_BASE_URL=http://localhost:3080
EOF

docker compose pull
docker compose up -d --wait postgres
docker compose run --rm setup
docker compose up -d --wait hub-a hub-b proxy
docker compose ps
curl --fail http://localhost:3080/nodes/a/readyz
curl --fail http://localhost:3080/nodes/b/readyz
```

Open `http://localhost:3080` and sign in as `admin` with the `ADMIN_PASSWORD` in `.env`. The setup job and both replicas always use the same image; `node dist/db/setup.js` runs inside the image, so the host needs no Node.js or pnpm. The read-only `settings.json` seeds an empty installation with the OAuth server disabled; manage shared configuration through the dashboard/API after startup. Shared configuration and credentials are stored in PostgreSQL, without shared writable settings files or stdio volumes.

To verify unreleased source only, run `docker build -t mcphub:replicas .` from the repository root, set `MCPHUB_IMAGE=mcphub:replicas` in `.env`, and skip pulling that local image.

The gateway binds only to host loopback; expose it through your HTTPS ingress and set `INSTALL_BASE_URL` to that external HTTPS URL **before setup**. Configure the same public installation/OAuth URLs on all nodes. The included Nginx configuration provides streaming, affinity headers and fixed node endpoints. Compose health checks gate initial startup, but Nginx does not actively poll `/readyz` or automatically drain unhealthy nodes: production routing must do that separately. Container recreation can change IP addresses; recreate/reload Nginx after changing backends. This single-host example does not protect against host, database or load-balancer loss.

For an existing installation, reuse the current database, JWT secret and credential encryption key; back up the database and keys before setup. Do not regenerate `.env` or rotate the database password during an application upgrade. Stop traffic and replicas before schema changes; this example uses a maintenance window rather than assuming every schema change supports rolling upgrades:

```sh theme={null}
docker compose stop proxy hub-a hub-b
# Update MCPHUB_IMAGE in .env to the new fixed version, then pull it.
docker compose pull setup hub-a hub-b
docker compose run --rm setup
docker compose up -d --wait hub-a hub-b
docker compose up -d --force-recreate proxy
```

If setup fails, leave serving containers stopped and inspect its error before continuing. `docker compose down` keeps the named database volume; `docker compose down -v` deletes it. Keep `.env` out of source control and preserve its secrets alongside database backups. This Compose file does not migrate an existing file-based installation automatically; that requires mounting its settings and credential sidecars into the setup job first.

## Deploy on Kubernetes

[`examples/kubernetes/replicas/`](https://github.com/samanhappy/mcphub/tree/main/examples/kubernetes/replicas) contains an independent setup Job, a two-replica Deployment, shared ConfigMaps, a Service/Ingress and a PodDisruptionBudget. It uses the official `samanhappy/mcphub:latest` image; before production deployment, set **both** `setup-job.yaml` and `deployment.yaml` to the same fixed version or digest.

Provide a reachable PostgreSQL database with pgvector support and setup permissions. Database provisioning/backups are external to these manifests. To expose the example Ingress, provide a Traefik controller with a `websecure` entrypoint, DNS and a TLS Secret named `mcphub-tls` in namespace `mcphub`. Replace `mcphub.example.com` in `configmap.yaml` and `service.yaml` with your public hostname before setup. The read-only seed configuration starts with no servers and OAuth disabled; manage shared configuration through the dashboard/API after setup.

```sh theme={null}
# From the repository root; create secrets only for a NEW installation.
cd examples/kubernetes/replicas
kubectl create namespace mcphub
umask 077
cat > .env <<EOF
DB_URL=postgresql://mcphub:REPLACE_ME@postgres.example.com:5432/mcphub
JWT_SECRET=$(openssl rand -hex 32)
MCPHUB_CREDENTIAL_ENCRYPTION_KEY=$(openssl rand -base64 32)
EOF
cat > .env.local <<EOF
ADMIN_PASSWORD=$(openssl rand -hex 24)
EOF
# Edit DB_URL in .env to the actual connection string before creating the Secret.
kubectl -n mcphub create secret generic mcphub-secrets --from-env-file=.env
kubectl -n mcphub create secret generic mcphub-admin --from-env-file=.env.local
kubectl -n mcphub apply -f configmap.yaml
kubectl -n mcphub apply -f setup-job.yaml
kubectl -n mcphub wait --for=condition=complete job/mcphub-setup --timeout=600s && \
  kubectl -n mcphub apply -f deployment.yaml
```

Only continue after the Job succeeds. On failure, inspect `kubectl -n mcphub logs job/mcphub-setup` and resolve it before creating serving pods; `backoffLimit: 0` prevents automatic schema retries. Keep one setup Job active at most; do not run schema preparation in each pod's init container. Retain the existing database and shared secrets on upgrades. The initial administrator password is in `.env.local`; only the Job receives that Secret. Keep both local secret files private and preserve the JWT/encryption keys with database backups.

```sh theme={null}
# Ensure mcphub-tls exists in this namespace before exposing HTTPS.
kubectl -n mcphub rollout status deployment/mcphub --timeout=600s
kubectl -n mcphub apply -f service.yaml
kubectl -n mcphub get pods -l app.kubernetes.io/name=mcphub -o wide
kubectl -n mcphub get endpointslices -l kubernetes.io/service-name=mcphub
curl --fail https://mcphub.example.com/readyz
```

The [startup/liveness probes](https://kubernetes.io/docs/concepts/workloads/pods/probes/) use `/livez`; readiness uses `/readyz` so a database outage removes pods from routing without causing liveness restarts. Tune the ten-minute startup budget and resource limits for your upstreams. Pod names identify replicas. The scheduler prefers spreading pods across hosts; provide at least two worker nodes for host redundancy. The PDB keeps one replica during voluntary evictions, but does not prevent crashes or explicit scale-down. A five-second preStop delay allows endpoint changes to propagate before SIGTERM; the 30-second termination grace period includes that delay and MCPHub's ten-second HTTP drain. Tune these values for your ingress propagation time; active sessions still disconnect on pod loss.

The example uses [Traefik's Service cookie affinity](https://doc.traefik.io/traefik/reference/routing-configuration/kubernetes/ingress/#on-service), routing directly to ready pod endpoints. Stateful clients must save and resend `mcphub_affinity` starting with initialization; this requires HTTPS because the cookie is Secure. Clients that cannot preserve cookies need a controller-specific stable header route or a fixed pod endpoint. Other ingress controllers require their own affinity configuration; changing `ingressClassName` alone is insufficient. Do not add buffering or retry middleware for MCP requests; set the controller's request/response timeouts for long-lived SSE connections. These manifests do not install or configure the controller itself.

For an upgrade with schema changes, back up the database and stop serving pods before preparing the new schema. Edit both image references to the same new fixed version, then use a maintenance window:

```sh theme={null}
kubectl -n mcphub scale deployment/mcphub --replicas=0
kubectl -n mcphub wait --for=delete pod -l app.kubernetes.io/name=mcphub --timeout=120s
kubectl -n mcphub delete job mcphub-setup --ignore-not-found
kubectl -n mcphub apply -f setup-job.yaml
kubectl -n mcphub wait --for=condition=complete job/mcphub-setup --timeout=600s && \
  kubectl -n mcphub apply -f deployment.yaml
kubectl -n mcphub rollout status deployment/mcphub --timeout=600s
```

Leave replicas stopped if setup fails. RollingUpdate settings protect image-only rollouts when the schema and configuration are compatible; they do not make arbitrary schema changes safe to roll out concurrently. Kubernetes does not share stdio processes, cookie jars or local files between pods. Cluster, database and ingress availability remain deployment responsibilities.

## Configuration and authorization

Database triggers advance a shared revision in the same transaction as configuration writes, including imports. Nodes check every five seconds and reconcile changed server definitions while preserving unchanged connections. The desired and applied revisions refer to loaded configuration, not successful connectivity to every upstream. An unavailable upstream is reported as degraded separately.

Configuration API reads return `X-MCPHub-Config-Version` and an `ETag`. Send the read ETag in `If-Match` when saving: a stale revision returns `412`. Concurrent requests without a precondition are serialized and return `409` if the revision changes while waiting; callers that omit `If-Match` cannot detect edits made before their request began. Reload and review conflicts instead of automatically resubmitting them. Failed configuration API transactions roll back before responding.

Tool invocation reads current server access and enabled state from PostgreSQL, independently of the polling interval. Shared authorization failures deny invocation. OAuth authorization codes and registration management tokens are shared, expire, and are stored by hash; authorization code consumption is atomic. Credential-scoped PostgreSQL locks serialize OAuth client operations that may rotate tokens, and conditional writes reject obsolete credential updates. These operations require additional pool connections: leave headroom beyond the number of simultaneous OAuth operations.

Configuration application failures retain the previous applied revision and make readiness fail until reconciliation succeeds. Database outages also prevent new invocations. There is no offline authorization mode.

## Load balancer and session affinity

Disable response buffering for SSE and allow sufficiently long response timeouts. Do not enable proxy retries for tool calls with unknown outcomes. Replica mode disables MCPHub's reconnect-and-replay tool retry and OpenAPI 401 replay; an upstream may still enforce its own protocol authentication behavior.

Choose a stable workflow identifier before initialization. The reference Nginx configuration uses `X-MCPHub-Affinity`, falling back to `X-MCPHub-State-Id`, for consistent hashing. The affinity value only selects a node; authentication remains mandatory. Hashing only `Mcp-Session-Id` does not route the initial sessionless handshake consistently. Clients that cannot send an affinity header can use a fixed node endpoint (`/nodes/a/` or `/nodes/b/` in the reference configuration); after failure, select a live node and initialize again. Keep OAuth callback/public resource URLs on the external installation address.

Modern notification subscriptions also belong to their serving process. Nodes broadcast configuration changes locally when they reconcile; this version does not provide a durable, cluster-wide upstream notification bus. Clients should rediscover after reconnecting.

## Health and diagnostics

| Endpoint | Contract |
| - | - |
| `GET /livez` | Process is alive; database failure does not request a restart. |
| `GET /readyz` | Initialization complete, accepting traffic, configuration applied, and database reachable. |
| `GET /health` | Existing database and upstream health summary. |
| `GET /api/cluster` | Authenticated administrator diagnostics: node ID, desired/applied revisions and last reconciliation error. |

Responses identify the node and request through `X-MCPHub-Node-Id` and `X-Request-Id`. Replica logs carry node/request context; metrics include node identity and desired/applied configuration revision. Restrict diagnostics and metrics to operators.

On shutdown, admission stops, HTTP requests drain for up to ten seconds, and then upstream clients are closed. Configure the orchestrator grace period longer than ten seconds and remove a node from routing before stopping it. Long-lived subscriptions are disconnected at the deadline; clients reconnect. Upstream tool side effects may already have completed when a connection fails: never assume failure means nothing happened.

## Reproducible Docker verification

```sh theme={null}
pnpm test:replicas
```

The test builds a backend-only Node 22 image from the locked dependencies, starts two replicas, PostgreSQL, Nginx and a controlled OpenAPI upstream, and checks configuration convergence, write conflicts, cross-node OAuth, revocation, affinity, response loss, database failure, node loss and draining. It removes its containers and temporary database on exit. The fixture credentials and HTTP endpoints under `tests/docker/replicas/` are for disposable local verification only. The test image omits the dashboard, Python and uv; use the normal project Dockerfile for those deployment capabilities.

This version does not include cross-region disaster recovery, seamless session migration, exactly-once tool execution, a separate control-plane service, or shared stdio/filesystem state. PostgreSQL and the load balancer need their own availability arrangements.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.