# Ubuntu

> Deploy an ASP.NET Core app with TX Text Control to Ubuntu: install the ASP.NET Core Runtime, publish for linux-x64, run it with systemd behind nginx.

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

---

An Ubuntu server runs an ASP.NET Core application with TX Text Control .NET Server without any installation of TX Text Control itself: the published application brings the engine, its native Linux libraries, its fonts and the Document Editor backend. What the server needs is the ASP.NET Core Runtime, a service that keeps the application running and a reverse proxy that passes the WebSocket connections of the Document Editor on.

The steps below take an application from the development machine to a running site on Ubuntu with systemd and nginx. They follow Microsoft's guide [Host ASP.NET Core on Linux with Nginx](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx) and add what TX Text Control needs.

### Prerequisites

- **A server with Ubuntu 22.04 LTS, 24.04 LTS or 26.04 LTS** and a user account with sudo privileges. Microsoft supports .NET 10 on these versions.
- **An ASP.NET Core application that uses the Core engine**, for example the application of [Document Editor: ASP.NET Core](https://docs.textcontrol.com/tx-text-control-dotnet-server/getting-started/document-editor/aspnet-core/llms-full.txt). The Classic engine doesn't run on Linux. The commands below use a .NET 10 project named *MyApp*.
- **A runtime license** for the server. See [Runtime Licenses](https://docs.textcontrol.com/tx-text-control-dotnet-server/installation/licensing/runtime/llms-full.txt).

### What Runs on the Server

```text
Browser
    |  HTTP and WebSocket, port 80
    v
nginx
    |  http://127.0.0.1:5000
    v
Your application: dotnet MyApp.dll, managed by systemd
    |  TCP, 127.0.0.1:4283
    v
Document Editor backend: dotnet TXTextControl.Web.Server.Core.dll
    |
    v
One worker process for each open editor
```

systemd starts and monitors only your application. The application starts the Document Editor backend itself through the hosted service `DocumentEditorWorkerManager`, and the backend starts a worker process for each editor that connects. Document processing with `ServerTextControl` and the Document Viewer run inside the application process.

### Install the ASP.NET Core Runtime

Ubuntu publishes .NET in its own package feeds. Where .NET 10 comes from depends on the version:

| Ubuntu | .NET 10 package source |
|---|---|
| 26.04 LTS | Built-in Ubuntu feed |
| 24.04 LTS | Built-in Ubuntu feed |
| 22.04 LTS | Ubuntu .NET backports repository |

On Ubuntu 22.04, add the backports repository first:

```bash
sudo add-apt-repository ppa:dotnet/backports
```

Then install the ASP.NET Core Runtime, which contains both the .NET Runtime and the ASP.NET Core Runtime:

```bash
sudo apt-get update && \
  sudo apt-get install -y aspnetcore-runtime-10.0
```

`dotnet --list-runtimes` lists the installed runtimes. The server needs no SDK. The Document Editor backend is built for .NET 8 and rolls forward to the newest installed major version, so the .NET 10 runtime runs both your application and the backend. Install .NET from one package source only: Microsoft warns that packages from several repositories lead to problems.

### Prepare the Application

Two changes in *Program.cs* prepare the application for a server behind nginx.

#### Bind the Backend to the Local Machine

Without configuration, the backend listens on TCP port 4283 on all network interfaces of the server. Only your application connects to it, so bind it to the loopback address. Add a section to *appsettings.json*:

```json
{
  "DocumentEditorWorker": {
    "ListenAddress": "127.0.0.1",
    "Port": 4283
  }
}
```

#### Accept the Headers of nginx

nginx passes the address of the client and the original scheme in the `X-Forwarded-For` and `X-Forwarded-Proto` headers. The Forwarded Headers Middleware applies them to the request, so that HTTPS redirection and generated links use the address the browser sees.

The complete *Program.cs* of an MVC application with the Document Editor and the Document Viewer:

```csharp
using Microsoft.AspNetCore.HttpOverrides;
using TXTextControl.Web;
using TXTextControl.Web.DocumentEditor.Backend;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

// Read the address and port of the backend process from appsettings.json
builder.Services.Configure<DocumentEditorWorkerOptions>(
    builder.Configuration.GetSection("DocumentEditorWorker"));
builder.Services.AddHostedService<DocumentEditorWorkerManager>();

var app = builder.Build();

// Use the client address and scheme that nginx passes on
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseRouting();

app.UseWebSockets();

// The same address and port as in appsettings.json
app.UseTXWebSocketMiddleware("127.0.0.1", 4283);

app.UseAuthorization();

app.MapStaticAssets();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}")
    .WithStaticAssets();

app.Run();
```

The code works as follows:

- `Configure<DocumentEditorWorkerOptions>` binds the `DocumentEditorWorker` section before `AddHostedService` registers the backend. `DocumentEditorWorkerManager` passes the address and port to the backend process when it starts it.
- `UseForwardedHeaders` comes first, before any middleware that reads the scheme or the client address. nginx runs on the same server, and the middleware trusts proxies on the loopback address by default.
- `UseTXWebSocketMiddleware("127.0.0.1", 4283)` connects the editors to the backend with the address and port from *appsettings.json*. If the values differ, the editor can't connect.
- Everything else is the *Program.cs* of [Configure Program.cs for Views](https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/aspnet-core/#configure-program-cs-for-views).

### Publish the Application

On the development machine, publish the application for Linux in the project folder:

```bash
dotnet publish -c Release -r linux-x64 --self-contained false
```

`-r linux-x64` selects the native Linux libraries of the TX Text Control packages, and `--self-contained false` leaves the .NET runtime out, because the server has it. The output is in *bin/Release/net10.0/linux-x64/publish*. Next to *MyApp.dll*, it contains:

- the TX Text Control assemblies and the native libraries such as *tx34core.so* and *tx34\_pdf.so*,
- the *Fonts* folder, from which the Core engine takes its fonts,
- the Document Editor backend: *TXTextControl.Web.Server.Core.dll*, its configuration file *TXTextControl.Web.Server.Core.config.json* and *TXTextControl.Web.Server.Core.Process.dll* for the worker processes.

In Visual Studio, a folder publish profile does the same: choose **Publish** on the project, select **Folder**, click **Show all settings** in the profile and set **Target runtime** to **linux-x64**, as [Publish From Visual Studio](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/windows/iis/#publish-the-application) shows for Windows.

![The Profile settings dialog of a Visual Studio publish profile with the target framework net8.0, the deployment mode Framework-dependent and the Target runtime drop-down open, with linux-x64 and win-x64 circled.](https://docs.textcontrol.com/assets/img/tx-text-control-dotnet-server/vs-publish-runtime-linux.badc1dc3.webp)

A web project copies *.config* files from its folder into the output. If your project folder contains a *nuget.config* with the credentials of the Text Control Private NuGet Feed, the credentials end up on the server. Keep the file in the folder above the project, where `dotnet restore` still finds it.

### Copy the Application to the Server

Copy the output with a tool such as SCP or SFTP. Web applications on Linux commonly live under */var/www*. From the development machine:

```bash
scp -r bin/Release/net10.0/linux-x64/publish/ user@server:~/myapp
```

On the server, move the files into place and give them to the user that runs the service:

```bash
sudo mkdir -p /var/www/myapp
sudo cp -r ~/myapp/. /var/www/myapp/
sudo chown -R www-data:www-data /var/www/myapp
```

Start the application once by hand to check it:

```bash
cd /var/www/myapp
dotnet MyApp.dll
```

Kestrel listens on `http://localhost:5000` when no address is configured. The output starts with the backend, which listens on the loopback address:

```text
info: TXTextControl.Web.DocumentEditor.Backend.DocumentEditorWorkerManager[0]
      Port 4283 provided via options.
info: TXTextControl.Web.DocumentEditor.Backend.DocumentEditorWorkerManager[0]
      Listen address "127.0.0.1" provided via options.
info: TXTextControl.Web.DocumentEditor.Backend.DocumentEditorWorkerManager[0]
      Started TXTextControl.Web.Server.Core with Process ID: 2154
info: TXTextControl.Web.DocumentEditor.Backend.DocumentEditorWorkerManager[0]
      [TXTextControl.Web.Server.Core] Using listen port 4283 from command-line arguments.
info: TXTextControl.Web.DocumentEditor.Backend.DocumentEditorWorkerManager[0]
      [TXTextControl.Web.Server.Core] Using listen address "127.0.0.1" from command-line arguments.
info: TXTextControl.Web.DocumentEditor.Backend.DocumentEditorWorkerManager[0]
      [TXTextControl.Web.Server.Core] Successfully read config file 'TXTextControl.Web.Server.Core.config.json'.
info: TXTextControl.Web.DocumentEditor.Backend.DocumentEditorWorkerManager[0]
      [TXTextControl.Web.Server.Core] Starting TCP listener on port 4283 for IPv4.
```

Start the application from its folder. ASP.NET Core reads *appsettings.json* from the working directory; started from another folder, the application doesn't find the file, and the backend listens on all interfaces again. Stop the application with Ctrl+C.

### Run the Application as a systemd Service

Create the service file */etc/systemd/system/kestrel-myapp.service*:

```text
[Unit]
Description=MyApp with TX Text Control

[Service]
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/dotnet /var/www/myapp/MyApp.dll
Restart=always
# Restart the service 10 seconds after the application ends unexpectedly
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=myapp
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://127.0.0.1:5000
Environment=DOTNET_NOLOGO=true

[Install]
WantedBy=multi-user.target
```

What matters for TX Text Control:

- **`WorkingDirectory`** is the application folder, so that the application reads its *appsettings.json*.
- **`ExecStart`** starts the DLL with *dotnet*. The application starts the backend with the `dotnet` command as well and finds it next to the running *dotnet* or on the search path. The Ubuntu packages install */usr/bin/dotnet*, which is on the search path of system services. If you start the application through its executable *MyApp* instead, and `dotnet` isn't on the search path, the application ends at startup with "Error starting TXTextControl.Web.Server.Core: An error occurred trying to start process 'dotnet'".
- **`User`** runs the application, the backend and its worker processes as *www-data*, which owns the files.
- **`ASPNETCORE_URLS`** sets the address nginx forwards to.

Enable and start the service, and check its state:

```bash
sudo systemctl enable kestrel-myapp.service
sudo systemctl start kestrel-myapp.service
sudo systemctl status kestrel-myapp.service
```

The application passes the output of the backend to its own logging, so the backend's messages appear in the journal of the service:

```bash
sudo journalctl -fu kestrel-myapp.service
```

When the service stops, the application stops the backend process and its worker processes.

### Configure nginx

Install nginx:

```bash
sudo apt update && sudo apt install nginx
```

Replace the content of */etc/nginx/sites-available/default* with the following configuration and set `server_name` to the domain of your site. The configuration is Microsoft's, and a missing or wrong `server_name` is a security risk:

```text
map $http_connection $connection_upgrade {
  "~*Upgrade" $http_connection;
  default keep-alive;
}

server {
  listen        80;
  server_name   example.com *.example.com;
  location / {
      proxy_pass         http://127.0.0.1:5000/;
      proxy_http_version 1.1;
      proxy_set_header   Upgrade $http_upgrade;
      proxy_set_header   Connection $connection_upgrade;
      proxy_set_header   Host $host;
      proxy_cache_bypass $http_upgrade;
      proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header   X-Forwarded-Proto $scheme;
  }
}
```

The `Upgrade` and `Connection` headers let the WebSocket connection of the Document Editor through; without them, the editor doesn't load. [Host the Connection](https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/websockets/#host-the-connection) explains why. Test the configuration and load it:

```bash
sudo nginx -t
sudo nginx -s reload
```

For HTTPS, configure nginx to listen on port 443 with a certificate from a trusted certificate authority. The application behind nginx stays on HTTP, and the forwarded headers tell it the original scheme. The editor builds its WebSocket address from the address of the page, so on an HTTPS page it connects with `wss`.

### Configure the Firewall

Close all ports that the site doesn't use. With the Uncomplicated Firewall:

```bash
sudo apt-get install ufw
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
```

Allow the SSH port before you enable the firewall, or you lock yourself out of the server. Port 4283 of the backend stays closed.

### Verify the Deployment

1. Open the site in a browser. The page with the Document Editor shows the editor with its ribbon.
2. Watch the journal with `sudo journalctl -fu kestrel-myapp.service`. When the editor connects, the backend logs "Starting web editor worker process with connection ID" followed by the ID of the connection. As long as the site runs without HTTPS, the journal also shows the warning "Failed to determine the https port for redirect.", and no redirection takes place.
3. Check with `sudo ss -ltnp` that the backend listens on `127.0.0.1:4283` and the application on `127.0.0.1:5000`.

If the editor doesn't appear, open the console of the browser's developer tools:

- **"Error during WebSocket handshake: Unexpected response code"** means that nginx doesn't forward the WebSocket connection. Check the `Upgrade` and `Connection` headers.
- **"Could not connect to TextControl TCP server at 127.0.0.1:4283"** means that the middleware can't reach the backend. Compare the address and port in *appsettings.json* and in *Program.cs*, and check the journal for backend errors.

[Troubleshooting Linux](https://docs.textcontrol.com/tx-text-control-dotnet-server/troubleshooting/linux/llms-full.txt) collects further problems.

### Next Steps

- [Debian](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/linux/debian/llms-full.txt): the same deployment on Debian 12 and 13.
- [Linux Requirements](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/linux/requirements/llms-full.txt): the libraries, ports and processes a Linux server needs.
- [Document Editor Architecture](https://docs.textcontrol.com/tx-text-control-dotnet-server/document-editor/architecture/llms-full.txt): the backend process and its configuration.
- [Docker](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/docker/llms-full.txt): run the same application in a Linux container.

---

## Nearby

- **Previous:** [Linux](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/linux/llms-full.txt)
- **Next:** [Debian](https://docs.textcontrol.com/tx-text-control-dotnet-server/deployment/linux/debian/llms-full.txt)
