How to run a .NET Core console app as a service using Systemd on Linux (RHEL)
Tags: dotnet, linux, redhat, console
Published: 2020-09-09 18:34:52

---
title: How to run a .NET Core console app as a service using Systemd on Linux (RHEL)
published: true
date: 2020-01-31 00:00:00 UTC
tags: dotnet, linux, redhat, console
canonical_url: https://swimburger.net/blog/dotnet/how-to-run-a-dotnet-core-console-app-as-a-service-using-systemd-on-linux
cover_image: https://swimburger.net/media/pihn1sda/dotnet_redhat-social.png?anchor=center&mode=crop&width=1000&height=420&rnd=132249651584030000
---

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.

Although the steps will be very similar for ASP.NET Core services, this blogpost does not cover ASP.NET Core. An article covering ASP.NET Core will be published soon.

Prerequisites:

- Red Hat Enterprise Linux (or a compatible Unix based OS)
- .NET Core 3.1 installed ([Get started instructions from Red Hat](https://access.redhat.com/documentation/en-us/net_core/3.1/html-single/getting_started_guide/index))
- Sudo privileges

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

## .NET Core console application

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

```sh
mkdir ~/HelloWorld
cd ~/HelloWorld
dotnet new console
```

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

```sh
sudo mkdir /srv/HelloWorld               # Create directory /srv/HelloWorld
sudo chown yourusername /srv/HelloWorld  # Assign ownership to yourself of the directory
dotnet publish -c Release -o /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:

```sh
/srv/HelloWorld/HelloWorld
#output will be 'Hello World!'
```

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:

```
[Unit]
Description=Hello World console application

[Service]
# systemd will run this executable to start the service
ExecStart=/srv/HelloWorld/HelloWorld
# to query logs using journalctl, set a logical name here
SyslogIdentifier=HelloWorld

# Use your username to keep things simple.
# If you pick a different user, make sure dotnet and all permissions are set correctly to run the app
# To update permissions, use 'chown yourusername -R /srv/HelloWorld' to take ownership of the folder and files,
#       Use 'chmod +x /srv/HelloWorld/HelloWorld' to allow execution of the executable file
User=yourusername

# This environment variable is necessary when dotnet isn't loaded for the specified user.
# To figure out this value, run 'env | grep DOTNET_ROOT' when dotnet has been loaded into your shell.
Environment=DOTNET_ROOT=/opt/rh/rh-dotnet31/root/usr/lib64/dotnet  

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

**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](https://www.freedesktop.org/software/systemd/man/systemd.service.html) or the [RedHat documentation](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/system_administrators_guide/sect-managing_services_with_systemd-unit_files).

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.

```sh
sudo cp HelloWorld.service /etc/systemd/system/HelloWorld.service
sudo systemctl daemon-reload
sudo systemctl start HelloWorld
```

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

```sh
sudo systemctl status HelloWorld
# Output should be similar to below:
#    ● HelloWorld.service - Hello World console application
#    Loaded: loaded (/etc/systemd/system/HelloWorld.service; disabled; vendor preset: disabled)
#    Active: inactive (dead)
#
#    Jan 27 19:15:47 rhtest systemd[1]: Started Hello World console application.
#    Jan 27 19:15:47 rhtest HelloWorld[4074]: Hello World!
```

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

```sh
sudo journalctl -u HelloWorld
# Output should be similar to below:
#   Jan 27 19:15:47 rhtest systemd[1]: Started Hello World console application.
#   Jan 27 19:15:47 rhtest HelloWorld[4074]: Hello World!
```

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:

```csharp
using System;
using System.Threading.Tasks;

namespace HelloWorld
{
    class Program
    {
        static async Task Main(string[] args)
        {
            while(true){
                Console.WriteLine("Hello World!");
                await Task.Delay(500);
            }
        }
    }
}
```

Let's publish the app again:

```sh
sudo systemctl stop HelloWorld # stop the HelloWorld service to remove any file-locks
dotnet publish -c Release -o /srv/HelloWorld
sudo systemctl start HelloWorld
```

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

```
[Unit]
Description=Hello World console application

[Service]
# systemd will run this executable to start the service
ExecStart=/srv/HelloWorld/HelloWorld
# to query logs using journalctl, set a logical name here
SyslogIdentifier=HelloWorld

# Use your username to keep things simple.
# If you pick a different user, make sure dotnet and all permissions are set correctly to run the app
# To update permissions, use 'chown yourusername -R /srv/HelloWorld' to take ownership of the folder and files,
#       Use 'chmod +x /srv/HelloWorld/HelloWorld' to allow execution of the executable file
User=yourusername

# ensure the service restarts after crashing
Restart=always
# amount of time to wait before restarting the service                        
RestartSec=5                            

# This environment variable is necessary when dotnet isn't loaded for the specified user.
# To figure out this value, run 'env | grep DOTNET_ROOT' when dotnet has been loaded into your shell.
Environment=DOTNET_ROOT=/opt/rh/rh-dotnet31/root/usr/lib64/dotnet  

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

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

```
sudo cp HelloWorld.service /etc/systemd/system/HelloWorld.service
sudo systemctl daemon-reload
sudo systemctl restart HelloWorld
```

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:

```
sudo systemctl enable HelloWorld
```

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:

```
mkdir ~/Worker
cd ~/Worker
dotnet new worker
```

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

```
sudo mkdir /srv/Worker                  # Create directory /srv/Worker
sudo chown yourusername /srv/Worker     # Assign ownership to yourself of the directory
dotnet publish -c Release -o /srv/Worker
```

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

```
/srv/Worker/Worker
# Output should look like this:
#   info: Worker.Worker[0]
#         Worker running at: 01/28/2020 15:08:56 +00:00
#   info: Microsoft.Hosting.Lifetime[0]
#         Application started. Press Ctrl+C to shut down.
#   info: Microsoft.Hosting.Lifetime[0]
#         Hosting environment: Production
#   info: Microsoft.Hosting.Lifetime[0]
#         Content root path: /home/yourusername/Worker
#   info: Worker.Worker[0]
#         Worker running at: 01/28/2020 15:08:57 +00:00
#   info: Worker.Worker[0]
#         Worker running at: 01/28/2020 15:08:58 +00:00
```

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

```
[Unit]
Description=Long running service/daemon created from .NET worker template

[Service]
# will set the Current Working Directory (CWD). Worker service will have issues without this setting
WorkingDirectory=/srv/Worker
# systemd will run this executable to start the service
ExecStart=/srv/Worker/Worker
# to query logs using journalctl, set a logical name here
SyslogIdentifier=Worker

# Use your username to keep things simple.
# If you pick a different user, make sure dotnet and all permissions are set correctly to run the app
# To update permissions, use 'chown yourusername -R /srv/Worker' to take ownership of the folder and files,
#       Use 'chmod +x /srv/Worker/Worker' to allow execution of the executable file
User=yourusername

# ensure the service restarts after crashing
Restart=always
# amount of time to wait before restarting the service                  
RestartSec=5

# This environment variable is necessary when dotnet isn't loaded for the specified user.
# To figure out this value, run 'env | grep DOTNET_ROOT' when dotnet has been loaded into your shell.
Environment=DOTNET_ROOT=/opt/rh/rh-dotnet31/root/usr/lib64/dotnet  

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

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

```sh
sudo cp Worker.service /etc/systemd/system/Worker.service
sudo systemctl daemon-reload
sudo systemctl start Worker
```

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.

```sh
sudo journalctl -u Worker -f
```

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](https://www.nuget.org/packages/Microsoft.Extensions.Hosting.Systemd/)) package:

```sh
dotnet add package Microsoft.Extensions.Hosting.Systemd --version 3.1.1
```

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

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace HelloWorldWorker
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .UseSystemd()
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<Worker>();
                });
    }
}
```

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

```
[Unit]
Description=Long running service/daemon created from .NET worker template

