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
stopexecutablefor custom stop commandsrefreshcommand to reload configuration without restarting the servicedev ps,dev kill,dev listcommands 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 WindowsWinSW-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:
- Press
Win + R, typeservices.msc, press Enter - Find your service by name
- 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
stopTimeoutcontrols how long to wait (in milliseconds) before force-killingstopParentFirstensures 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.1instead of0.0.0.0for local-only services - Use
sc.exe qcto 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 WinSWfor 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:
- Open
eventvwr.msc - Navigate to Windows Logs → System
- Filter by Source: Service Control Manager
- 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
Comments