DEV Community

Cover image for Solved: Run PowerShell Scripts as Windows Services — Updated Version (.NET 10)
Darian Vance
Darian Vance

Posted on • Originally published at wp.me

Solved: Run PowerShell Scripts as Windows Services — Updated Version (.NET 10)

🚀 Executive Summary

TL;DR: PowerShell scripts inherently lack persistence as Windows Services, leading to issues like stopping on user logout or server reboot. This article details three methods—NSSM, New-Service, and .NET Worker Service—to reliably run PowerShell scripts as persistent background processes. These solutions range from simple wrappers to a robust, enterprise-grade .NET hosting model, bridging the gap between script execution and managed service operation.

🎯 Key Takeaways

  • Traditional methods like sc.exe and New-Service for running PowerShell scripts as services offer limited fault tolerance, requiring reliance on Windows SCM Recovery settings for basic restarts.
  • NSSM (Non-Sucking Service Manager) is a lightweight, open-source service wrapper that provides excellent built-in process monitoring, automatic restarts, and configurable error handling for PowerShell scripts.
  • The .NET Worker Service approach is the most robust and modern solution, allowing PowerShell scripts to be hosted within a managed .NET application, providing native control over structured logging, configuration, dependency injection, and a full service lifecycle for enterprise-grade automation.

Learn how to reliably run PowerShell scripts as persistent Windows Services using three distinct methods, from the classic NSSM utility to the modern, robust .NET Worker Service approach, complete with code examples and a detailed comparison table for IT professionals.

Symptoms: The Challenge of Persistent PowerShell Scripts

As a DevOps engineer, you’ve likely written a PowerShell script to perform a critical, long-running task: monitoring a log file, performing periodic cleanup, or syncing data between systems. The script works perfectly when you run it from a console. The problem arises when you need it to run continuously, automatically start on boot, and be managed like a true background process. A scheduled task running every minute isn’t a true service; it’s a workaround with significant gaps in execution.

The core symptoms of this problem are:

  • The script stops running if the user who launched it logs out.
  • The script does not automatically restart if the server reboots.
  • There’s no native way to monitor the script’s health (e.g., via services.msc).
  • Error handling and automatic restarts upon failure are non-existent without complex, custom logic inside the script itself.

PowerShell scripts are designed to execute and terminate. Windows Services are designed for persistence. Our goal is to bridge this gap. Let’s explore three solutions, from the battle-tested classics to the modern, enterprise-grade approach.

Solution 1: The Old Guard – sc.exe and NSSM

For years, the standard solution involved using a “service wrapper” utility. These tools act as a thin executable that the Windows Service Control Manager (SCM) can manage, and their sole job is to launch and monitor your script.

Using sc.exe with a PowerShell Wrapper

The most basic method uses the built-in Windows Service Control utility, sc.exe. You can’t point it directly at a .ps1 file, but you can tell it to run powershell.exe and pass your script as an argument. This is a bare-bones approach with limited fault tolerance.

Here’s how you would create a service that runs a script:

sc.exe create "MyPSScriptService" binPath= "powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"C:\Scripts\MyLongRunningScript.ps1\"" DisplayName= "My PowerShell Script Service" start= auto
Enter fullscreen mode Exit fullscreen mode

The major drawback is that if your script terminates for any reason (an unhandled exception, for instance), the service will enter a “Stopped” state. The SCM can be configured to restart it, but it’s not as robust as dedicated tools.

Using NSSM (the Non-Sucking Service Manager)

NSSM has long been the gold standard for this task. It’s a lightweight, open-source service wrapper that provides the resilience missing from the sc.exe method. It handles logging, process monitoring, and automatic restarts with grace.

First, download NSSM and place it in a location in your system’s PATH. Then, installing your script as a service is a simple command:

nssm.exe install MyNSSMService
Enter fullscreen mode Exit fullscreen mode

This command opens a GUI where you can configure the path to the executable (powershell.exe or pwsh.exe), the arguments (-File C:\Scripts\MyScript.ps1), and crucial restart behavior on the “Exit actions” tab.

You can also do this entirely from the command line:

nssm.exe install MyNSSMService pwsh.exe "-NoProfile -ExecutionPolicy Bypass -File C:\Scripts\MyLongRunningScript.ps1"
nssm.exe set MyNSSMService DisplayName "My NSSM PowerShell Service"
nssm.exe set MyNSSMService Start SERVICE_AUTO_START
nssm.exe start MyNSSMService
Enter fullscreen mode Exit fullscreen mode

NSSM is a fantastic, reliable tool that has solved this problem for countless administrators.

Solution 2: The Modern PowerShell Approach – New-Service

With PowerShell 7+, Microsoft has improved the native tooling. While the New-Service cmdlet still requires an executable file, just like sc.exe, it provides a fully scriptable, PowerShell-native way to create and manage services without third-party dependencies.

The principle is the same: we create a service that launches PowerShell, which in turn runs our script.

# Ensure you are running as Administrator

$serviceName = "MyPwshNativeSvc"
$serviceDisplayName = "My PowerShell Native Service"
$scriptPath = "C:\Scripts\MyLongRunningScript.ps1"

# The command that the service will execute. Note the escaped quotes.
$binaryPath = "C:\Program Files\PowerShell\7\pwsh.exe -NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`""

New-Service -Name $serviceName -BinaryPathName $binaryPath -DisplayName $serviceDisplayName -StartupType Automatic
Enter fullscreen mode Exit fullscreen mode

After running this, you can manage the service with standard cmdlets:

Start-Service -Name $serviceName
Get-Service -Name $serviceName
Stop-Service -Name $serviceName
Enter fullscreen mode Exit fullscreen mode

