🚀 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.exeandNew-Servicefor 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
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
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
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
After running this, you can manage the service with standard cmdlets:
Start-Service -Name $serviceName
Get-Service -Name $serviceName
Stop-Service -Name $serviceName
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
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
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();
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);
}
}
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
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
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-Servicewhen 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.
👉 Read the original article on TechResolve.blog
☕ Support my work
If this article helped you, you can buy me a coffee:

Top comments (0)