> ## Documentation Index
> Fetch the complete documentation index at: https://ayakaleaf-pro.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Horizontale Skalierung

> Mehrere Ayakaleaf-Pro-Replikate hinter einem Load Balancer mit Sticky Sessions betreiben.

Ein Replikat nutzt für jede Kompilierung die CPU eines einzigen Knotens: Auf unseren Testknoten mit 2 vCPUs dauerten sechs gleichzeitige Kompilierungen dreimal so lange wie eine, während die anderen Knoten untätig blieben. Mehrere Replikate verteilen die Benutzer und ihre Kompilierungen auf die Knoten.

Dies folgt Overleafs Konzept zur [horizontalen Skalierung](https://ayakaleaf-pro.mintlify.site/de/on-premises/maintenance/horizontal-scaling.md), übertragen auf Kubernetes:

| Anforderung | Hier |
| - | - |
| Gemeinsam genutztes MongoDB und Redis | `10-mongo.yaml` und `20-redis.yaml` aus der Installation |
| Gemeinsam genutzte Secrets | das Secret `overleaf-secrets` aus [Schritt 2](https://ayakaleaf-pro.mintlify.site/de/on-premises/installation/using-kubernetes/2.-create-the-namespace-and-secrets.md) enthält sie bereits |
| Projektdateien und Verlauf in **S3** (nicht NFS) | ein beliebiger S3-Dienst, siehe [S3](https://ayakaleaf-pro.mintlify.site/de/on-premises/configuration/kubernetes/s3.md); dort Garage als clusterinternes Beispiel |
| Eine lokale Festplatte pro Replikat für Kompilierungen und Uploads | `hostPath` `/srv/overleaf-ha` auf jedem Knoten |
| Dauerhaftes Routing, WebSockets, 10-Minuten-Timeouts, 50-MB-Uploads | HAProxy mit Overleafs Beispielkonfiguration |
| git-bridge als Singleton neben einem Replikat | git-bridge mit `overleaf-0` als Sibling |

> [!WARNING]
>
> Planen Sie S3 von Anfang an ein. Eine Instanz, die bereits Dateien auf ihrem Volume speichert, benötigt zuerst die [S3-Migration](https://ayakaleaf-pro.mintlify.site/de/on-premises/maintenance/s3-migration.md). Dieses Setup ersetzt das Deployment aus Schritt 5: Löschen Sie es (`kubectl delete -f 40-overleaf.yaml`), bevor Sie das StatefulSet anwenden.

**1. S3-Speicher**

Richten Sie die drei Buckets und zwei Benutzer wie unter [S3](https://ayakaleaf-pro.mintlify.site/de/on-premises/configuration/kubernetes/s3.md) beschrieben ein, mit Ihrem S3-Dienst oder mit Garage im Cluster, und tragen Sie die vier Schlüssel in das Secret `overleaf-s3` ein. Die folgende Konfiguration verwendet das Garage-Beispiel von dieser Seite.

**2. Konfiguration**

Diese ConfigMap ersetzt `30-config.yaml`. Sie fügt S3 und git-bridge hinzu und richtet `OVERLEAF_SITE_URL` auf den Load Balancer:

```yaml title="60-config.yaml" wrap theme={null}
apiVersion: v1
kind: ConfigMap
metadata:
  name: overleaf-env
  namespace: overleaf
data:
  OVERLEAF_APP_NAME: Ayakaleaf Pro
  OVERLEAF_SITE_URL: http://192.0.2.10:30081      # the load balancer (HAProxy) URL
  OVERLEAF_ADMIN_EMAIL: admin@example.com
  # Emails: add the sender and the SMTP server together, see Email delivery
  OVERLEAF_MONGO_URL: mongodb://mongo-0.mongo:27017,mongo-1.mongo:27017,mongo-2.mongo:27017/sharelatex?replicaSet=overleaf
  OVERLEAF_REDIS_HOST: redis
  REDIS_HOST: redis
  ENABLED_LINKED_FILE_TYPES: project_file,project_output_file,url
  ENABLE_CONVERSIONS: "true"
  EMAIL_CONFIRMATION_DISABLED: "true"
  OVERLEAF_TRUSTED_PROXY_IPS: loopback,10.244.0.0/16   # HAProxy runs in the pod CIDR
  # Sandboxed compiles, node paths: each replica owns /srv/overleaf-ha on its node
  SANDBOXED_COMPILES: "true"
  SANDBOXED_COMPILES_SIBLING_CONTAINERS: "true"
  DOCKER_RUNNER: "true"
  SANDBOXED_COMPILES_HOST_DIR_COMPILES: /srv/overleaf-ha/data/compiles
  SANDBOXED_COMPILES_HOST_DIR_OUTPUT: /srv/overleaf-ha/data/output
  SANDBOXED_COMPILES_HOST_DIR_CACHE: /srv/overleaf-ha/data/cache
  ALL_TEX_LIVE_DOCKER_IMAGES: ghcr.io/ayaka-notes/texlive-full:2026.1
  ALL_TEX_LIVE_DOCKER_IMAGE_NAMES: TeX Live 2026
  TEX_LIVE_DOCKER_IMAGE: ghcr.io/ayaka-notes/texlive-full:2026.1
  # S3 (required for more than one replica); keys are in the Secret overleaf-s3
  OVERLEAF_FILESTORE_BACKEND: s3
  OVERLEAF_FILESTORE_TEMPLATE_FILES_BUCKET_NAME: overleaf-template-files
  OVERLEAF_FILESTORE_S3_ENDPOINT: http://garage:3900
  OVERLEAF_FILESTORE_S3_PATH_STYLE: "true"
  OVERLEAF_FILESTORE_S3_REGION: garage
  OVERLEAF_HISTORY_BACKEND: s3
  OVERLEAF_HISTORY_PROJECT_BLOBS_BUCKET: overleaf-project-blobs
  OVERLEAF_HISTORY_CHUNKS_BUCKET: overleaf-chunks
  OVERLEAF_HISTORY_S3_ENDPOINT: http://garage:3900
  OVERLEAF_HISTORY_S3_PATH_STYLE: "true"
  OVERLEAF_HISTORY_S3_REGION: garage
  # Git integration: one git-bridge, its sibling is overleaf-0
  GIT_BRIDGE_ENABLED: "true"
  GIT_BRIDGE_HOST: 10.96.100.80      # the git-bridge Service's fixed ClusterIP, see 75-git-bridge.yaml
  GIT_BRIDGE_PORT: "8000"
```

**3. Replikate**

```yaml title="70-overleaf.yaml" wrap theme={null}
apiVersion: v1
kind: Service
metadata:
  name: overleaf-hl
  namespace: overleaf
spec:
  clusterIP: None            # stable DNS per replica: overleaf-0.overleaf-hl, ...
  selector:
    app: overleaf-ha
  ports:
    - {name: http, port: 80}
    - {name: web, port: 3000}       # git-bridge API (sibling only)
    - {name: history, port: 3100}   # history-v1, used by git-bridge via V1_HISTORY_URL
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: overleaf
  namespace: overleaf
spec:
  serviceName: overleaf-hl
  replicas: 3
  # OrderedReady: overleaf-0 runs the migrations alone before the next replica starts
  podManagementPolicy: OrderedReady
  selector:
    matchLabels:
      app: overleaf-ha
  template:
    metadata:
      labels:
        app: overleaf-ha
    spec:
      terminationGracePeriodSeconds: 60
      # Keep the environment to the ConfigMap and Secrets: no REDIS_PORT=tcp://..., GIT_BRIDGE_PORT=tcp://...
      enableServiceLinks: false
      affinity:
        podAntiAffinity:            # one replica per node: each uses that node's Docker and disk
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app: overleaf-ha
              topologyKey: kubernetes.io/hostname
      containers:
        - name: sharelatex
          image: ghcr.io/ayaka-notes/overleaf-pro:6.3.2
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            # Each replica hands out history URLs with its own stable name, so git-bridge can reach them
            - name: V1_HISTORY_URL
              value: http://$(POD_NAME).overleaf-hl.overleaf.svc.cluster.local:3100/api
          envFrom:
            - configMapRef: {name: overleaf-env}
            - secretRef: {name: overleaf-secrets}
            - secretRef: {name: overleaf-s3}
          ports:
            - containerPort: 80
          readinessProbe:
            httpGet: {path: /status, port: 80}
            periodSeconds: 10
          resources:
            requests: {cpu: 500m, memory: 1536Mi}
            limits: {memory: 3Gi}
          volumeMounts:
            - {name: local, mountPath: /var/lib/overleaf}
            - {name: docker-sock, mountPath: /var/run/docker.sock}
      volumes:
        # Local disk for ephemeral files (compiles, output, uploads); the HOST_DIR_* paths are under it
        - name: local
          hostPath: {path: /srv/overleaf-ha, type: DirectoryOrCreate}
        - name: docker-sock
          hostPath: {path: /var/run/docker.sock, type: Socket}
```

* **StatefulSet, nicht Deployment.** Jedes Replikat erhält einen stabilen Namen (`overleaf-0`, `overleaf-1`, ...), auf den sich das Load-Balancer-Cookie und git-bridge beziehen, und `OrderedReady` startet zuerst `overleaf-0` allein, sodass nur ein Replikat die Datenbankmigrationen ausführt.
* **Ein Replikat pro Knoten.** Jedes Replikat startet seine Kompilierungen über das Docker seines Knotens und speichert sie in `/srv/overleaf-ha` seines Knotens.
* **`V1_HISTORY_URL` pro Replikat.** Jedes Replikat gibt Verlaufslinks mit seinem eigenen Namen aus, damit git-bridge sie erreichen kann.

**4. git-bridge**

```yaml title="75-git-bridge.yaml" wrap theme={null}
apiVersion: v1
kind: Service
metadata:
  name: git-bridge
  namespace: overleaf
spec:
  # Fixed ClusterIP (inside the service CIDR): the sharelatex image's nginx proxies /git/ with
  # `resolver 127.0.0.11` (Docker's DNS), which does not exist in Kubernetes, so GIT_BRIDGE_HOST is this IP
  clusterIP: 10.96.100.80
  selector:
    app: git-bridge
  ports:
    - {name: http, port: 8000}
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: git-bridge
  namespace: overleaf
spec:
  serviceName: git-bridge
  replicas: 1
  selector:
    matchLabels:
      app: git-bridge
  template:
    metadata:
      labels:
        app: git-bridge
    spec:
      # Service "git-bridge" would inject GIT_BRIDGE_PORT=tcp://..., which git-bridge reads as its port
      enableServiceLinks: false
      containers:
        - name: git-bridge
          image: ghcr.io/ayaka-notes/overleaf-pro/git-bridge:6.3.2   # same version as the sharelatex image
          command: ["/server-pro-start.sh"]
          securityContext:
            runAsUser: 0
          env:
            - {name: GIT_BRIDGE_API_BASE_URL, value: "http://overleaf-0.overleaf-hl.overleaf.svc.cluster.local:3000/api/v0/"}
            - {name: GIT_BRIDGE_OAUTH2_SERVER, value: "http://overleaf-0.overleaf-hl.overleaf.svc.cluster.local"}
            - {name: GIT_BRIDGE_POSTBACK_BASE_URL, value: "http://git-bridge.overleaf.svc.cluster.local:8000"}
            - {name: GIT_BRIDGE_ROOT_DIR, value: /data/git-bridge}
          ports:
            - containerPort: 8000
          resources:
            requests: {cpu: 100m, memory: 256Mi}
            limits: {memory: 768Mi}
          volumeMounts:
            - {name: data, mountPath: /data/git-bridge}
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: [ReadWriteOnce]
        resources:
          requests:
            storage: 5Gi
```

Unter [Git-Integration](https://ayakaleaf-pro.mintlify.site/de/on-premises/configuration/kubernetes/integrations/git-integration.md) erfahren Sie, warum `GIT_BRIDGE_HOST` eine feste IP-Adresse ist.

**5. Load Balancer**

Die HAProxy-Konfiguration ist Overleafs Beispiel mit fünf Änderungen: `/project/<id>/mcp` wird wie `/git/` an `overleaf-0` geleitet (siehe [MCP-Server](https://ayakaleaf-pro.mintlify.site/de/on-premises/configuration/kubernetes/integrations/mcp-server.md#several-replicas)), einfaches HTTP auf Port `8080` (das Image läuft als Nicht-Root-Benutzer, der unter containerd Port 80 nicht binden kann), Server werden über das Cluster-DNS gefunden (Pod-IPs ändern sich bei jedem Neustart), ein DaemonSet, damit jeder Knoten antwortet, und `balance leastconn` statt des Standards `roundrobin` (siehe Q\&A unten).

```yaml title="80-haproxy.yaml" wrap theme={null}
apiVersion: v1
kind: ConfigMap
metadata:
  name: haproxy-config
  namespace: overleaf
data:
  haproxy.cfg: |
    global
      log stdout format raw local0 info

    resolvers kubedns
      parse-resolv-conf
      hold valid 10s

    defaults
      mode                    http
      # New sessions go to the replica with the fewest open connections (editors keep a WebSocket open)
      balance                 leastconn
      option                  httpchk HEAD /status
      http-check              expect status 200
      default-server          check resolvers kubedns init-addr none
      log                     global
      option                  httplog
      # Reroute to a different backend if the sticky one is down
      option                  redispatch 1
      retries                 3
      # Sticky session for 24h of inactivity -- compile output is deleted after 24h
      cookie                  server-pro-ha insert maxidle 24h
      timeout queue           1m
      timeout connect         15s
      timeout client          1m
      # Allow slow compiles -- hard-coded limit in clsi is 10min
      timeout server          10m
      # Disconnect the editor after 23h
      timeout tunnel          23h
      # Lower than NGINX_KEEPALIVE_TIMEOUT (65s) in the sharelatex container
      timeout http-keep-alive 60s

    listen overleaf
      bind :8080
      option forwardfor
      # When TLS ends in front of HAProxy, also pass:  http-request set-header X-Forwarded-Proto https
      # git requests go to the git-bridge sibling
      use-server overleaf-0 if { path_beg /git/ }
      # MCP clients send no cookie: keep compile, log and PDF download on one replica
      use-server overleaf-0 if { path_reg ^/project/[0-9a-f]{24}/mcp(/|$) }
      http-response add-header X-Served-By %s
      stats enable
      stats uri /haproxy
      server overleaf-0 overleaf-0.overleaf-hl.overleaf.svc.cluster.local:80 cookie overleaf-0
      server overleaf-1 overleaf-1.overleaf-hl.overleaf.svc.cluster.local:80 cookie overleaf-1
      server overleaf-2 overleaf-2.overleaf-hl.overleaf.svc.cluster.local:80 cookie overleaf-2
---
apiVersion: apps/v1
# DaemonSet: one HAProxy per node, so the NodePort answers on every node with externalTrafficPolicy
# Local. The cookie holds the replica name, so stickiness works whichever HAProxy a request hits.
kind: DaemonSet
metadata:
  name: haproxy
  namespace: overleaf
spec:
  selector:
    matchLabels:
      app: haproxy
  template:
    metadata:
      labels:
        app: haproxy
      annotations:
        config-hash: "1"   # bump to restart after editing the ConfigMap
    spec:
      containers:
        - name: haproxy
          image: haproxy:lts-alpine
          ports:
            - containerPort: 8080
          resources:
            requests: {cpu: 50m, memory: 32Mi}
            limits: {memory: 256Mi}
          volumeMounts:
            - {name: config, mountPath: /usr/local/etc/haproxy}
      volumes:
        - name: config
          configMap:
            name: haproxy-config
---
apiVersion: v1
kind: Service
metadata:
  name: haproxy
  namespace: overleaf
spec:
  type: NodePort
  externalTrafficPolicy: Local    # keep the client IP (no SNAT); only nodes running HAProxy answer
  selector:
    app: haproxy
  ports:
    - {name: http, port: 80, targetPort: 8080, nodePort: 30081}
```

```bash theme={null}
kubectl delete -f 40-overleaf.yaml --ignore-not-found
kubectl apply -f 60-config.yaml -f 70-overleaf.yaml -f 75-git-bridge.yaml -f 80-haproxy.yaml
kubectl -n overleaf rollout status statefulset/overleaf --timeout=20m
curl -sI http://192.0.2.10:30081/login | grep -i x-served-by
```

`externalTrafficPolicy: Local` behält die IP-Adressen der Benutzer in `X-Forwarded-For` bei.

**6. TLS**

Terminieren Sie HTTPS in HAProxy wie unter [TLS-Proxy](https://ayakaleaf-pro.mintlify.site/de/on-premises/configuration/kubernetes/tls-proxy.md) beschrieben und setzen Sie dann in `60-config.yaml` `OVERLEAF_SITE_URL` auf die `https://`-Adresse sowie `OVERLEAF_SECURE_COOKIE: "true"`.

### Was Sie erwarten können

Wir haben drei Replikate auf drei Knoten mit jeweils 2 vCPUs und 4 GB getestet, mit MongoDB, Redis, Garage, git-bridge und HAProxy auf denselben Knoten:

* 12 Benutzer, die gleichzeitig ein 600-seitiges Dokument kompilieren: Alle 12 kompilierten und luden ihr PDF ab, 4 Benutzer pro Replikat; eine Runde dauerte 8 bis 18 Sekunden. Auf jedem Knoten blieben mindestens 1,3 GB Arbeitsspeicher verfügbar.
* Jeder Benutzer bleibt auf einem Replikat (Cookie `server-pro-ha`), einschließlich des PDF-Downloads, der von dem Replikat kommen muss, das kompiliert hat. Dasselbe gilt über HTTPS, einschließlich des WebSockets des Editors.
* Wenn ein Replikat stoppt, erhalten seine Benutzer etwa 15 bis 20 Sekunden lang Fehler, bis HAProxy es als ausgefallen markiert; dann wechseln sie zu einem anderen Replikat und bleiben angemeldet, da die Sitzungen in Redis liegen.

### Q\&A

> [accordion] Warum `balance leastconn`?
>
> **Antwort:** Mit Overleafs Beispiel (`roundrobin`, ein HAProxy pro Knoten) verteilten sich in unserem Test 12 neue Benutzer mit 8, 3 und 1 auf die drei Replikate, und das am stärksten ausgelastete Replikat ließ die langsamsten Benutzer 29 Sekunden warten. Mit `leastconn`, das das Replikat mit den wenigsten offenen Verbindungen bevorzugt (jeder Editor hält einen WebSocket offen), ergab derselbe Test 4, 4 und 4 sowie 18 Sekunden. Die Sticky Sessions bleiben unverändert: Sie beruhen auf dem Cookie.

> [accordion] Kann ich statt HAProxy meinen Ingress-Controller verwenden?
>
> **Antwort:** Ja, sofern er per Cookie an ein stabiles Replikat routet, WebSockets unterstützt, Anfragezeiten von 10 Minuten und Uploads von 50 MB erlaubt, sein Keep-Alive-Timeout unter 65 Sekunden hält, `X-Forwarded-For` und `X-Forwarded-Proto` sendet und `/git/` sowie `/project/<id>/mcp` an `overleaf-0` leitet. Routen Sie zu den eigenen Namen der Replikate (`overleaf-N.overleaf-hl`), nicht zu einem Service, der jede Anfrage verteilt.

> [accordion] Wie füge ich ein viertes Replikat hinzu?
>
> **Antwort:** Fügen Sie einen Knoten mit Docker und den TeX-Live-Images hinzu, setzen Sie `replicas: 4` in `70-overleaf.yaml` und fügen Sie der HAProxy-Konfiguration eine Zeile `server overleaf-3 ...` hinzu (und erhöhen Sie `config-hash`, damit HAProxy neu startet).


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