Skip to content

Instantly share code, notes, and snippets.

@maxfierke
Last active September 8, 2020 14:34
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save maxfierke/a85ba9d717d6e121405c to your computer and use it in GitHub Desktop.
Save maxfierke/a85ba9d717d6e121405c to your computer and use it in GitHub Desktop.
PM2 as a Windows Service under Local Service

PM2 as a Windows Service under Local Service

This is a PoC for running PM2 as a Windows Service under the Local Service account instead of the Local System account.

Prerequsites

  • Neither pm2 or pm2-windows-service installed yet. (The Powershell script will run npm i)
    • At the very least, you should run pm2-service-uninstall before running this script
  • npm and npm-cache global folders should be somewhere accessible to NT AUTHORITY\LocalService.

Use

  • Copy install.ps1 to the root of your node app

  • From an Elevated Powershell prompt run: .\install.ps1 -Pm2Home "C:\etc\.pm2" -AppStart "[path to node app start]"

  • Your Node app should be running under PM2. pm2 monit should show processes running.

# Deploys application as an app within a PM2 service
# Run from root of Node application
param(
[string] $Pm2Home = $env:PM2_HOME,
[string] $AppStart = "app.js"
)
$ErrorActionPreference = "Stop"
function Install-Node-Modules
{
Write-Host "Running npm install"
& "npm" i
}
function Create-Pm2-Home
{
Write-Host "Attempting to create $Pm2Home and give FullControl to LOCAL SERVICE"
New-Item -ItemType Directory -Force -Path $Pm2Home
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
"LOCAL SERVICE", "FullControl", "ContainerInherit, ObjectInherit",
"None", "Allow")
try {
$acl = Get-Acl -Path $Pm2Home -ErrorAction Stop
$acl.SetAccessRule($rule)
Set-Acl -Path $Pm2Home -AclObject $acl -ErrorAction Stop
Write-Host "Successfully set FullControl permissions on $Pm2Home"
} catch {
throw "$Pm2Home : Failed to set permissions. Details : $_"
}
}
function Install-Pm2-Service
{
Write-Host "Installing pm2"
& "npm" i "pm2@latest" "-g"
Write-Host "Installing pm2-windows-service npm module"
& "npm" i pm2-windows-service "-g"
& "pm2-service-install"
# Create wrapper log file, otherwise it won't start
$wrapperLogPath = "$(npm config get prefix)\node_modules\pm2-windows-service\src\daemon\pm2.wrapper.log"
if (Test-Path $wrapperLogPath) {
Write-Debug "PM2 service wrapper log file already exists"
} else {
Out-File $wrapperLogPath -Encoding utf8
}
}
function Create-Pm2-Service-Config
{
param([string] $ConfigPath, [string] $CmdPath)
$configContent = @"
{
"apps": [{
"name": "node-app",
"script": "$($CmdPath -replace "\\","\\")",
"args": [],
"cwd": "$((Split-Path $CmdPath) -replace "\\","\\")",
"merge_logs": true,
"instances": 4,
"exec_mode": "cluster_mode",
"env": {
"NODE_ENV": "development"
}
}]
}
"@
# Write out config to JSON file
Write-Host "Writing PM2 service configuration to $ConfigPath"
$configContent | Out-File $ConfigPath -Encoding utf8
}
# From http://stackoverflow.com/a/4370900/964356
function Set-ServiceAcctCreds
{
param([string] $serviceName, [string] $newAcct, [string] $newPass)
$filter = "Name='$serviceName'"
$tries = 0
while (($service -eq $null -and $tries -le 3)) {
if ($tries -ne 0) {
sleep 2
}
$service = Get-WMIObject -namespace "root\cimv2" -class Win32_Service -Filter $filter
$tries = $tries + 1
}
if ($service -eq $null) {
throw "Could not find '$serviceName' service"
}
$service.Change($null,$null,$null,$null,$null,$null,$newAcct,$newPass)
$service.StopService()
while ($service.Started) {
sleep 2
$service = Get-WMIObject -namespace "root\cimv2" -class Win32_Service -Filter $filter
}
$service.StartService()
}
function Change-Pm2-Service-Account
{
Write-Host "Changing PM2 to run as LOCAL SERVICE"
Set-ServiceAcctCreds -serviceName "pm2.exe" -newAcct "NT AUTHORITY\LocalService" -newPass ""
}
$env:PM2_HOME = $Pm2Home
$env:PM2_SERVICE_SCRIPTS = "$Pm2Home\ecosystem.json"
[Environment]::SetEnvironmentVariable("PM2_HOME", $env:PM2_HOME, "Machine")
[Environment]::SetEnvironmentVariable("PM2_SERVICE_SCRIPTS", $env:PM2_SERVICE_SCRIPTS, "Machine")
& Install-Node-Modules
& Create-Pm2-Home
& Create-Pm2-Service-Config -ConfigPath $env:PM2_SERVICE_SCRIPTS -CmdPath $AppStart
& Install-Pm2-Service
& Change-Pm2-Service-Account
@mauron85
Copy link

mauron85 commented Jul 28, 2019

Awesome script. Many thanks for that. I found small permission issue on Windows 2008 server, which prevented service from starting.
It maybe environmental issue, but LOCAL SERVICE didn't had permission to write pm2.err.log and pm2.out.log so modified your script to add FullControl on C:\ProgramData\npm\node_modules\pm2-windows-service\src\daemon.

Also some more modifications.

  1. replaced original pm2-windows-service with innomizetech's fork
    (resolves issue, where service is not properly installed)

  2. Unattended service install - (script adds environmental variables itself, rather than asking user if he/she wants to do the setup).

If anybody interested, here is the modified gist: https://gist.github.com/mauron85/e55b3b9d722f91366c50fddf2fca07a4

@zubair1024
Copy link

Here is an alternate way of going about it:
https://gist.github.com/zubair1024/8f6126db7ffbafd706f0e328ef8d4662

@jessety
Copy link

jessety commented May 9, 2020

This is fantastic, thank you @maxfierke!

I adapted @mauron85's version of this script into a standalone installer, with a few additions:

  • Optionally automate migrating npm and npm-cache folders to C:\ProgramData\
  • Enable offline installation by caching packages on a build machine
  • Query for the Local Service user by its security identifier to better support non-English Windows installations

Hopefully this is helpful to someone: https://github.com/jessety/pm2-installer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment