Skip to main content

From Docker Compose to Multi-Replica Kubernetes: Scaling Zerq Without Rewriting Anything

Move Zerq from Docker Compose to multi-replica Kubernetes with rolling updates and HA data stores, while gateway config and policies stay identical.

  • architecture
  • kubernetes
  • docker
  • deployment
  • operations
Zerq team

The standard recommendation for Zerq deployments is: Docker Compose for development and early environments, Kubernetes for production. This is not about Kubernetes being harder or better — it is about what each tool is actually good at.

Docker Compose is excellent for spinning up a complete local stack in seconds, iterating on configuration, and running integration tests without cluster overhead. It also suits small environments where a single host carries the load, as long as you accept that availability is yours to own. Kubernetes gives you multi-replica horizontal scaling, rolling updates with health checks, auto-scaling on load metrics, and the declarative infrastructure model that production environments need.

The question teams always ask is: "how much do I have to rewrite when I move?" The answer is: the orchestration manifests, and nothing else. Gateway configuration, API products, policies, workflows, and credentials all live in MongoDB — they travel with you to Kubernetes without modification.

This post is the concrete path, not the abstract principle.

The Compose baseline

A minimal Docker Compose setup for Zerq includes four services:

services:
  gateway:
    image: zerq/gateway:latest
    ports:
      - "8080:8080"   # API traffic
      - "8443:8443"   # API traffic (TLS)
    environment:
      MONGODB_URI: mongodb://mongodb:27017/zerq
      REDIS_URL: redis://redis:6379
    depends_on:
      - mongodb
      - redis

  management:
    image: zerq/management:latest
    ports:
      - "3000:3000"
    environment:
      MONGODB_URI: mongodb://mongodb:27017/zerq
    depends_on:
      - mongodb

  mongodb:
    image: mongo:7
    volumes:
      - mongodb_data:/data/db

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  mongodb_data:
  redis_data:

This runs correctly on a developer laptop. The gateway serves traffic, the management UI is available for configuration, MongoDB persists config and audit data, and Redis handles distributed rate limit state.

What this does not provide: HA, rolling updates, horizontal scaling, health check integration with load balancers, pod auto-scaling, or resource limits that prevent a runaway service from taking down the host. Compose is fine for dev; these properties are what you get from Kubernetes.

What moves to Kubernetes unchanged

Before the Kubernetes manifests, the important point: everything in MongoDB moves unchanged.

When you migrate from Compose to Kubernetes, you are changing how containers are scheduled and how traffic reaches them. You are not changing:

  • The API products (collections, proxies, endpoints) you have configured
  • The access policies and client credentials you have issued
  • The workflow definitions attached to your proxies
  • The audit records accumulated since you started

If you are migrating a development environment to a staging or production Kubernetes cluster, you can either:

  1. Point the new deployment at the same MongoDB instance (if network-accessible)
  2. Export and restore MongoDB data to a new MongoDB deployment using standard mongodump / mongorestore

Either way, the API products your team built in Compose appear in Kubernetes without re-entry.

Two layers, two lifecycles

"Without rewriting" is realistic because the migration touches only one of two layers:

  • Platform config — collections, proxies, workflows, policies, clients — follows your product lifecycle. It means the same thing on a laptop and in a cluster.
  • Orchestration manifests — Deployments, Services, Ingress, HPAs — are infrastructure. They change when you adopt Kubernetes, but they should never force a different authorization model for your APIs.

Your identity and secrets integration patterns sit with the first layer too: the same IdP and the same Vault (or equivalent), not one-off wiring per orchestrator.

The failure mode to avoid is business policy re-entered by hand per cluster. A one-time mongodump covers the initial move; once dev, staging, and production run separate MongoDB instances, promote config between them through the management API rather than by clicking through the UI again. Management MCP lets a CI/CD pipeline do that promotion under a scoped service identity, with every change in the admin audit trail.

The Kubernetes topology

A production Kubernetes deployment typically looks like:

Namespace and separation

apiVersion: v1
kind: Namespace
metadata:
  name: zerq

Run all Zerq components in a dedicated namespace. This scopes RBAC, network policies, and resource quotas cleanly.

Gateway Deployment — multiple replicas

apiVersion: apps/v1
kind: Deployment
metadata:
  name: zerq-gateway
  namespace: zerq
spec:
  replicas: 3
  selector:
    matchLabels:
      app: zerq-gateway
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0   # zero-downtime rolling updates
  template:
    metadata:
      labels:
        app: zerq-gateway
    spec:
      containers:
        - name: gateway
          image: zerq/gateway:latest
          ports:
            - containerPort: 8080
            - containerPort: 8443
          env:
            - name: MONGODB_URI
              valueFrom:
                secretKeyRef:
                  name: zerq-secrets
                  key: mongodb-uri
            - name: REDIS_URL
              valueFrom:
                secretKeyRef:
                  name: zerq-secrets
                  key: redis-url
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "512Mi"
              cpu: "1000m"
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 10

Why maxUnavailable: 0: This ensures rolling updates never take a pod down before a replacement is ready. Combined with Go's fast startup (seconds, not the minutes a JVM might need), new pods reach their readiness probe quickly, so updates proceed without traffic drops.

Why 3 replicas minimum: With 2 replicas, a rolling update (surge 1, unavailable 0) temporarily runs 3 pods. With 3 as the base, you always have 2 healthy pods serving traffic during an update, never 1.