This approach is excellent for environments where third-party tools are prohibited and you want to keep your entire toolchain within the PowerShell ecosystem. However, like the sc.exe method, robust error handling and restart logic must be configured on the service’s recovery tab or built into the script itself.

Solution 3: The .NET Worker Service – The Enterprise Way

This is the most modern and robust solution, aligning with current DevOps practices for building reliable, long-running applications. Instead of wrapping a script, we build a lightweight .NET application (a “Worker Service”) that hosts and executes our PowerShell code. This gives us access to native dependency injection, structured logging, configuration management, and a proper, managed service lifecycle.

This approach treats our PowerShell logic as a component within a true application, not just a standalone file.

Step 1: Create the .NET Worker Service Project

You need the .NET SDK installed (e.g., .NET 8, with an eye toward .NET 10). Create a new worker project:

dotnet new worker -o PsAsServiceWorker
cd PsAsServiceWorker
Enter fullscreen mode Exit fullscreen mode

Step 2: Add Necessary NuGet Packages

We need packages to host as a Windows Service and to run PowerShell:

dotnet add package Microsoft.Extensions.Hosting.WindowsServices
dotnet add package Microsoft.PowerShell.SDK
Enter fullscreen mode Exit fullscreen mode

Step 3: Modify Program.cs to enable Windows Service Hosting

Open Program.cs and add the .UseWindowsService() extension method.

using PsAsServiceWorker;

IHost host = Host.CreateDefaultBuilder(args)
    .UseWindowsService(options =>
    {
        options.ServiceName = "My .NET PowerShell Worker";
    })
    .ConfigureServices(services =>
    {
        services.AddHostedService<Worker>();
    })
    .Build();

host.Run();
Enter fullscreen mode Exit fullscreen mode

Step 4: Implement the Worker.cs to run the script

This is where the magic happens. We’ll use the System.Management.Automation namespace to invoke our script. This gives us immense control over the execution pipeline.

using System.Management.Automation;

namespace PsAsServiceWorker;

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly string _scriptPath = @"C:\Scripts\MyLongRunningScript.ps1";

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

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Worker starting at: {time}", DateTimeOffset.Now);

        try
        {
            using (PowerShell ps = PowerShell.Create())
            {
                ps.AddScript(File.ReadAllText(_scriptPath));

                // Add parameters if needed
                // ps.AddParameter("param1", "value1");

                _logger.LogInformation("Invoking PowerShell script: {path}", _scriptPath);

                // This is a blocking call. Run it in a separate task to respect cancellation.
                var psTask = Task.Run(() => ps.Invoke(), stoppingToken);

                await psTask;

                if (ps.HadErrors)
                {
                    foreach (var error in ps.Streams.Error)
                    {
                        _logger.LogError("PowerShell Error: {error}", error.ToString());
                    }
                }
                else
                {
                    _logger.LogInformation("Script completed successfully.");
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception occurred while running the PowerShell script.");
        }

        _logger.LogInformation("Worker stopping at: {time}", DateTimeOffset.Now);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Publish and Register the Service

First, publish the application as a self-contained executable.

dotnet publish -c Release -r win-x64 --self-contained
Enter fullscreen mode Exit fullscreen mode

Then, use sc.exe to register the generated .exe file from the publish directory (e.g., bin\Release\net8.0\win-x64\publish\PsAsServiceWorker.exe).

sc.exe create "MyDotNetPsWorker" binPath="C:\Path\To\publish\PsAsServiceWorker.exe" DisplayName="My .NET PowerShell Worker" start=auto
Enter fullscreen mode Exit fullscreen mode

This approach is significantly more involved but provides unparalleled control, logging, and integration into a modern CI/CD pipeline.

Solution Comparison: NSSM vs. New-Service vs. .NET Worker

Feature NSSM New-Service (Wrapper) .NET Worker Service
Ease of Setup Very Easy Easy Complex
Dependencies Third-party executable (NSSM) None (built into PowerShell) .NET SDK for development
Error Handling/Restart Excellent, built-in and configurable Relies on Windows SCM Recovery settings Application-level control (try/catch), plus SCM Recovery
Logging Good (can redirect stdout/stderr to files) Poor (requires script to handle all logging) Excellent (structured logging, sinks for Seq, AppInsights, etc.)
Configuration Basic (environment variables) Handled by the script Advanced (appsettings.json, Key Vault, etc.)
DevOps Integration Moderate (can be scripted) Good (fully scriptable) Excellent (part of a standard build/release pipeline)

Conclusion: Choosing the Right Tool for the Job

The “best” solution depends entirely on your context, requirements, and environment.

  • Use NSSM when you need a quick, reliable, and “fire-and-forget” way to run a simple script as a service. It’s a fantastic tool for legacy systems or when development overhead needs to be zero.
  • Use New-Service when you are in a pure PowerShell environment, cannot use third-party tools, and need a fully automated, script-based deployment. It’s the native, dependency-free choice.
  • Use a .NET Worker Service for critical, enterprise-level tasks. When your PowerShell script is a core part of an application and requires robust logging, configuration, monitoring, and integration into a modern CI/CD pipeline, this is the definitive, forward-looking solution. It represents a paradigm shift from “running a script” to “hosting a managed automation process.”

As we move towards environments managed by code and robust delivery pipelines, elevating important PowerShell logic into a managed .NET host is the most sustainable and professional path forward.


Darian Vance

👉 Read the original article on TechResolve.blog


Support my work

If this article helped you, you can buy me a coffee:

👉 https://buymeacoffee.com/darianvance

Top comments (0)