Skip to main content
AI
  • Get started with AI

    • Red Hat AI

      Accelerate the development and deployment of enterprise AI solutions.

    • AI learning hub

      Explore learning materials and tools, organized by task.

    • AI interactive demos

      Click through scenarios with Red Hat AI, including training LLMs and more.

    • AI/ML learning paths

      Expand your Red Hat OpenShift AI knowledge using these learning resources.

    • AI quickstarts

      Focused AI use cases designed for fast deployment on Red Hat AI platforms.

    • No-cost AI training

      Foundational Red Hat AI training.

  • Featured resources

    • Red Hat OpenShift AI
    • Open source AI for developers
    • AI product application development
    • Open source-powered AI/ML for hybrid cloud
    • AI and Node.js cheat sheet

Red Hat AI Factory with NVIDIA

Red Hat AI Factory with NVIDIA is a co-engineered, enterprise-grade AI solution for building, deploying, and managing AI at scale across hybrid cloud environments.

Learn
  • Self-guided

    • Documentation

      Find answers, get step-by-step guidance, and learn how to use Red Hat products.

    • Learning paths

      Explore curated walkthroughs for common development tasks.

    • Guided learning

      Receive custom learning paths powered by our AI assistant.

    • See all learning
  • Hands-on

    • Developer Sandbox

      Spin up Red Hat's products and technologies without setup or configuration.

    • Interactive labs

      Learn by doing in these hands-on, browser-based experiences.

    • Interactive demos

      Click through product features in these guided tours.

  • Browse by topic

    • AI/ML
    • Automation
    • Java
    • Kubernetes
    • Linux
    • See all topics
  • Training & certifications

    • Courses and exams
    • Certifications
    • Skills assessments
    • Red Hat Academy
    • Learning subscription
    • Explore training
Build
  • Get started

    • Red Hat build of Podman Desktop

      A downloadable, local development hub to experiment with our products and builds.

    • Developer Sandbox

      Spin up Red Hat's products and technologies without setup or configuration.

  • Download products

    • Red Hat Enterprise Linux
    • Red Hat AI
    • Red Hat OpenShift
    • Red Hat Ansible Automation Platform
    • See all products
  • Featured

    • Red Hat build of OpenJDK
    • Red Hat JBoss Enterprise Application Platform
    • Red Hat OpenShift Dev Spaces
    • Red Hat Developer Toolset
  • References

    • E-books
    • Documentation
    • Cheat sheets
    • Architecture center
Community
  • Get involved

    • Events
    • Live AI events
    • Red Hat Summit
    • Red Hat Accelerators
    • Community discussions
  • Follow along

    • Articles & blogs
    • Developer newsletter
    • Videos
    • Github
  • Get help

    • Customer service
    • Customer support
    • Regional contacts
    • Find a partner

Join the Red Hat Developer program

Download Red Hat products and project builds, access support documentation, learning content, and more.

ConsoleDocsSupport Search
New For you

Recommended

We'll recommend resources you may like as you browse. Try these suggestions for now.

  • Product downloads
  • Cheat sheets
  • Red Hat Developer learning portal
  • Developer blog
  • E-books
  • Developer Sandbox
Log in

Get more with a Red Hat Developer account

  • Product downloads
  • Console access
  • Event registration
  • Training & trials
Explore more account benefitsLog in or register

A subscription may be required for some services.

Contact us

How to deploy .NET apps as systemd services using containers

