In this article, we are going to learn how to run a .NET application as a Linux service.
So let’s get started.
.NET Support for Linux Service
Linux operating system contains a service manager called Systemd
. The Systemd
acts as an initialization system and controls what programs and services run when the system boots up. While working with Linux, we can make use of Systemd
to manage our services.
.NET supports hosting applications as a Systemd
service using the Microsoft.Extensions.Hosting.Systemd
NuGet package. Once we integrate our application with Systemd
using this package, Systemd
becomes aware of the application events like application start, stop, etc. Additionally, this configures the application logs to be sent to the Journald
, which is the logging system of Systemd
and it even understands the log priorities.
With that said, let’s proceed to create a Systemd
service in .NET and see it in action.
Creating the Project
Let’s start by following the same steps that we explained for creating a worker service project in the Running .NET Core Applications as a Windows Service article. We can create a new worker service project using the dotnet new worker
command:
dotnet new worker --name <project name>
As discussed in the linked article, the worker template will create a project with 2 files – the Program
class and the Worker
class. These classes will contain the default code that comes as part of the worker service template. But don’t worry, in the next section, we are going to learn and apply the changes required to make this worker service run as a Linux Systemd
service.
Configuring the Project
Once we create the project, the next step is to add the Microsoft.Extensions.Hosting.Systemd
NuGet package:
dotnet add package Microsoft.Extensions.Hosting.Systemd
This NuGet package provides the support for hosting the application as a Linux Systemd
service.
After adding the package, we need to call the AddSystemd()
method in the Program
class:
var builder = Host.CreateApplicationBuilder(args); builder.Services.AddSystemd(); builder.Services.AddHostedService<Worker>(); IHost host = builder.Build(); await host.RunAsync();
The AddSystemd()
method will prepare the worker service for running as a Linux Systemd
service by making a few changes to it:
- It will set the host lifetime to
SystemdLifetime
. - This will provide notification messages for events like the application start, stop, etc to the
Systemd
. - Additionally, this will configure the console logging in the
Systemd
format as well.
However, remember that these settings are context-aware and will only activate if it detects that the process is running as a Systemd
service.
After that, let’s modify the ExecuteAsync()
method of the Worker
class to add some logging. In this example, for illustration purposes, let’s log an Information, Warning, and Error:
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { _logger.LogInformation("Code-Maze Service running at: {time}", DateTimeOffset.Now); _logger.LogWarning("A warning from Code-Maze Service at: {time}", DateTimeOffset.Now); _logger.LogError("An error from Code-Maze Service at: {time}", DateTimeOffset.Now); await Task.Delay(1000, stoppingToken); } }
This will help us later to understand how the Linux system captures and differentiates different types of logs while this worker service runs as a Systemd
service.
With that, our application is ready to run as a Linux Systemd
service.
Publishing the Service
For publishing the worker service, we can follow the same steps that we performed while publishing the application in the Running .NET Core Applications as a Windows Service article.
The only change that we need to do here is to change the Target Runtime to linux-x64
as we are planning to host this service in a Linux system. For all other settings, let’s keep the same values that we provided in the linked article. After providing the settings, let’s publish the service to a local folder.
Once the publish succeeds, it will produce a self-contained output file that is compatible with the Linux system. Now we can create a Linux service using this output file.
Creating the Linux Service
For creating a Linux Service, first, we need to create the configurations in a Systemd
unit file. A Systemd
unit file will contain the information regarding the unit, which is a service in this case. This unit file for services should have the .service
extension and contain the information about the service and how to run it, etc. Let’s create a plain text file and name it codemaze-worker.service
:
[Unit] Description=codemaze-worker [Service] Type=notify ExecStart=/usr/sbin/CodeMazeLinuxWorker [Install] WantedBy=multi-user.target
The [Unit]
section defines the metadata for the service. In this example, we have provided a name for the service using the Description
directive.
The [Service]
section provides configurations that are specific to services. The Type=notify
indicates that the service will provide a notification when it starts, stops, etc. The ExecStart
specifies the full path and the arguments of the command to be executed to start the process. Remember that we should copy the published output file of our worker service to this path.
The [Install]
section defines the behavior of a unit like whether it is enabled or disabled. The WantedBy
directive specifies how a unit should be enabled. In this example, we just set the Systemd
service to start in a multi-user environment, which will be the most common scenario.
After creating the Systemd
unit file, we need to place it in the /etc/systemd/system
Linux system’s directory.
Assigning Permissions for the Linux Service
Let’s make some quick tweaks to our worker service file to ensure everything runs smoothly.
First, we are going to make the worker service file executable so that we can run it as a program:
sudo chmod +x /usr/sbin/CodeMazeLinuxWorker
The chmod
command stands for change mode and we can use it to manage file and directory permissions on Linux.
Next, let’s make sure that the current user has permission on the worker service file:
sudo chown <currentuser> /usr/sbin/CodeMazeLinuxWorker
The chown
command stands for change owner and we can use it to give the ownership of a file or directory to the specified user.
Remember that to run system commands in Linux, we must delegate a system administrator privilege using the sudo
command.
Once we create the service unit file and copy the published service to the ExecStart
path specified inside it, we need to reload the Systemd
services using the systemctl daemon-reload
command:
sudo systemctl daemon-reload
With that, our service is up and running. Now we are going to verify it.
Verifying the Linux Service
We can use the systemctl status
command and pass the service name as the argument to verify the service:
For verifying the service, we can use the systemctl status
command and pass the service name as the argument:
sudo systemctl status codemaze-worker
This will provide a similar output indicating that the service is loaded, but in a disabled state:
● codemaze-worker.service - codemaze-worker Loaded: loaded (/etc/systemd/system/codemaze-worker.service; disabled; vendor preset: disabled) Active: inactive (dead)
To start the service, we can use the systemctl start
command and pass the service name as the argument:
sudo systemctl start codemaze-worker
Since we specified Type=notify
for the service, the Systemd
will be notified when the service starts. Let’s check the status once again using the systemctl status
command:
sudo systemctl status codemaze-worker
This will produce a similar result which contains logs from the service:
● codemaze-worker.service - codemaze-worker Loaded: loaded (/etc/systemd/system/codemaze-worker.service; disabled; vendor preset: disabled) Active: active (running) since Wed 2022-07-13 06:12:57 UTC; 44s ago Main PID: 8487 (CodeMazeLinuxWo) CGroup: /system.slice/codemaze-worker.service └─8487 /usr/sbin/CodeMazeLinuxWorker Jul 13 06:13:38 service-test-linux CodeMazeLinuxWorker[8487]: CodeMazeLinuxWorker.Worker[0] An error from Code-Maz...:00Jul 13 06:13:39 service-test-linux CodeMazeLinuxWorker[8487]: ... Some lines were ellipsized, use -l to show in full.
Note that the log messages may be trimmed and for seeing the full message, we can append the -l
argument at the end:
sudo systemctl status codemaze-worker -l
This will show the full output from the service:
● codemaze-worker.service - codemaze-worker Loaded: loaded (/etc/systemd/system/codemaze-worker.service; disabled; vendor preset: disabled) Active: active (running) since Wed 2022-07-13 06:12:57 UTC; 1min 20s ago Main PID: 8487 (CodeMazeLinuxWo) CGroup: /system.slice/codemaze-worker.service └─8487 /usr/sbin/CodeMazeLinuxWorker Jul 13 06:14:14 service-test-linux CodeMazeLinuxWorker[8487]: CodeMazeLinuxWorker.Worker[0] An error from Code-Maze Service at: 07/13/2022 06:14:14 +00:00 Jul 13 06:14:15 service-test-linux CodeMazeLinuxWorker[8487]: CodeMazeLinuxWorker.Worker[0] Code-Maze Service running at: 07/13/2022 06:14:15 +00:00 Jul 13 06:14:15 service-test-linux CodeMazeLinuxWorker[8487]: CodeMazeLinuxWorker.Worker[0] A warning from Code-Maze Service at: 07/13/2022 06:14:15 +00:00 ...
Alternately, we can use the journalctl
utility for querying and displaying logs from the system journal. One of the benefits of using Systemd
is the centralized logging into the system journal that we can access with journalctl
command. While using this utility, we can provide the -u
argument to specify the name of the service as the unit:
sudo journalctl -u codemaze-worker
This will display all the logs from the service codemaze-service
:
The logging provided by journalctl
is different from the one that we get from the terminal and is well formatted with each message shown on different lines. Also, note that it highlights the warnings in yellow and errors in red.
It is also possible to filter the messages based on priority using the -p
argument. For instance, we can filter just the errors by using priority 3:
sudo journalctl -p 3 -u codemaze-worker
Similarly, we can use priority 4 for filtering warnings, 6 for information, etc.
Conclusion
In this article, we learned how to create a .NET worker service that is optimized for running as a Linux Service. Additionally, we looked at how to configure it as a Linux Service and verified its behavior and logging capabilities.