# Kubernetes

> Run TX Text Control .NET Server in Kubernetes: Deployment and Service manifests, session affinity for the Document Editor, a separate backend and licensing.

- **Product:** TX Text Control .NET Server
- **Section:** Documentation
- **Kind:** Topic
- **Path:** TX Text Control .NET Server > Deployment Guide > Kubernetes
- **2756 words**
- **Web URL:** https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/kubernetes/
- **LLMs URL:** https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/kubernetes/llms.txt
- **LLMs-Full URL:** https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/kubernetes/llms-full.txt

---

A Kubernetes cluster runs the container image of an application with TX Text Control .NET Server like any other web application, with two differences: an editing session of the Document Editor lives in one pod, and the backend process it talks to listens on a TCP port inside that pod. Both shape the manifests, from the ports of a Deployment to the session affinity of the component in front of it.

The manifests below deploy the image from [Dockerfile](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/docker/dockerfile/llms-full.txt), first with the backend inside the application pods and then as a separate Deployment.

### What You Deploy

```text
Ingress or Gateway, with cookie affinity
    |
    v
Service myapp (ClusterIP, port 80 -> 8080)
    |
    v
Pods of the Deployment: your application, each with its Document Editor backend
```

- **The image** contains the application, the engine, the fonts and the Document Editor backend. Build and push it as described in [Dockerfile](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/docker/dockerfile/llms-full.txt).
- **No secret for TX Text Control at run time.** The license is compiled into your assembly when you build it, and the credentials of the Text Control Private NuGet Feed are only needed during the image build.
- **A runtime license per pod.** Each pod that runs TX Text Control needs one. See [Runtime Licenses](https://docs.textcontrol.com/tx-text-control-dotnet-server/installation/licensing/runtime/#cloud-and-container-deployments).

### Deploy the Application

The Deployment runs three replicas of the image, and the Service makes them reachable in the cluster:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: myregistry.example.com/myapp:1.0.0
          ports:
            - containerPort: 8080
          env:
            - name: DocumentEditorWorker__ListenAddress
              value: "127.0.0.1"
            - name: DocumentEditorWorker__Port
              value: "4283"
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
```

The manifests work as follows:

- **`containerPort: 8080`** is the port the .NET container images listen on. The Service maps its port 80 to it.
- **The environment variables** bind the Document Editor backend to the loopback address of the pod, so that only the application in the same pod reaches it. They set the `DocumentEditorWorker` section, which *Program.cs* binds to `DocumentEditorWorkerOptions`, as [Prepare the Application](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/linux/ubuntu/#prepare-the-application) shows. Pass the same values to `UseTXWebSocketMiddleware`.
- **The probes** need a health endpoint in the application. Add one in *Program.cs*:

```csharp
builder.Services.AddHealthChecks();

// Reachable at /healthz for the probes of Kubernetes
app.MapHealthChecks("/healthz");
```

1. **A liveness probe on the application doesn't notice a stopped backend.** The application keeps answering requests, and only the editors fail. Watch the log of the pods for messages of the backend, which the application forwards with the prefix `[TXTextControl.Web.Server.Core]`.

2. **<code>replicas: 3</code>** means three runtime licenses.

### Keep an Editing Session on One Pod

The Service distributes requests over all pods. An editor that reconnects after a lost connection has to reach the pod that holds its session, as [Scaling Containers](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/docker/scaling/llms-full.txt) describes, so the component in front of the Service has to keep each browser on one pod:

- **An ingress controller or a Gateway API implementation with cookie-based affinity** is the usual place for this. Which settings it offers depends on the implementation. The ingress-nginx controller that many examples use is retired, and Kubernetes points to the Gateway API instead.
- **In Azure Kubernetes Service**, Application Gateway for Containers forwards WebSocket connections without configuration when it's used through the Gateway API, and it offers session affinity with a cookie. See [Session affinity](https://learn.microsoft.com/en-us/azure/application-gateway/for-containers/session-affinity) and [WebSockets](https://learn.microsoft.com/en-us/azure/application-gateway/for-containers/websockets).
- **The Service itself** offers only `sessionAffinity: ClientIP`, which is `None` by default. It keys on the address of the client, so browsers that share an address, for example behind a company proxy, land on the same pod, and a changing address breaks the affinity. Use it for traffic inside the cluster, not for browsers.
- **Idle connections.** An open editor that nobody types in sends nothing for a while. Set the idle timeout of the ingress or load balancer higher than the interval at which the application sends WebSocket ping frames; `WebSocketOptions.KeepAliveInterval` is two minutes by default, as [Add the Middleware to the Pipeline](https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/websockets/#add-the-middleware-to-the-pipeline) describes.

### Run the Backend in Its Own Deployment

The Document Editor backend can run in pods of its own, built as described in [Multiple Processes](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/docker/multiple-processes/llms-full.txt). Its image already starts the backend on `0.0.0.0:4283`:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: txbackend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: txbackend
  template:
    metadata:
      labels:
        app: txbackend
    spec:
      containers:
        - name: txbackend
          image: myregistry.example.com/mybackend:1.0.0
          ports:
            - containerPort: 4283
          livenessProbe:
            tcpSocket:
              port: 4283
            periodSeconds: 60
---
apiVersion: v1
kind: Service
metadata:
  name: txbackend
spec:
  selector:
    app: txbackend
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800
  ports:
    - protocol: TCP
      port: 4283
      targetPort: 4283
```

In the application, remove `AddHostedService<DocumentEditorWorkerManager>()` and the environment variables above, and connect the middleware to the Service:

```csharp
app.UseTXWebSocketMiddleware("txbackend", 4283);
```

What matters here:

- **The middleware resolves the name once,** when the application starts, and the Service must exist at that moment. The cluster IP of a Service stays the same while the Service exists, so replaced backend pods don't force a restart of the application pods.
- **`sessionAffinity: ClientIP`** keeps each application pod on one backend pod. The client of this Service is the application pod, not the browser, so its address is stable. The timeout defaults to 10800 seconds and accepts at most 86400.
- **The TCP probe** only opens a connection to port 4283. It shows that the backend is listening, not that an editor can work with it, and a long `periodSeconds` keeps the number of these connections small.
- **Every backend pod also needs a runtime license,** because TX Text Control runs in it.

### When Pods Stop

A pod that stops takes its editing sessions with it: Kubernetes sends `SIGTERM`, the application shuts down and stops the backend and the worker processes with the documents of all open editors. This affects rolling updates, scaling in and node maintenance alike. Save documents regularly, as [Scaling Containers](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/docker/scaling/#when-containers-stop-or-are-replaced) describes.

### Next Steps

- [Scaling Containers](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/docker/scaling/llms-full.txt): which requests need affinity, and which don't.
- [Multiple Processes](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/docker/multiple-processes/llms-full.txt): the image of the separate backend.
- [WebSocket Architecture](https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/websockets/#host-the-connection): the connection of the editor through proxies.
- [Performance and Scalability](https://docs.textcontrol.com/tx-text-control-dotnet-server/performance/llms-full.txt): memory and sizing for many concurrent editors.

---

## Nearby

- **Previous:** [Scaling Containers](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/docker/scaling/llms-full.txt)
- **Next:** [Azure](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/azure/llms-full.txt)