[Service]
Type=notify
# will set the Current Working Directory (CWD). Worker service will have issues without this setting
WorkingDirectory=/srv/Worker
# systemd will run this executable to start the service
ExecStart=/srv/Worker/Worker
# to query logs using journalctl, set a logical name here  
SyslogIdentifier=Worker

# Use your username to keep things simple.
# If you pick a different user, make sure dotnet and all permissions are set correctly to run the app
# To update permissions, use 'chown yourusername -R /srv/Worker' to take ownership of the folder and files,
#       Use 'chmod +x /srv/Worker/Worker' to allow execution of the executable file
User=yourusername

# ensure the service restarts after crashing
Restart=always
# amount of time to wait before restarting the service              
RestartSec=5

# This environment variable is necessary when dotnet isn't loaded for the specified user.
# To figure out this value, run 'env | grep DOTNET_ROOT' when dotnet has been loaded into your shell.
Environment=DOTNET_ROOT=/opt/rh/rh-dotnet31/root/usr/lib64/dotnet  

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

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

```sh
sudo systemctl stop Worker # stop service to release any file locks which could conflict with dotnet publish
dotnet publish -c Release -o /srv/Worker
sudo cp Worker.service /etc/systemd/system/Worker.service
sudo systemctl daemon-reload
sudo systemctl start Worker   
```

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:

```sh
sudo journalctl -u Worker -f -p 4
```

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

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Worker
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;

        public Worker(ILogger<Worker> logger)
        {
            _logger = logger;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation("Information - Worker running at: {time}", DateTimeOffset.Now);
                _logger.LogWarning("Warning - Worker running at: {time}", DateTimeOffset.Now);
                _logger.LogError("Error - Worker running at: {time}", DateTimeOffset.Now);
                _logger.LogCritical("Critical - Worker running at: {time}", DateTimeOffset.Now);
                await Task.Delay(1000, stoppingToken);
            }
        }
    }
}
```

Republish and restart the service:

```sh
sudo systemctl stop Worker # stop service to release any file locks which could conflict with dotnet publish
dotnet publish -c Release -o /srv/Worker
sudo systemctl start Worker
```

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.

```sh
sudo journalctl -u Worker -f -p 4
# Output should look like this:
#   Jan 28 18:17:02 rhtest Worker[33537]: Worker.Worker[0] Warning - Worker running at: 01/28/2020 18:17:02 +00:00
#   Jan 28 18:17:02 rhtest Worker[33537]: Worker.Worker[0] Error - Worker running at: 01/28/2020 18:17:02 +00:00
#   Jan 28 18:17:02 rhtest Worker[33537]: Worker.Worker[0] Critical - Worker running at: 01/28/2020 18:17:02 +00:00
#   Jan 28 18:17:03 rhtest Worker[33537]: Worker.Worker[0] Warning - Worker running at: 01/28/2020 18:17:03 +00:00
#   Jan 28 18:17:03 rhtest Worker[33537]: Worker.Worker[0] Error - Worker running at: 01/28/2020 18:17:03 +00:00
#   Jan 28 18:17:03 rhtest Worker[33537]: Worker.Worker[0] Critical - Worker running at: 01/28/2020 18:17:03 +00:00
```

The 'UseSystemd' function will not do anything when run outside of a systemd service.  
The [implementation checks](https://github.com/dotnet/extensions/blob/master/src/Hosting/Systemd/src/SystemdHelpers.cs#L29-L44) 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.