January 17, 2023
Tom Deseyn
Related topics:
.NETContainersLinuxKubernetes
Related products:
Red Hat Enterprise Linux

    In this article, we will deploy a .NET app as a systemd service. Instead of installing the application directly on the system, we will take a different approach by using a container instead.

    Why use a container?

    When we deploy an application as a systemd service, we need to solve problems that go beyond starting the application. Containers solve problems such as: how the application gets distributed, how upgrades are managed, and how native dependencies are provided.

    Container images are distributed through registries. Images are versioned using tags, allowing for easy upgrades and including all the dependencies. When deploying to Kubernetes, packing up your .NET application as a container for systemd gives it the benefits of being self-contained and universally deployable.

    Creating an application

    As an example for this article, we will use an application based on the Worker template. The Worker template is a console application based on Microsoft.Extension.Hosting and provides the configuration and dependency injection services you know from ASP.NET Core. Worker is meant for long-running applications that aren't web applications. You can learn more about the Worker template in Worker Services. What we do in this article also applies to ASP.NET Core.

    Create the application as follows:

    $ dotnet new worker -o worker
    $ cd worker

    If you open up the application folder, you'll find the Worker.cs file, which implements a simple long-running job that prints a message to the console once a second.

    Execute dotnet run to see the application in action. When you've seen a couple of messages, press Ctrl-C to terminate the application.

    The .NET Core 3.0 added support for systemd services. Specifically, the application host (from Microsoft.Extension.Hosting) can notify systemd upon startup. For a web application, notifying systems indicates that the application is ready to serve requests. In our example, the notification means that all the hosted services, such as the Worker class, have started for our worker.

    If you want a BackgroundService such as Worker.cs to do asynchronous initialization that affects readiness, you can allow time for that activity by overriding the base StartAsync method, as shown in the following code:

    public async override Task StartAsync(CancellationToken cancellationToken)
    {
      // Perform startup.
      System.Console.WriteLine("Starting");
      await Task.Delay(500, cancellationToken);
      System.Console.WriteLine("Started");
    
      // Call base method.
      await base.StartAsync(cancellationToken);
    }

    In .NET 7, systemd support was extended to include .NET running in a container managed by systemd. If you are using an earlier version of .NET, systemd can run your container, but it won't know when your application is fully ready.

    To enable systemd integration, add the Microsoft.Extensions.Hosting.Systemd package:

    $ dotnet add package Microsoft.Extensions.Hosting.Systemd

    Next, call UseSystemd on your application's IHostBuilder:

    IHost host = Host.CreateDefaultBuilder(args)
      .ConfigureServices(services =>
      {
          services.AddHostedService<Worker>();
      })
    + .UseSystemd()
      .Build();

    You can add this code to any .NET application to make it runnable with systemd. If the application doesn't execute from systemd, the UseSystemd call is a no-op.

    Creating a container image

    Now we're going to create a container image. There are a number of ways you can do this. You can hand-craft a Containerfile (Dockerfile) based on Microsoft's Containerize a .NET app tutorial. You can use the new SDK feature that builds an image for you: as described in Announcing built-in container support for the .NET SDK. And a third option is to use the dotnet build-image global tool that I introduced in the article Containerize .NET applications without writing Dockerfiles.

    Feel free to pick the option you prefer. I'm going to use the third option.

    Install the global tool:

    $ dotnet tool install -g dotnet-build-image

    And now, create the image:

    $ dotnet build-image -t awesome-worker-app:latest -b ubi

    We're passing two arguments. awesome-worker-app:latest is the name of the image. ubi indicates that creation should be based on Red Hat's UBI images. You can pick another base, such as alpine or jammy-chiseled.

    For this tutorial, we'll deploy the systemd unit on the local system. To deploy it on several systems, you can set the tag to match the image registry (for example: -t quay.io/tmds/awesome-worker-app:latest), and add the --push argument to push the image to the registry after it was built.

    The following command runs the image locally:

    $ podman run –rm awesome-worker-app:latest

    As this executes, you'll see the messages from the containerized .NET application, similar to when executing dotnet run. Terminate the container by pressing Ctrl+C.

    Creating a systemd unit

    We'll use Podman to generate a systemd unit file for us.

    Start the container by running the following command:

    $ podman run --name awesome-worker-app --rm --label "io.containers.autoupdate=local" localhost/awesome-worker-app:latest

    This command is similar to the one we used earlier. We're using the fully qualified name of the image (by adding the localhost/ prefix), and we've added a label (io.containers.autoupdate), which we'll discuss in a moment. If your image is deployed to a remote registry, set the label value to registry instead of local.

    If your application uses .NET 7, which supports systemd containers, you can add --sdnotify=container to use that support.

    In another terminal, run:

    $ podman generate systemd --new -n -f --start-timeout 600 awesome-worker-app

    This will write out a file named container-awesome-worker-app.service with the following content:

    # container-awesome-worker-app.service
    # autogenerated by Podman 4.2.0
    
    [Unit]
    Description=Podman container-awesome-worker-app.service
    Documentation=man:podman-generate-systemd(1)
    Wants=network-online.target
    After=network-online.target
    RequiresMountsFor=%t/containers
    
    [Service]
    Environment=PODMAN_SYSTEMD_UNIT=%n
    Restart=on-failure
    TimeoutStartSec=600
    TimeoutStopSec=70
    ExecStartPre=/bin/rm -f %t/%n.ctr-id
    ExecStart=/usr/bin/podman run \
        --cidfile=%t/%n.ctr-id \
        --cgroups=no-conmon \
        --rm \
        --sdnotify=conmon \
        -d \
        --replace \
        --name awesome-worker-app \
        --label io.containers.autoupdate=local localhost/awesome-worker-app:latest
    ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
    ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
    Type=notify
    NotifyAccess=all
    
    [Install]
    WantedBy=default.target

    This systemd file isn't specific to your system. It can be used to deploy the image anywhere.

    We've provided a large --start-timeout value of 10 minutes to allow the system time to pull the image (in case the image is hosted on a remote registry).

    The label we added through the --label argument shows up in the ExecStart command. This label is detected by a podman auto-update command, which monitors all containers with this label. If a new image is available, the command pulls it and restarts the systemd unit.

    Podman ships with a timer that triggers the update daily at midnight. If the new image fails to deploy, Podman even rolls back the container to the previous version. Failure detection works best if you use the --sdnotify=container option (with .NET 7).

    Now, if you host the image in an image registry solely using this unit file, you can deploy the .NET application to a large number of systems and even have it update automatically.

    Deploying the service

    You can deploy this unit with the system units at /usr/systemd/system. We'll deploy it as part of our user's systemd services as follows:

    $ mkdir -p ~/.config/systemd/user
    $ cp container-awesome-worker-app.service ~/.config/systemd/user/awesome-worker-app.service
    $ systemctl --user daemon-reload
    $ systemctl --user start awesome-worker-app

    That's it—your service is up and running.

    You can get the logs of the service by running the following:

    $ journalctl --user -f -u awesome-worker-app

    If you'd like this service to start at boot, you have to enable lingering for your user and enable the service as follows:

    $ sudo loginctl enable-linger $(id -u)
    $ systemctl -- user start awesome-worker-app

    Triggering an update

    To finish the example, let's see auto-update in action.

    First, change the message that is printed in Worker.cs:

      {
        while (!stoppingToken.IsCancellationRequested)
        {
    -     _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
    +     _logger.LogInformation("Updated worker running at: {time}", DateTimeOffset.Now);
          await Task.Delay(1000, stoppingToken);
        }
      }

    Then rebuild the image:

    $ dotnet build-image -t awesome-worker-app:latest -b ubi

    The new image is now available. A system that has the auto-update timer enabled will start using it at midnight. But you don't have to wait for that: to update the image the right way, just invoke podman auto-update:

    $ podman auto-update
    UNIT                        CONTAINER                          IMAGE                      POLICY  UPDATED
    awesome-worker-app.service  342942f00932 (awesome-worker-app)  awesome-worker-app:latest  local   true

    The command updates the service, and you can see the change in the journal:

    $ journalctl --user -f -u awesome-worker-app

    Containers and new .NET features simplify systemd

    This article has covered how to deploy .NET applications as systemd services using containers. Containers offer a convenient way to distribute, deploy, and update your application. You've also learned that applications using .NET 7 support notifying systemd about their readiness when running in containers.

    Last updated: June 11, 2025

    Related Posts

    • Running systemd in a non-privileged container

    • Managing temporary files with systemd-tmpfiles on RHEL 7

    • Monitoring .NET Core applications on Kubernetes

    • Containerize .NET for Red Hat OpenShift: Linux containers and .NET Core

    Recent Posts

    • A day in the life of an NVIDIA H100 on Red Hat OpenShift 5

    • Migrate BuildConfigs to builds for Red Hat OpenShift with one plug-in install

    • Run Red Hat Enterprise Linux CoreOS on NVIDIA BlueField DPUs

    • Agent harness engineering: Separating model capabilities from enforcement

    • What's new for developers in Red Hat Enterprise Linux 10.3 and 9.9

    What’s up next?

    This cheat sheet covers the basics of installing .NET on Red Hat Enterprise Linux (RHEL), how to get a simple program running, and how to run a program in a Linux container.

    Get the cheat sheet
    Red Hat Developers logo

    Platforms

    • Red Hat AI
    • Red Hat Enterprise Linux
    • Red Hat OpenShift
    • Red Hat Ansible Automation Platform
    • See all products

    Build

    • Developer Sandbox
    • Developer tools
    • Interactive tutorials
    • API catalog

    Quicklinks

    • Learning resources
    • E-books
    • Cheat sheets
    • Blog
    • Events
    • Newsletter

    Communicate

    • About us
    • Contact sales
    • Find a partner
    • Report a website issue
    • Site status dashboard
    • Report a security problem

    Red Hat Developer

    Build here. Go anywhere.

    We serve the builders. The problem solvers who create careers with code.

    Join us if you’re a developer, software engineer, web designer, front-end designer, UX designer, computer scientist, architect, tester, product manager, project manager or team lead.

    Sign me up

    Red Hat legal and privacy links

    • About Red Hat
    • Jobs
    • Events
    • Locations
    • Contact Red Hat
    • Red Hat Blog
    • Inclusion at Red Hat
    • Cool Stuff Store
    • Red Hat Summit
    © 2026 Red Hat

    Red Hat legal and privacy links

    • Privacy statement
    • Terms of use
    • All policies and guidelines
    • Digital accessibility
    Ask AI