> ## 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.

# Mise à l'échelle horizontale

> Exécuter plusieurs réplicas Ayakaleaf Pro derrière un répartiteur de charge avec des sessions persistantes.

Un seul réplica utilise le CPU d'un seul nœud pour chaque compilation : sur nos nœuds de test dotés de 2 vCPU, six compilations simultanées ont pris trois fois plus de temps qu'une seule, tandis que les autres nœuds restaient inactifs. Plusieurs réplicas répartissent les utilisateurs, et leurs compilations, sur les nœuds.

Ceci suit la conception de la [mise à l'échelle horizontale](https://ayakaleaf-pro.mintlify.site/fr/on-premises/maintenance/horizontal-scaling.md) d'Overleaf, transposée à Kubernetes :

| Exigence | Ici |
| - | - |
| MongoDB et Redis partagés | `10-mongo.yaml` et `20-redis.yaml` issus de l'installation |
| Secrets partagés | le Secret `overleaf-secrets` de l'[étape 2](https://ayakaleaf-pro.mintlify.site/fr/on-premises/installation/using-kubernetes/2.-create-the-namespace-and-secrets.md) les contient déjà |
| Fichiers et historique des projets dans **S3** (pas NFS) | n'importe quel service S3, voir [S3](https://ayakaleaf-pro.mintlify.site/fr/on-premises/configuration/kubernetes/s3.md) ; Garage y sert d'exemple interne au cluster |
| Un disque local par réplica pour les compilations et les téléversements | `hostPath` `/srv/overleaf-ha` sur chaque nœud |
| Routage persistant, WebSockets, délais d'attente de 10 min, téléversements de 50 Mo | HAProxy avec la configuration d'exemple d'Overleaf |
| git-bridge en instance unique à côté d'un réplica | git-bridge avec `overleaf-0` comme voisin (sibling) |

> [!WARNING]
>
> Prévoyez S3 dès le départ. Une instance qui conserve déjà ses fichiers sur son volume doit d'abord effectuer la [migration vers S3](https://ayakaleaf-pro.mintlify.site/fr/on-premises/maintenance/s3-migration.md). Cette configuration remplace le Deployment de l'étape 5 : supprimez-le (`kubectl delete -f 40-overleaf.yaml`) avant d'appliquer le StatefulSet.

**1. Stockage S3**

Créez les trois buckets et les deux utilisateurs comme décrit dans [S3](https://ayakaleaf-pro.mintlify.site/fr/on-premises/configuration/kubernetes/s3.md), avec votre service S3 ou avec Garage dans le cluster, et placez les quatre clés dans le Secret `overleaf-s3`. La configuration ci-dessous utilise l'exemple Garage de cette page.

**2. Configuration**

Cette ConfigMap remplace `30-config.yaml`. Elle ajoute S3 et git-bridge, et fait pointer `OVERLEAF_SITE_URL` vers le répartiteur de charge :

```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. Réplicas**

```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, et non Deployment.** Chaque réplica obtient un nom stable (`overleaf-0`, `overleaf-1`, ...) auquel se réfèrent le cookie du répartiteur de charge et git-bridge, et `OrderedReady` démarre d'abord `overleaf-0` seul, de sorte qu'un seul réplica exécute les migrations de base de données.
* **Un réplica par nœud.** Chaque réplica lance ses compilations via le Docker de son nœud et les conserve dans le répertoire `/srv/overleaf-ha` de son nœud.
* **`V1_HISTORY_URL` par réplica.** Chaque réplica distribue des liens d'historique portant son propre nom, afin que git-bridge puisse les atteindre.

**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
```

Consultez [Intégration Git](https://ayakaleaf-pro.mintlify.site/fr/on-premises/configuration/kubernetes/integrations/git-integration.md) pour comprendre pourquoi `GIT_BRIDGE_HOST` est une adresse IP fixe.

**5. Répartiteur de charge**

La configuration HAProxy est celle de l'exemple d'Overleaf avec cinq modifications : `/project/<id>/mcp` routé vers `overleaf-0` comme `/git/` (voir [Serveur MCP](https://ayakaleaf-pro.mintlify.site/fr/on-premises/configuration/kubernetes/integrations/mcp-server.md#several-replicas)), HTTP simple sur le port `8080` (l'image s'exécute avec un utilisateur non root, qui ne peut pas se lier au port 80 sous containerd), des serveurs trouvés via le DNS du cluster (les IP des pods changent à chaque redémarrage), un DaemonSet pour que chaque nœud réponde, et `balance leastconn` au lieu du `roundrobin` par défaut (voir les questions-réponses ci-dessous).

```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` conserve les adresses IP des utilisateurs dans `X-Forwarded-For`.

**6. TLS**

Terminez HTTPS dans HAProxy comme décrit dans [Proxy TLS](https://ayakaleaf-pro.mintlify.site/fr/on-premises/configuration/kubernetes/tls-proxy.md), puis définissez `OVERLEAF_SITE_URL` sur l'adresse `https://` et `OVERLEAF_SECURE_COOKIE: "true"` dans `60-config.yaml`.

### À quoi s'attendre

Nous avons testé trois réplicas sur trois nœuds dotés chacun de 2 vCPU et 4 Go, avec MongoDB, Redis, Garage, git-bridge et HAProxy sur les mêmes nœuds :

* 12 utilisateurs compilant simultanément un document de 600 pages : les 12 ont compilé et récupéré leur PDF, à raison de 4 utilisateurs par réplica ; un cycle a pris de 8 à 18 secondes. Chaque nœud a conservé au moins 1,3 Go de mémoire disponible.
* Chaque utilisateur reste sur un seul réplica (cookie `server-pro-ha`), y compris pour le téléchargement du PDF, qui doit provenir du réplica qui l'a compilé. Il en va de même en HTTPS, y compris pour le WebSocket de l'éditeur.
* Lorsqu'un réplica s'arrête, ses utilisateurs reçoivent des erreurs pendant environ 15 à 20 secondes, jusqu'à ce que HAProxy le marque comme indisponible ; ils passent alors sur un autre réplica et restent connectés, car les sessions sont stockées dans Redis.

### Questions-réponses

> [accordion] Pourquoi `balance leastconn` ?
>
> **Réponse :** Avec l'exemple d'Overleaf (`roundrobin`, un HAProxy par nœud), 12 nouveaux utilisateurs se sont répartis à raison de 8, 3 et 1 sur les trois réplicas lors de notre test, et le réplica le plus chargé a fait attendre les utilisateurs les plus lents 29 secondes. Avec `leastconn`, qui privilégie le réplica ayant le moins de connexions ouvertes (chaque éditeur garde un WebSocket ouvert), le même test a donné 4, 4 et 4, et 18 secondes. La persistance est inchangée : elle provient du cookie.

> [accordion] Puis-je utiliser mon contrôleur d'ingress à la place de HAProxy ?
>
> **Réponse :** Oui, s'il route selon un cookie vers un réplica stable, prend en charge les WebSockets, autorise des durées de requête de 10 minutes et des téléversements de 50 Mo, maintient son délai keep-alive en dessous de 65 secondes, envoie `X-Forwarded-For` et `X-Forwarded-Proto`, et envoie `/git/` et `/project/<id>/mcp` vers `overleaf-0`. Routez vers les noms propres des réplicas (`overleaf-N.overleaf-hl`), et non vers un Service qui répartit chaque requête.

> [accordion] Comment ajouter un quatrième réplica ?
>
> **Réponse :** Ajoutez un nœud avec Docker et les images TeX Live, définissez `replicas: 4` dans `70-overleaf.yaml`, et ajoutez une ligne `server overleaf-3 ...` à la configuration HAProxy (et incrémentez `config-hash` pour que HAProxy redémarre).


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