{"page":{"agent_metadata":{"content_type":"guide","outputs":["deploy-multi-cluster-temporal-on-minikube","configure-cross-cluster-networking","manage-independent-temporal-instances"],"prerequisites":["temporal-ha-cluster","minikube-advanced"]},"categories":["workflow-orchestration"],"content_plain":"Multiple Temporal Servers on Minikube# Running two independent Temporal Server instances locally lets you develop and test cross-cluster patterns \u0026ndash; worker bridges, namespace replication, and multi-region failover \u0026ndash; without cloud infrastructure. This article walks through deploying two Temporal clusters on minikube using profiles and connecting them over Docker networking.\nAll configuration files and Makefile targets reference the companion repository at github.com/statherm/temporal-examples in the multi-cluster/ directory.\nWhy Multiple Clusters?# A single Temporal cluster handles most use cases. You need multiple clusters when:\nIsolation boundaries. Different teams or environments (dev, staging, prod) run on separate clusters so a bad deployment or runaway workflow in one cannot affect the others. Namespaces provide logical isolation; separate clusters provide physical isolation.\nRegional deployment. Workflows that must run close to their data or users. A cluster in us-east processes US customer workflows; a cluster in eu-west processes EU customer workflows. Cross-cluster communication bridges these regions when needed.\nBlast radius reduction. If one Temporal cluster\u0026rsquo;s database fails, only the workflows on that cluster are affected. Other clusters continue operating independently.\nCompliance boundaries. Data residency requirements may mandate that certain workflow histories never leave a specific geographic or network boundary. Separate clusters enforce this at the infrastructure level.\nArchitecture Overview# The local setup runs two completely independent Temporal stacks:\n┌─────────────────────────────────┐ ┌─────────────────────────────────┐ │ minikube: temporal-cluster-a │ │ minikube: temporal-cluster-b │ │ │ │ │ │ ┌────────────┐ ┌────────────┐ │ │ ┌────────────┐ ┌────────────┐ │ │ │ PostgreSQL │ │ Temporal │ │ │ │ PostgreSQL │ │ Temporal │ │ │ │ │ │ Server │ │ │ │ │ │ Server │ │ │ │ Port: 5432 │ │ gRPC: 7233│ │ │ │ Port: 5432 │ │ gRPC: 7233│ │ │ └────────────┘ │ Web: 8080│ │ │ └────────────┘ │ Web: 8080│ │ │ └────────────┘ │ │ └────────────┘ │ │ │ │ │ │ Host ports: 7233, 8080 │ │ Host ports: 7234, 8081 │ └─────────────────────────────────┘ └─────────────────────────────────┘ │ Docker network: minikube-a │ │ Docker network: minikube-b │ └────────────┬───────────────┘ └────────────┬───────────────┘ │ Docker bridge │ └──────────────────────────────────────┘Each cluster has its own PostgreSQL instance, its own Temporal Server, and its own minikube profile. They share nothing except the Docker daemon and, optionally, a bridged network.\nResource Planning# Running two Temporal clusters with PostgreSQL is resource-intensive. Minimum requirements:\nResource Minimum Recommended CPU cores 8 12 RAM 16 GB 24 GB Disk 40 GB 60 GB Minikube profiles share the host\u0026rsquo;s Docker daemon, so container images pulled for Cluster A are available to Cluster B without re-downloading. This saves significant disk space and startup time.\nCheck your available resources before starting:\n# macOS sysctl -n hw.ncpu sysctl -n hw.memsize | awk \u0026#39;{print $0/1073741824 \u0026#34; GB\u0026#34;}\u0026#39; # Linux nproc free -h | grep Mem | awk \u0026#39;{print $2}\u0026#39;Setting Up Cluster A# Create the first minikube profile with enough resources for Temporal and PostgreSQL:\nminikube start \\ --profile=temporal-cluster-a \\ --cpus=4 \\ --memory=8192 \\ --driver=docker \\ --kubernetes-version=v1.28.3Switch to the profile and install the infrastructure:\n# Set kubectl context minikube profile temporal-cluster-a # Add Helm repos helm repo add bitnami https://charts.bitnami.com/bitnami helm repo add temporal https://charts.temporal.io helm repo update # Install PostgreSQL helm install postgresql bitnami/postgresql \\ --set auth.postgresPassword=temporal \\ --set auth.database=temporal \\ --set primary.persistence.size=8Gi \\ --wait # Wait for PostgreSQL to be ready kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=postgresql --timeout=120s # Create Temporal databases kubectl exec -it postgresql-0 -- psql -U postgres -c \u0026#34;CREATE DATABASE temporal_visibility;\u0026#34; # Install Temporal Server helm install temporal temporal/temporal \\ --set server.replicaCount=1 \\ --set cassandra.enabled=false \\ --set mysql.enabled=false \\ --set postgresql.enabled=false \\ --set schema.setup.enabled=false \\ --set schema.update.enabled=false \\ --set server.config.persistence.default.driver=sql \\ --set server.config.persistence.default.sql.driver=postgres12 \\ --set server.config.persistence.default.sql.host=postgresql \\ --set server.config.persistence.default.sql.port=5432 \\ --set server.config.persistence.default.sql.database=temporal \\ --set server.config.persistence.default.sql.user=postgres \\ --set server.config.persistence.default.sql.password=temporal \\ --set server.config.persistence.visibility.driver=sql \\ --set server.config.persistence.visibility.sql.driver=postgres12 \\ --set server.config.persistence.visibility.sql.host=postgresql \\ --set server.config.persistence.visibility.sql.port=5432 \\ --set server.config.persistence.visibility.sql.database=temporal_visibility \\ --set server.config.persistence.visibility.sql.user=postgres \\ --set server.config.persistence.visibility.sql.password=temporal \\ --wait --timeout=300sSet up port forwarding for Cluster A:\n# gRPC (for Temporal clients and workers) kubectl port-forward svc/temporal-frontend 7233:7233 \u0026amp; # Web UI kubectl port-forward svc/temporal-web 8080:8080 \u0026amp;Verify the cluster is healthy:\ntemporal operator cluster health --address localhost:7233 temporal operator namespace list --address localhost:7233Access the Web UI at http://localhost:8080.\nSetting Up Cluster B# The process is identical but uses a different profile name and different host ports:\nminikube start \\ --profile=temporal-cluster-b \\ --cpus=4 \\ --memory=8192 \\ --driver=docker \\ --kubernetes-version=v1.28.3minikube profile temporal-cluster-b # Install PostgreSQL (same commands as Cluster A) helm install postgresql bitnami/postgresql \\ --set auth.postgresPassword=temporal \\ --set auth.database=temporal \\ --set primary.persistence.size=8Gi \\ --wait kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=postgresql --timeout=120s kubectl exec -it postgresql-0 -- psql -U postgres -c \u0026#34;CREATE DATABASE temporal_visibility;\u0026#34; # Install Temporal Server (same Helm values as Cluster A) helm install temporal temporal/temporal \\ --set server.replicaCount=1 \\ --set cassandra.enabled=false \\ --set mysql.enabled=false \\ --set postgresql.enabled=false \\ --set schema.setup.enabled=false \\ --set schema.update.enabled=false \\ --set server.config.persistence.default.driver=sql \\ --set server.config.persistence.default.sql.driver=postgres12 \\ --set server.config.persistence.default.sql.host=postgresql \\ --set server.config.persistence.default.sql.port=5432 \\ --set server.config.persistence.default.sql.database=temporal \\ --set server.config.persistence.default.sql.user=postgres \\ --set server.config.persistence.default.sql.password=temporal \\ --set server.config.persistence.visibility.driver=sql \\ --set server.config.persistence.visibility.sql.driver=postgres12 \\ --set server.config.persistence.visibility.sql.host=postgresql \\ --set server.config.persistence.visibility.sql.port=5432 \\ --set server.config.persistence.visibility.sql.database=temporal_visibility \\ --set server.config.persistence.visibility.sql.user=postgres \\ --set server.config.persistence.visibility.sql.password=temporal \\ --wait --timeout=300sPort forwarding for Cluster B uses different host ports:\n# gRPC on 7234 (not 7233) kubectl port-forward svc/temporal-frontend 7234:7233 \u0026amp; # Web UI on 8081 (not 8080) kubectl port-forward svc/temporal-web 8081:8080 \u0026amp;Verify Cluster B:\ntemporal operator cluster health --address localhost:7234 temporal operator namespace list --address localhost:7234Access Cluster B\u0026rsquo;s Web UI at http://localhost:8081.\nDocker Network Bridging# When using the Docker driver, each minikube profile creates its own Docker network. By default, these networks are isolated \u0026ndash; pods in Cluster A cannot reach pods in Cluster B. For cross-cluster communication, you need to bridge them.\nFirst, identify the Docker networks:\ndocker network ls | grep minikube # Output: # abc123 temporal-cluster-a bridge local # def456 temporal-cluster-b bridge localConnect each minikube container to the other cluster\u0026rsquo;s network:\n# Get the minikube container names CLUSTER_A_CONTAINER=$(docker ps --filter \u0026#34;name=temporal-cluster-a\u0026#34; --format \u0026#34;{{.Names}}\u0026#34;) CLUSTER_B_CONTAINER=$(docker ps --filter \u0026#34;name=temporal-cluster-b\u0026#34; --format \u0026#34;{{.Names}}\u0026#34;) # Connect Cluster A\u0026#39;s container to Cluster B\u0026#39;s network docker network connect temporal-cluster-b \u0026#34;$CLUSTER_A_CONTAINER\u0026#34; # Connect Cluster B\u0026#39;s container to Cluster A\u0026#39;s network docker network connect temporal-cluster-a \u0026#34;$CLUSTER_B_CONTAINER\u0026#34;After bridging, pods in either cluster can reach the other cluster\u0026rsquo;s minikube node IP. Find the node IPs:\n# Cluster A\u0026#39;s IP minikube ip --profile temporal-cluster-a # Cluster B\u0026#39;s IP minikube ip --profile temporal-cluster-bTo reach Cluster B\u0026rsquo;s Temporal Frontend from within Cluster A, use \u0026lt;cluster-b-ip\u0026gt;:\u0026lt;nodeport\u0026gt;. You will need a NodePort service or use the minikube IP with port forwarding.\nDNS Considerations# Kubernetes DNS is cluster-local. A pod in Cluster A cannot resolve temporal-frontend.default.svc.cluster.local for Cluster B. Use IP addresses or set up CoreDNS stub zones pointing to the other cluster\u0026rsquo;s DNS server. For local development, IP addresses are simpler.\nVerifying Both Clusters# Run a quick health check across both clusters:\n#!/bin/bash echo \u0026#34;=== Cluster A ===\u0026#34; temporal operator cluster health --address localhost:7233 temporal operator namespace list --address localhost:7233 echo \u0026#34;\u0026#34; echo \u0026#34;=== Cluster B ===\u0026#34; temporal operator cluster health --address localhost:7234 temporal operator namespace list --address localhost:7234Both should report SERVING status and show the default namespace.\nMakefile Targets# The companion repository provides Makefile targets for managing both clusters:\nPROFILE_A := temporal-cluster-a PROFILE_B := temporal-cluster-b .PHONY: cluster-a-up cluster-b-up clusters-up clusters-down clusters-status cluster-a-up: minikube start --profile=$(PROFILE_A) --cpus=4 --memory=8192 --driver=docker minikube profile $(PROFILE_A) $(MAKE) _install-temporal cluster-b-up: minikube start --profile=$(PROFILE_B) --cpus=4 --memory=8192 --driver=docker minikube profile $(PROFILE_B) $(MAKE) _install-temporal clusters-up: cluster-a-up cluster-b-up $(MAKE) _bridge-networks clusters-down: minikube delete --profile=$(PROFILE_A) minikube delete --profile=$(PROFILE_B) clusters-status: @echo \u0026#34;=== Profiles ===\u0026#34; @minikube profile list @echo \u0026#34;\u0026#34; @echo \u0026#34;=== Cluster A Pods ===\u0026#34; @kubectl --context=$(PROFILE_A) get pods @echo \u0026#34;\u0026#34; @echo \u0026#34;=== Cluster B Pods ===\u0026#34; @kubectl --context=$(PROFILE_B) get pods clusters-pause: minikube stop --profile=$(PROFILE_A) minikube stop --profile=$(PROFILE_B) clusters-resume: minikube start --profile=$(PROFILE_A) minikube start --profile=$(PROFILE_B) $(MAKE) _bridge-networks _install-temporal: helm repo add bitnami https://charts.bitnami.com/bitnami --force-update helm repo add temporal https://charts.temporal.io --force-update helm install postgresql bitnami/postgresql \\ --set auth.postgresPassword=temporal \\ --set auth.database=temporal \\ --set primary.persistence.size=8Gi --wait kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=postgresql --timeout=120s kubectl exec -it postgresql-0 -- psql -U postgres -c \u0026#34;CREATE DATABASE temporal_visibility;\u0026#34; helm install temporal temporal/temporal -f helm/temporal-values.yaml --wait --timeout=300s _bridge-networks: docker network connect $(PROFILE_B) $$(docker ps --filter \u0026#34;name=$(PROFILE_A)\u0026#34; --format \u0026#34;{{.Names}}\u0026#34;) docker network connect $(PROFILE_A) $$(docker ps --filter \u0026#34;name=$(PROFILE_B)\u0026#34; --format \u0026#34;{{.Names}}\u0026#34;) Run make clusters-up to bring up both clusters with networking in one command. Run make clusters-pause when you are done for the day \u0026ndash; this is far faster than make clusters-down and preserves all state.\nResource Management Tips# Two Temporal clusters consume significant resources. Manage them carefully:\nPause when not in use. minikube stop --profile=temporal-cluster-a pauses the VM/container without deleting anything. Resume with minikube start --profile=temporal-cluster-a. This is the single most effective way to reclaim resources.\nMonitor disk usage. Each profile\u0026rsquo;s Docker volumes accumulate over time. Check usage with:\ndocker system df minikube ssh --profile=temporal-cluster-a -- df -h /Delete vs stop. minikube delete removes everything \u0026ndash; containers, volumes, configuration. Use it only when you want a fresh start. minikube stop preserves all state for later resume.\nShare images across profiles. Since both profiles use the same Docker daemon, images pulled for one profile are available to both. Run minikube image load only once.\nTroubleshooting# Insufficient Resources# If pods stay in Pending state, check resource availability:\nkubectl describe node | grep -A 5 \u0026#34;Allocated resources\u0026#34;Reduce Temporal\u0026rsquo;s resource requests in the Helm values if needed. The companion repository\u0026rsquo;s helm/temporal-values.yaml uses minimal resource requests suitable for local development.\nDocker Network Conflicts# If docker network connect fails with \u0026ldquo;already connected\u0026rdquo;, the bridge is already in place. If it fails with a subnet conflict, remove the existing bridge and recreate:\ndocker network disconnect temporal-cluster-b \u0026#34;$CLUSTER_A_CONTAINER\u0026#34; docker network connect temporal-cluster-b \u0026#34;$CLUSTER_A_CONTAINER\u0026#34;Port Collisions# If port forwarding fails with \u0026ldquo;address already in use\u0026rdquo;, find and kill the existing forwarder:\nlsof -ti:7233 | xargs kill -9 lsof -ti:8080 | xargs kill -9Profile Confusion# The most common mistake is running commands against the wrong cluster. Always verify your current context:\nminikube profile list kubectl config current-contextUse explicit --profile and --context flags rather than relying on the default context.\nNext Steps# With two clusters running, you are ready to build cross-cluster communication patterns. Cross-Cluster Communication: Architecture and Patterns covers the approaches \u0026ndash; namespace replication, worker bridges, and workflow-level coordination. Building a Worker Bridge implements the bridge pattern on this infrastructure.\nFor background on single-cluster HA deployment, see Temporal High Availability. For minikube fundamentals, see Minikube Setup and Minikube Multi-Cluster Profiles.\n","date":"2026-02-22","description":"Deploy two independent Temporal Server instances on minikube using profiles, with cross-cluster Docker network bridging for local multi-cluster development and testing.","lastmod":"2026-02-22","levels":["advanced"],"reading_time_minutes":8,"section":"knowledge","skills":["multi-cluster-temporal-deployment","minikube-profiles","cross-cluster-networking"],"tags":["temporal","multi-cluster","minikube","kubernetes","helm","networking","advanced-deployment"],"title":"Multiple Temporal Servers on Minikube: Multi-Cluster Setup","tools":["temporal","minikube","helm","kubectl","docker"],"url":"https://agent-zone.ai/knowledge/workflow-orchestration/temporal-multi-cluster-minikube/","word_count":1692}}