Run a .NET App as a Windows Service on a VPS
Publish a .NET worker as a Windows service using sc.exe or New-Service, configure recovery, and keep logs on High-Speed SSD disk after reboot.
Netbay Developer Relations
Netbay Engineering
On this page
IIS is the right host for HTTP. A background worker, a queue consumer, or a .NET app that should start before anyone hits a URL belongs in a Windows service. If you start it from an RDP session and then disconnect, the process dies with the session. If you put it in Task Scheduler as At log on, it waits for a desktop that may never exist. sc.exe and the generic host UseWindowsService() are the supported path on Server 2022.
The app should be a Worker SDK project (or a trimmed console that uses Microsoft.Extensions.Hosting). Publish it as a folder, not a framework-dependent guess that requires an SDK on the VPS. Register the service, set recovery to restart, log somewhere that survives a reboot, and confirm it comes back after Windows Update.
Build a worker that can run without a console
On your development machine, not on the VPS, create a worker and add the Windows service package. UseWindowsService() hooks ServiceBase so SCM can start and stop the process. Logs should go to Event Log or a file under C:\apps\logs. Console output is invisible once the service is running.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _log;
public Worker(ILogger<Worker> log) { _log = log; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_log.LogInformation("worker alive at {time}", DateTimeOffset.Now);
await Task.Delay(5000, stoppingToken);
}
}
}
public class Program
{
public static void Main(string[] args)
{
Host.CreateDefaultBuilder(args)
.UseWindowsService(options => { options.ServiceName = "NetbayWorker"; })
.ConfigureServices((ctx, services) =>
{
services.AddHostedService<Worker>();
})
.Build()
.Run();
}
}The log template uses {time} as a structured placeholder. That is not a shell variable and it is not a JavaScript template. Publish with dotnet publish -c Release -o C:\build\worker (or a Linux/macOS equivalent path) and copy the folder to the VPS, for example C:\apps\worker\Worker.exe plus its DLLs. Install the .NET runtime on Server 2022 if you published framework-dependent. Self-contained publish is larger and avoids a second runtime install.
Do not wrap an IIS site as a service. Do not use a third-party service wrapper until the generic host path has failed. sc.exe is enough.
Register, recover, and prove reboot
On the VPS, use sc.exe. The binPath= and start= tokens need a space after the equals sign. That is a twenty-year-old quirk, not a typo. Set failure actions so a crash at 02:00 is a restart, not a silent Stopped service you discover at Monday standup.
New-Item -ItemType Directory -Path 'C:\apps\worker' -Force
New-Item -ItemType Directory -Path 'C:\apps\logs' -Force
sc.exe create NetbayWorker binPath= "C:\apps\worker\Worker.exe" start= auto DisplayName= "Netbay worker"
sc.exe description NetbayWorker "Queue worker hosted as a Windows service"
sc.exe failure NetbayWorker reset= 86400 actions= restart/60000/restart/60000/restart/60000
sc.exe start NetbayWorker
Get-Service NetbayWorker | Format-Table Name, Status, StartType
Get-WinEvent -FilterHashtable @{ LogName = 'Application'; StartTime = (Get-Date).AddMinutes(-10) } |
Where-Object { $_.ProviderName -match 'Worker|Netbay' } | Select-Object TimeCreated, Id, Message -First 10New-Service -Name NetbayWorker -BinaryPathName 'C:\apps\worker\Worker.exe' -StartupType Automatic is the PowerShell equivalent of sc create. Recovery still wants sc.exe failure. Delayed-auto (start= delayed-auto) is useful if the worker talks to SQL that starts on the same guest; a 30-second delay beats a crash loop at boot.
Run the worker as a dedicated local user if it does not need to be LocalSystem. Grant that user Modify on C:\apps\worker and C:\apps\logs, nothing on C:\Windows. LocalSystem will work and will also be able to read every secret on the box. Least privilege still applies.
After sc start, disconnect RDP and confirm Get-Service from a new session. Then reboot in a window and confirm again. If Status is Stopped, the executable path is wrong, the runtime is missing, or the worker threw in Main before the host started. The Application log will say which.
Day-2 operations
Treat the service like IIS: it is part of the verify list after Windows Update. Restart-Service NetbayWorker is the tame recycle. sc.exe stop and sc.exe start are the blunt ones. When you deploy a new publish folder, stop the service first or the EXE is locked. Copy files, start, watch logs.
Do not store connection strings in the service environment with a GUI dialog you will forget. Use a file under C:\apps\worker\appsettings.Production.json with NTFS locked to the service account. Do not open a custom TCP port in the firewall unless the worker is actually a server. Most workers only make outbound calls.
If you later put HTTP in the same process, prefer IIS and a separate worker. Mixing a Kestrel listener into a Windows service is valid and is also two failure modes in one PID. Keep them apart on a small VPS.
Takeaway
Publish a Worker SDK app with UseWindowsService(), register it with sc.exe, set restart recovery, and verify it after reboot and after patching. Do not leave a background process tied to your RDP session. Deploy the worker on a Windows VPS from Netbay in Lucknow, on Intel Xeon Platinum with High-Speed SSD, and wire it up from netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance