TX Text Control .NET Server

Ubuntu

Topic

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

What Runs on the Server

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 LTSBuilt-in Ubuntu feed
24.04 LTSBuilt-in Ubuntu feed
22.04 LTSUbuntu .NET backports repository

On Ubuntu 22.04, add the backports repository first:

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:

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:

{
  "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:

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.

Publish the Application

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

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

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:

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:

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:

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:

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:

[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:

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:

sudo journalctl -fu kestrel-myapp.service

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

Configure nginx

Install nginx:

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:

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 explains why. Test the configuration and load it:

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:

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 collects further problems.

Next Steps