Horizontal Pod Autoscaler

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: zerq-gateway-hpa
  namespace: zerq
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: zerq-gateway
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80

Because the gateway binary is stateless on its own (state lives in MongoDB and Redis), scaling out is clean. New pods join the cluster, connect to the shared data stores, and start serving traffic. No warmup state to synchronise.

Service and Ingress

apiVersion: v1
kind: Service
metadata:
  name: zerq-gateway
  namespace: zerq
spec:
  selector:
    app: zerq-gateway
  ports:
    - name: http
      port: 80
      targetPort: 8080
    - name: https
      port: 443
      targetPort: 8443
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: zerq-gateway-ingress
  namespace: zerq
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.yourdomain.com
      secretName: zerq-tls
  rules:
    - host: api.yourdomain.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: zerq-gateway
                port:
                  number: 80

MongoDB — production options

For production, MongoDB should run as a replica set for HA. You have three practical options:

MongoDB Atlas in your cloud tenant — easiest to operate, available in your region of choice for data residency. Connect via the Atlas connection string in your Kubernetes secret.

MongoDB Community Operator — runs a replica set as a CRD inside your Kubernetes cluster:

apiVersion: mongodbcommunity.mongodb.com/v1
kind: MongoDBCommunity
metadata:
  name: zerq-mongodb
  namespace: zerq
spec:
  members: 3
  type: ReplicaSet
  version: "7.0.0"
  security:
    authentication:
      modes: ["SCRAM"]
  users:
    - name: zerq-app
      db: admin
      passwordSecretRef:
        name: mongodb-password
      roles:
        - name: readWrite
          db: zerq
  statefulSet:
    spec:
      volumeClaimTemplates:
        - metadata:
            name: data-volume
          spec:
            accessModes: ["ReadWriteOnce"]
            storageClassName: fast
            resources:
              requests:
                storage: 50Gi

Self-managed replica set on dedicated VMs — appropriate for environments where Kubernetes does not manage stateful workloads, or where the MongoDB cluster is shared with other services.

Redis — HA with Sentinel or Cluster

For distributed rate limiting, Redis needs to be available across gateway replicas. For production:

# Using Bitnami Redis chart values (helm install)
# redis/values.yaml
architecture: replication
auth:
  enabled: true
  existingSecret: redis-secret
  existingSecretPasswordKey: redis-password
replica:
  replicaCount: 2
sentinel:
  enabled: true
  masterSet: zerq-redis

Redis Sentinel provides automatic failover if the primary goes down. Rate limit counters are ephemeral — if Redis restarts, counters reset. This is a known operational property: a brief window after Redis recovery where clients may exceed their quota before counters rebuild. For most deployments this is acceptable; if it is not, a Redis Cluster topology provides no-downtime failover.

Secrets management

Do not put MongoDB connection strings or Redis passwords in ConfigMaps. Use Kubernetes Secrets at minimum:

kubectl create secret generic zerq-secrets \
  --namespace zerq \
  --from-literal=mongodb-uri="mongodb+srv://user:[email protected]/zerq" \
  --from-literal=redis-url="redis://:password@zerq-redis-sentinel:26379/0"

For environments with a secrets manager (Vault, AWS Secrets Manager, Azure Key Vault), use the External Secrets Operator to synchronise secrets into Kubernetes Secrets without embedding credentials in Kubernetes manifests.

The observability difference at scale

In a single Compose instance, all logs come from one process. In a multi-replica Kubernetes deployment, logs come from multiple pods with different pod IDs. The structured logs that Zerq produces become more valuable at scale because they carry product, partner, and endpoint dimensions — not just pod identity.

When you are debugging elevated error rates on a specific endpoint across a 10-pod deployment, filtering log aggregation by product=payments and status=502 gives you the affected calls across all pods immediately. The observability design that works on one instance works at any replica count.

Make sure your log and metric pipeline adds pod or replica identity as a label alongside those dimensions, not instead of them. You want to answer both "is one pod misbehaving?" and "is the payments product degraded?" from the same data. See Observability for how metrics and logging behave when you scale out.

Cutover checklist

Before production traffic moves to the cluster, confirm:

  1. Shared state — every gateway replica points at the same MongoDB and Redis, so policies and rate limit counters are consistent no matter which pod serves a request.
  2. Data store posture — MongoDB and Redis have the same backup and HA posture as your other tier-1 services, not the single-volume setup from Compose.
  3. Ingress and TLS — TLS terminates where your security model expects it, and clients still see one logical gateway hostname regardless of replica count.
  4. Secrets — connection strings live in Kubernetes Secrets or a synchronised external secrets manager, never in ConfigMaps or manifests.
  5. Observability labels — logs and metrics carry pod identity without losing product, partner, and endpoint dimensions.

What does not change at all

To close the loop on the original question — here is what is identical between your Docker Compose dev environment and your Kubernetes production environment:

ThingSame in ComposeSame in K8s
API products, proxies, endpoints✓✓
Auth policies, rate limits✓✓
Workflow definitions✓✓
Client credentials✓✓
Audit record format✓✓
Management UI behaviour✓✓
MongoDB connection string format✓✓

What changes: docker-compose.yml becomes Deployments, Services, Ingress, HPAs, and a MongoDB replica set configuration. The platform does not know or care about the difference.


See Zerq's architecture page for the full deployment options overview, or request a demo to walk through your specific Kubernetes topology and data store requirements.