Skip to main content

Run a Program as a Windows Service at Startup with WinSW

Published: August 7, 2023 Updated: May 24, 2026 Larry Qu 10 min read

Introduction

Windows Services start automatically at boot, before any user logs in, and run in the background. If you have a program (a Go binary, Python script, Node.js app, etc.) that you want to run as a service, WinSW is the easiest way to wrap it.

WinSW is a lightweight executable that wraps any program as a Windows service — no coding required, just a simple XML config file.

WinSW History and Current Status

WinSW (Windows Service Wrapper) was created by Kohsuke Kawaguchi, the founder of Jenkins, to solve the common problem of running Java and other processes as Windows services. It was originally part of the Jenkins ecosystem but quickly gained adoption as a standalone tool.

Development Status

  • Current stable release: v2.11.0 (March 2021) — mature and production-tested with thousands of deployments
  • v3.0.0-alpha: In development with significant improvements:
    • YAML configuration support as an alternative to XML
    • Prestart, poststart, prestop, and poststop hook scripts
    • stopexecutable for custom stop commands
    • refresh command to reload configuration without restarting the service
    • dev ps, dev kill, dev list commands for debugging
  • Maintainership: The project moved from Kohsuke’s personal account to the winsw GitHub organization with active community maintainers
  • .NET evolution: Originally built on .NET Framework, v2.x ships both .NET Framework and .NET Core native executables. The .NET Core versions are fully self-contained with no runtime dependency.

WinSW is widely used in production environments — CI/CD systems, monitoring agents, and custom daemon processes on Windows.

Prerequisites

  • Windows 10/11 or Windows Server
  • Administrator access
  • Your program’s executable (e.g., dosomething.exe)

Step 1: Download WinSW

Download the latest WinSW.exe from the WinSW releases page.

Choose the right version:

  • WinSW-x64.exe — for 64-bit Windows (most common)
  • WinSW-x86.exe — for 32-bit Windows
  • WinSW-arm64.exe — for ARM64

Rename it to WinSW.exe and place it in your working directory alongside your program.

Example directory structure:

