Skip to main content
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:
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:
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/: 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.
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:
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/ 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.
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.
The startup/liveness 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, 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:
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

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

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.