Swimburger

How to run a .NET Core console app as a service using Systemd on Linux (RHEL)

Niels Swimberghe

Niels Swimberghe - - .NET

Follow me on Twitter, buy me a coffee

.NET Core logo + Dotnet bot wearing Red Hat

This article has been updated for .NET 6 and RHEL 8 on 03/20/2022.

This article walks us through running a .NET Core console application on systemd. After running a simple console app as a service, we'll upgrade to the worker template which is designed for long running services/daemons. Lastly, we'll add the systemd package for increased integration with systemd.

To learn how to run ASP.NET Core services (web stuff) on Linux, check out "How to run ASP.NET Core as a service on Linux without reverse proxy, no NGINX or Apache".

Prerequisites:

This walkthrough should work for most .NET Core supported Linux distributions, not just RHEL.

This walkthrough should work for older versions of .NET too, but you'll need to make small modifications.

.NET Core console application #

Let's start by making a new console application using the dotnet CLI:

Verify that the console app works by running dotnet run. The output should be "Hello World!".
If the application works, we can publish it somewhere logical such as /srv/HelloWorld:

The published result contains an executable called 'HelloWorld' which will run the application. Let's verify we can also run the published application:

To run services on Linux, Systemd uses 'service unit configuration' files to configure services.
Let's create the file 'HelloWorld.service' inside our project so we can store it in source control along with our code. Add the following content to the file:

Make sure to update the 'User' to your username. Refer to the comments in the file for a basic explanation. For more in-depth information, read the freedesktop manual page or the Red Hat documentation.

Depending on how you installed .NET, the path to the dotnet executable (/usr/bin/dotnet) may be different. You can use the command `which dotnet` to find the correct path.
Alternatively, you can use `dotnet --info` which will list information about the .NET installation, and dig through the files to find the correct `dotnet` executable path.

Systemd expects all configuration files to be put under '/etc/systemd/system/'. Copy the service configuration file to '/etc/systemd/system/HelloWorld.service'. Then tell systemd to reload the configuration files, and start the service.

Using the `systemctl status` command we can view the status of the service:

In addition to the status command, we can use the 'journalctl' command to read everything our service is printing to the console. Using the unit-flag (-u), we can filter down to our HelloWorld service.

The console app only logs "Hello world!" to the console and then exits. When querying the status, systemd reports the service is inactive (dead). That's because the console app starts, runs, and immediately exits. That's not very useful, so let's add some code that will let the app run until told to stop. Update Program.cs with the following content:

Let's publish the app again:

Now we have a minimal application that is continuously running until told to stop.
If the application stops due to a crash, systemd will not automatically restart the service unless we configure that. Add the 'Restart' & 'RestartSec' options to HelloWorld.service:

Copy the service file, reload, and restart the service:

Now the service will automatically restart in case of a crash. But when the OS reboots, the application will not automatically start. To enable automatic startup of the service on boot, run the following command:

This console app works fine, but Microsoft has provided the worker template which is a more robust solution for long running services/daemons. Let's upgrade to using the worker template next.

.NET Core worker template #

Let's create a new empty directory and create the worker using the dotnet CLI:

Verify the worker is functional using the command dotnet run.
If the application works, publish it somewhere logical such as /srv/WorkerApp:

Let's verify we can also run the published application:

Create a service unit configuration file called "WorkerApp.service" inside our project:

Copy the service configuration file to /etc/systemd/system/WorkerApp.service and tell systemd to reload the configuration files:

Using 'journalctl', we can verify that the application is contentiously running successfully. The following 'journalctl' command will follow the output of the application. Use Ctrl-C to exit the command.

The .NET Core worker now runs as a systemd service, but the integration between .NET Core and systemd can bi improved on by installing the systemd-integration.

Adding .NET Core Systemd integration #

Microsoft recently added a package to better integrate with systemd. The .NET Core application will notify systemd when it's ready and when it's stopping. Additionally, systemd will now understand the different log levels when the .NET Core application logs to output.

Using the dotnet CLI, add the 'Microsoft.Extensions.Hosting.Systemd' (nuget) package:

Next, we'll need to add one line to the 'Program.cs', `.UseSystemd()`:

Lastly, we need to update our service unit configuration file to specify 'type=Notify':

Let's publish, reload, and restart the service:

With the Systemd integration in place, we can now use the priority-flag (-p) on 'journalctl' to filter the output according to the log levels below:

LogLevel Syslog level systemd name
Trace/Debug 7 debug
Information 6 info
Warning 4 warning
Error 3 err
Critical 2 crit

For example, the following command will only print output with log level 4 and below:

We won't see much because there's nothing being logged as a warning, error, or critical.
Update the 'WorkerApp.cs' file to include 'LogWarning', 'LogError', 'LogCritical' and republish:

Republish and restart the service:

When we run the same 'journalctl' command, we can now see the warning output as bold white text, the error and critical output as red bold text.

The 'UseSystemd' function will not do anything when run outside of a systemd service.
The implementation checks if the OS is a Unix system and whether the parent process is systemd.
If not, the systemd integration is skipped.

Summary #

.NET Core has good support for running services on Linux. Using the worker template, we can create a long running service/daemon that integrates well with systemd. Using the systemd hosting integration, systemd is notified when the .NET Core application is ready & also understand the different log levels in .NET.

The setup for ASP.NET Core apps is very similar, though it'll take some extra steps to ensure the web application is accessible to other machines in your network or the internet.

Related Posts

Related Posts