D:\dosomething\
├── dosomething.exe      ← your program
├── dosomething.ini      ← your program's config
├── WinSW.exe            ← WinSW executable
└── winsw.xml            ← WinSW config (you'll create this)

Step 2: Create the WinSW Config File

Create winsw.xml in the same directory:

<service>
    <!-- Unique service ID (no spaces) -->
    <id>dosomething</id>

    <!-- Display name in Services panel -->
    <name>dosomething</name>

    <!-- Description shown in Services panel -->
    <description>dosomething client service</description>

    <!-- Path to your executable (relative to winsw.xml location) -->
    <executable>dosomething</executable>

    <!-- Command-line arguments for your program -->
    <arguments>-c dosomething.ini</arguments>

    <!-- Log mode: reset (new log each start), roll, append -->
    <logmode>reset</logmode>
</service>

Extended Configuration Options

<service>
    <id>myapp</id>
    <name>My Application</name>
    <description>My application service</description>
    <executable>myapp.exe</executable>
    <arguments>--config config.yaml --port 8080</arguments>

    <!-- Working directory -->
    <workingdirectory>D:\myapp</workingdirectory>

    <!-- Environment variables -->
    <env name="APP_ENV" value="production"/>
    <env name="LOG_LEVEL" value="info"/>

    <!-- Restart on failure -->
    <onfailure action="restart" delay="10 sec"/>
    <onfailure action="restart" delay="20 sec"/>
    <onfailure action="none"/>

    <!-- Log configuration -->
    <log mode="roll-by-size">
        <sizeThreshold>10240</sizeThreshold>
        <keepFiles>8</keepFiles>
    </log>

    <!-- Start mode: Automatic, Manual, Boot, System -->
    <startmode>Automatic</startmode>

    <!-- Run as a specific user (optional) -->
    <!-- <serviceaccount>
        <username>.\LocalUser</username>
        <password>password</password>
    </serviceaccount> -->
</service>

YAML Configuration Support

Starting with v3.0.0 (currently in alpha), WinSW supports YAML configuration as an alternative to XML. YAML is less verbose, more readable, and integrates naturally with CI/CD pipelines.

Create winsw.yml instead of winsw.xml:

id: myapp
name: My Application
description: "My application running as a Windows service"
executable: myapp.exe
arguments: --port 8080
log:
  mode: roll-by-size
  sizeThreshold: 10240
  keepFiles: 8
env:
  - name: APP_ENV
    value: production
  - name: LOG_LEVEL
    value: info

Install and manage with the same commands — WinSW auto-detects the config format:

WinSW-x64.exe install
WinSW-x64.exe start

YAML support makes WinSW more DevOps-friendly. You can template the configuration with tools like Helm, Ansible, or handwritten YAML generation in CI scripts without dealing with XML parsing.

For v2.x users, stick with XML — the YAML feature is only available in the v3 alpha.

Step 3: Register the Service

Open Command Prompt as Administrator and navigate to your working directory:

d:
cd dosomething

Install the service:

WinSW.exe install winsw.xml

You should see: 2023-08-07 10:00:00,000 INFO - Installing the service with id 'dosomething'

Step 4: Start the Service

The service is installed but not yet running. Start it:

WinSW.exe start winsw.xml

Or start it from the Windows Services panel:

  1. Press Win + R, type services.msc, press Enter
  2. Find your service by name
  3. Right-click → Start

Managing the Service

# Check service status
WinSW.exe status winsw.xml

# Stop the service
WinSW.exe stop winsw.xml

# Restart the service
WinSW.exe restart winsw.xml

# Uninstall the service
WinSW.exe uninstall winsw.xml

Or use the built-in sc command:

# Start
sc start dosomething

# Stop
sc stop dosomething

# Query status
sc query dosomething

# Delete service
sc delete dosomething

Or PowerShell:

# Start
Start-Service dosomething

# Stop
Stop-Service dosomething

# Get status
Get-Service dosomething

# Set to automatic start
Set-Service dosomething -StartupType Automatic

### Advanced PowerShell Management

For scripted environments, PowerShell provides comprehensive service management beyond basic start/stop:

```powershell
# Check if service exists before installing
if (-not (Get-Service "dosomething" -ErrorAction SilentlyContinue)) {
    & ".\WinSW-x64.exe" install
}

# Wait for service to reach a specific state
Start-Service dosomething
$service = Get-Service dosomething
$service.WaitForStatus('Running', '00:00:30')

# Configure recovery options
sc.exe failure dosomething reset=86400 actions=restart/5000/restart/10000/reboot/60000

# Set service description and display name
sc.exe description dosomething "My custom service description"
sc.exe config dosomething DisplayName= "My Service Display Name"

# Get detailed service information
Get-CimInstance Win32_Service -Filter "Name='dosomething'" |
    Select-Object Name, State, StartMode, ProcessId, PathName

The sc.exe failure command configures Windows to automatically restart the service — after 5 seconds on first failure, 10 seconds on second, and reboot on subsequent failures.

Step 5: Verify at Startup

Restart Windows and verify your service started automatically:

sc query dosomething

Look for STATE: 4 RUNNING.

Viewing Logs

WinSW creates log files in the same directory as winsw.xml:

D:\dosomething\
├── dosomething.out.log   ← stdout from your program
├── dosomething.err.log   ← stderr from your program
└── dosomething.wrapper.log ← WinSW's own log

Runaway Process Killer Extension

WinSW includes a built-in plugin system. The RunawayProcessKiller extension prevents duplicate processes from accumulating on service restart — a common issue when the service restarts faster than the old process fully terminates.

Add this to your winsw.xml:

<extensions>
  <extension enabled="true"
    className="winsw.Plugins.RunawayProcessKiller.RunawayProcessKillerExtension"
    id="killOnStartup">
    <pidfile>%BASE%\pid.txt</pidfile>
    <stopTimeout>5000</stopTimeout>
    <stopParentFirst>true</stopParentFirst>
  </extension>
</extensions>

How it works:

  • On service start, WinSW reads the PID file written by the previous instance
  • If a process with that PID is still running, it terminates it before starting the new instance
  • stopTimeout controls how long to wait (in milliseconds) before force-killing
  • stopParentFirst ensures the parent process tree is terminated in order

This plugin is essential for services that write to locked files or bind to TCP ports — it prevents Address already in use errors on restart.

Security Best Practices

Use Least Privilege Accounts

Never run services as SYSTEM or Administrator unless absolutely necessary. WinSW supports the <serviceaccount> element for specifying a dedicated account:

<serviceaccount>
  <username>DOMAIN\svc-myapp</username>
  <password>****</password>
  <allowservicelogon>true</allowservicelogon>
</serviceaccount>

For domain-joined servers, use gMSA (group Managed Service Accounts) — they auto-rotate passwords and eliminate credential management:

# Create gMSA (done once by domain admin)
New-ADServiceAccount -Name gmsa-myapp -DNSHostName myapp.domain.com

# Install on the target server
Install-ADServiceAccount -Identity gmsa-myapp

# Reference in WinSW config
<serviceaccount>
  <username>DOMAIN\gmsa-myapp$</username>
  <managedaccount>true</managedaccount>
</serviceaccount>

Secure Config Files

The XML/YAML config files may contain credentials or sensitive paths:

  • Restrict permissions: icacls winsw.xml /inheritance:r /grant "SYSTEM:(R)" "Administrators:(R)"
  • Store configs in a protected directory, not world-readable locations
  • Use Windows DPAPI or a secrets manager for passwords instead of plaintext

Firewall and Network Security

  • If your service listens on a port, configure Windows Firewall rules explicitly
  • Bind to 127.0.0.1 instead of 0.0.0.0 for local-only services
  • Use sc.exe qc to verify the service runs under the expected account

CI/CD Integration

WinSW is well-suited for automated deployment pipelines. Use a PowerShell script in your CI/CD release process:

param(
    [string]$ServiceName = "myapp",
    [string]$SourceDir = ".\release",
    [string]$TargetDir = "C:\Services\$ServiceName"
)

# Stop and uninstall existing service
if (Get-Service $ServiceName -ErrorAction SilentlyContinue) {
    & "$TargetDir\WinSW-x64.exe" stop
    & "$TargetDir\WinSW-x64.exe" uninstall
}

# Copy fresh binaries
if (-not (Test-Path $TargetDir)) {
    New-Item -ItemType Directory -Path $TargetDir
}
Copy-Item "$SourceDir\*" $TargetDir -Recurse -Force

# Install and start
& "$TargetDir\WinSW-x64.exe" install
& "$TargetDir\WinSW-x64.exe" start

# Verify deployment
Start-Sleep -Seconds 3
$status = & "$TargetDir\WinSW-x64.exe" status
if ($status -eq "RUNNING") {
    Write-Host "Service deployed successfully." -ForegroundColor Green
} else {
    Write-Host "Service failed to start." -ForegroundColor Red
    exit 1
}

In GitHub Actions, GitLab CI, or Azure DevOps, call this script as a deployment step. WinSW’s non-interactive, file-based configuration makes it ideal for automated builds — no GUI prompts or user interaction required.

Windows Server 2025

Windows Server 2025 introduces several improvements relevant to WinSW:

  • Built-in OpenSSH: Manage services remotely over SSH without third-party tools
  • WinGet package manager: Install WinSW via winget install WinSW for quicker setup
  • Modern Task Manager: Better process tree visualization for debugging service child processes
  • Performance improvements: Faster service start times and reduced overhead for background services

WinSW remains the recommended approach for running custom applications as services on Windows Server 2025. Microsoft’s native sc and PowerShell cmdlets handle simple cases, but WinSW’s lifecycle hooks, log rotation, and plugin system provide production-grade reliability that built-in tools lack.

Troubleshooting

Service fails to start

Check dosomething.wrapper.log for errors. Common causes:

  • Wrong path to executable
  • Missing dependencies (DLLs, runtimes)
  • Insufficient permissions
# Run the executable manually first to check for errors
D:\dosomething\dosomething.exe -c dosomething.ini

Access denied errors

Run Command Prompt as Administrator. For services that need network access or specific file permissions, configure the service account in winsw.xml.

Service starts then immediately stops

Your program may be exiting. Check dosomething.err.log. Make sure your program runs as a foreground process (doesn’t daemonize itself).

Service Logs Show IOException (File Locked)

If your service crashes with IOException: The process cannot access the file because it is being used by another process, the logs are likely locked by a previous instance. Enable the RunawayProcessKiller extension to clean up stale processes.

Alternatively, configure log mode to avoid file contention:

<log mode="none"/>

Exit Code Analysis

WinSW logs the exit code of your process. Check dosomething.wrapper.log for lines like:

INFO  - Stopping dosomething
INFO  - Process exited with code -1

Exit code reference:

Exit Code Meaning Action
0 Normal exit No action needed (on intentional stop)
-1 (0xFFFFFFFF) Process killed or crashed Check event logs
1-255 Application-specific error Check application logs
> 255 WinSW wrapper error Check wrapper log

Event Viewer Integration

Windows logs all service events to the System event log:

  1. Open eventvwr.msc
  2. Navigate to Windows Logs → System
  3. Filter by Source: Service Control Manager
  4. Look for events 7036 (service state change), 7034 (service crash), 7040 (start type change)

Using sc query for Detailed Status

The sc query command provides more detail than Get-Service:

sc query dosomething
sc queryex dosomething   # Shows process ID and additional info
sc qc dosomething        # Shows service configuration

Look for STATE: 4 RUNNING and note the PID field — use it to correlate with Task Manager or tasklist.

Alternative: Native Windows Service Registration

For .NET applications or when you don’t want WinSW, use sc create directly:

sc create "MyService" ^
    binPath= "D:\myapp\myapp.exe --config config.yaml" ^
    DisplayName= "My Application" ^
    start= auto

sc description "MyService" "My application running as a Windows service"
sc start "MyService"

Alternative Wrappers Comparison

Several tools exist for running programs as Windows services:

Feature WinSW NSSM Servy Shawl
Status Active (v2.11 stable, v3 alpha) Abandoned (last release 2014) Active Active
Configuration XML / YAML GUI + Registry GUI + JSON CLI flags
GUI No Yes (ncurses-style) Yes (real-time monitoring) No
Log Rotation Built-in (size, date, count) Basic Built-in No
Hooks/Scripts Prestart, poststart, prestop, poststop None Limited None
Plugin System Yes (RunawayProcessKiller, etc.) No No No
CI/CD Friendly Yes (file-based config) Partial (relies on GUI) Yes Yes
Installation winget install WinSW Manual download Manual download winget install shawl
.NET Required No (self-contained) No Yes No

NSSM (Non-Sucking Service Manager)

NSSM was the go-to wrapper before WinSW matured:

nssm install dosomething "D:\dosomething\dosomething.exe"
nssm edit dosomething
nssm start dosomething

Important: NSSM has been abandoned since 2014. There are no security updates, no bug fixes, and no support for modern Windows features like gMSA or Windows Containers. Migrating to WinSW is recommended for any production deployment.

Servy

Servy is a modern wrapper with a real-time monitoring GUI:

  • Web-based dashboard for monitoring service status
  • JSON configuration
  • Automatic restart policies
  • Active development with regular releases

Shawl

Shawl is a lightweight alternative designed for simplicity:

  • Minimal configuration via command-line arguments only
  • Installable via Winget: winget install shawl
  • No config file required — ideal for quick setups
  • Best suited for simple services without log rotation or hooks

Resources

Comments

👍 Was this article helpful?