Ubuntu
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 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:
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 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 theDocumentEditorWorkersection beforeAddHostedServiceregisters the backend.DocumentEditorWorkerManagerpasses the address and port to the backend process when it starts it.UseForwardedHeaderscomes 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 Release linux-x64 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.

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 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 /var/www/myapp
sudo cp ~/myapp/. /var/www/myapp/
sudo chown 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:
WorkingDirectoryis the application folder, so that the application reads its appsettings.json.ExecStartstarts the DLL with dotnet. The application starts the backend with thedotnetcommand 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, anddotnetisn'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'".Userruns the application, the backend and its worker processes as www-data, which owns the files.ASPNETCORE_URLSsets 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 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
sudo nginx 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
- Open the site in a browser. The page with the Document Editor shows the editor with its ribbon.
- 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. - Check with
sudo ss -ltnpthat the backend listens on127.0.0.1:4283and the application on127.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
UpgradeandConnectionheaders. - "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
- Debian: the same deployment on Debian 12 and 13.
- Linux Requirements: the libraries, ports and processes a Linux server needs.
- Document Editor Architecture: the backend process and its configuration.
- Docker: run the same application in a Linux container.