Skip to main content
Redhat Developers  Logo
  • 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 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

    • OpenShift AI learning
    • 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.
    • Explore the solution
  • 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

    • Access product downloads to start building and testing right away.
    • 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.
    • Explore the benefits

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

    • Debugging image mode with Red Hat OpenShift 4.20: A practical guide

    • EvalHub: Because "looks good to me" isn't a benchmark

    • SQL Server HA on RHEL: Meet Pacemaker HA Agent v2 (tech preview)

    • Deploy with confidence: Continuous integration and continuous delivery for agentic AI

    • Every layer counts: Defense in depth for AI agents with Red Hat AI

    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 LinkedIn YouTube Twitter Facebook

    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

    Chat Support

    Please log in with your Red Hat account to access chat support.