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

# Mở rộng theo chiều ngang

> Chạy nhiều replica Ayakaleaf Pro phía sau một load balancer với sticky session.

Một replica dùng CPU của một node cho mọi lần biên dịch: trên các node kiểm thử có 2 vCPU của chúng tôi, sáu lần biên dịch đồng thời mất thời gian gấp ba lần so với một lần, trong khi các node khác vẫn nhàn rỗi. Nhiều replica sẽ phân bổ người dùng, cùng các lần biên dịch của họ, lên các node.

Cách làm này tuân theo thiết kế [Mở rộng theo chiều ngang](https://ayakaleaf-pro.mintlify.site/vi/on-premises/maintenance/horizontal-scaling.md) của Overleaf, được chuyển sang Kubernetes:

| Yêu cầu | Ở đây |
| - | - |
| MongoDB và Redis dùng chung | `10-mongo.yaml` và `20-redis.yaml` từ bước cài đặt |
| Secret dùng chung | Secret `overleaf-secrets` từ [bước 2](https://ayakaleaf-pro.mintlify.site/vi/on-premises/installation/using-kubernetes/2.-create-the-namespace-and-secrets.md) đã có sẵn chúng |
| Tệp dự án và lịch sử trên **S3** (không phải NFS) | bất kỳ dịch vụ S3 nào, xem [S3](https://ayakaleaf-pro.mintlify.site/vi/on-premises/configuration/kubernetes/s3.md); Garage ở đó là ví dụ chạy trong cluster |
| Một ổ đĩa cục bộ cho mỗi replica để biên dịch và tải lên | `hostPath` `/srv/overleaf-ha` trên mỗi node |
| Định tuyến cố định, WebSocket, timeout 10 phút, tải lên 50 MB | HAProxy với cấu hình mẫu của Overleaf |
| git-bridge chạy đơn lẻ cạnh một replica | git-bridge với `overleaf-0` làm sibling |

> [!WARNING]
>
> Hãy lên kế hoạch dùng S3 ngay từ đầu. Một phiên bản đang lưu tệp trên volume của mình cần thực hiện [di chuyển sang S3](https://ayakaleaf-pro.mintlify.site/vi/on-premises/maintenance/s3-migration.md) trước. Thiết lập này thay thế Deployment từ bước 5: hãy xóa nó (`kubectl delete -f 40-overleaf.yaml`) trước khi áp dụng StatefulSet.

**1. Lưu trữ S3**

Thiết lập ba bucket và hai người dùng như mô tả trong [S3](https://ayakaleaf-pro.mintlify.site/vi/on-premises/configuration/kubernetes/s3.md), với dịch vụ S3 của bạn hoặc với Garage trong cluster, và đưa bốn khóa vào Secret `overleaf-s3`. Cấu hình bên dưới dùng ví dụ Garage từ trang đó.

**2. Cấu hình**

ConfigMap này thay thế `30-config.yaml`. Nó bổ sung S3 và git-bridge, đồng thời trỏ `OVERLEAF_SITE_URL` tới 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. Các replica**

```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, không phải Deployment.** Mỗi replica có một tên cố định (`overleaf-0`, `overleaf-1`, ...) mà cookie của load balancer và git-bridge tham chiếu tới, và `OrderedReady` khởi động `overleaf-0` một mình trước, nên chỉ một replica chạy các migration cơ sở dữ liệu.
* **Một replica cho mỗi node.** Mỗi replica khởi chạy các lần biên dịch qua Docker của node mình và lưu chúng trong `/srv/overleaf-ha` của node đó.
* **`V1_HISTORY_URL` cho từng replica.** Mỗi replica cung cấp các liên kết lịch sử với tên riêng của mình, để git-bridge có thể truy cập chúng.

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

Xem [Tích hợp Git](https://ayakaleaf-pro.mintlify.site/vi/on-premises/configuration/kubernetes/integrations/git-integration.md) để biết lý do `GIT_BRIDGE_HOST` là một địa chỉ IP cố định.

**5. Load balancer**

Cấu hình HAProxy là bản mẫu của Overleaf với năm thay đổi: `/project/<id>/mcp` được định tuyến tới `overleaf-0` giống như `/git/` (xem [MCP Server](https://ayakaleaf-pro.mintlify.site/vi/on-premises/configuration/kubernetes/integrations/mcp-server.md#several-replicas)), HTTP thường trên cổng `8080` (image chạy dưới người dùng không phải root, vốn không thể bind cổng 80 trong containerd), các máy chủ được tìm qua DNS của cluster (IP của pod thay đổi mỗi lần khởi động lại), một DaemonSet để mọi node đều phản hồi, và `balance leastconn` thay cho `roundrobin` mặc định (xem phần Hỏi & Đáp bên dưới).

```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` giữ địa chỉ IP của người dùng trong `X-Forwarded-For`.

**6. TLS**

Kết thúc HTTPS tại HAProxy như mô tả trong [Proxy TLS](https://ayakaleaf-pro.mintlify.site/vi/on-premises/configuration/kubernetes/tls-proxy.md), sau đó đặt `OVERLEAF_SITE_URL` thành địa chỉ `https://` và `OVERLEAF_SECURE_COOKIE: "true"` trong `60-config.yaml`.

### Kết quả mong đợi

Chúng tôi đã kiểm thử ba replica trên ba node, mỗi node có 2 vCPU và 4 GB, với MongoDB, Redis, Garage, git-bridge và HAProxy trên cùng các node:

* 12 người dùng biên dịch một tài liệu 600 trang cùng lúc: cả 12 đều biên dịch và lấy được PDF của mình, 4 người dùng trên mỗi replica; mỗi lượt mất từ 8 đến 18 giây. Mỗi node luôn còn ít nhất 1,3 GB bộ nhớ khả dụng.
* Mỗi người dùng ở lại trên một replica (cookie `server-pro-ha`), bao gồm cả việc tải PDF vốn phải đến từ replica đã biên dịch nó. Điều tương tự cũng đúng qua HTTPS, bao gồm cả WebSocket của trình soạn thảo.
* Khi một replica dừng, người dùng của nó gặp lỗi trong khoảng 15 đến 20 giây, cho đến khi HAProxy đánh dấu replica đó là ngừng hoạt động; sau đó họ chuyển sang replica khác và vẫn giữ trạng thái đăng nhập, vì session nằm trong Redis.

### Hỏi & Đáp

> [accordion] Tại sao dùng `balance leastconn`?
>
> **Trả lời:** Với bản mẫu của Overleaf (`roundrobin`, một HAProxy trên mỗi node), trong bài kiểm thử của chúng tôi 12 người dùng mới bị phân bổ thành 8, 3 và 1 trên ba replica, và replica bận nhất khiến những người dùng chậm nhất phải chờ 29 giây. Với `leastconn`, vốn ưu tiên replica có ít kết nối mở nhất (mỗi trình soạn thảo giữ một WebSocket mở), cùng bài kiểm thử cho kết quả 4, 4 và 4 và 18 giây. Tính bám dính không thay đổi: nó đến từ cookie.

> [accordion] Tôi có thể dùng ingress controller của mình thay cho HAProxy không?
>
> **Trả lời:** Có, nếu nó định tuyến theo cookie tới một replica cố định, hỗ trợ WebSocket, cho phép thời gian yêu cầu 10 phút và tải lên 50 MB, giữ keep-alive timeout dưới 65 giây, gửi `X-Forwarded-For` và `X-Forwarded-Proto`, và gửi `/git/` cùng `/project/<id>/mcp` tới `overleaf-0`. Hãy định tuyến tới tên riêng của các replica (`overleaf-N.overleaf-hl`), không phải tới một Service cân bằng tải mọi yêu cầu.

> [accordion] Làm thế nào để thêm replica thứ tư?
>
> **Trả lời:** Thêm một node có Docker và các image TeX Live, đặt `replicas: 4` trong `70-overleaf.yaml`, và thêm một dòng `server overleaf-3 ...` vào cấu hình HAProxy (và tăng `config-hash` để HAProxy khởi động lại).


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