Installing software on Windows Server

Installing software with winget, getting past the Internet Explorer download block, and running an application so it survives sign-out.

Windows8 min read

On this page
  1. Installing with winget
  2. Finding and installing a package
  3. Listing, upgrading, and removing
  4. The Internet Explorer prompt that blocks downloads
  5. Use winget or PowerShell instead
  6. Copy the file from your own machine
  7. Turning IE ESC off, properly
  8. Installing common runtimes
  9. Windows roles and features
  10. Running an application as a service
  11. Why it stops
  12. Check whether the software installs its own service
  13. Registering an existing service binary
  14. Using a startup task instead
  15. Recovery for a real service
  16. Prove it with a reboot
  17. Let the traffic in
  18. Related

Installing software on Windows Server differs from a desktop in two ways that catch people out: the browser refuses to download anything until you deal with a security setting, and an application you start by hand dies when you sign out. This page covers both, plus winget and the runtimes most software depends on.

Installing with winget

winget is Microsoft's package manager. It installs from a command line, which means an install is one repeatable line rather than a browser session and a wizard.

Check whether you have it:

PowerShell
winget --version

It ships with Windows Server 2025 and with Windows 11. On Windows Server 2019 and 2022 it is not installed by default. To add it on a server with Desktop Experience, download the App Installer package, Microsoft.DesktopAppInstaller, from Microsoft's WinGet releases and install it with Add-AppxPackage, along with its Microsoft.VCLibs and Microsoft.UI.Xaml dependencies. On Server Core there is no Appx support, so use the vendor's own installer instead.

Finding and installing a package

PowerShell
winget search "7-zip"

Take the value in the Id column, and install it by that rather than by name, because names collide and ids do not:

PowerShell
winget install --id 7zip.7zip --exact

On a server you usually want the machine-wide copy and no prompts:

PowerShell
winget install --id 7zip.7zip --exact --scope machine --silent --accept-package-agreements --accept-source-agreements

--scope machine installs for every account rather than only the one you are signed in as, which matters when the software will later run as a service under a different account. --silent runs the underlying installer without its wizard. The two --accept switches skip the licence and source prompts, which otherwise stop an unattended run on the first invocation.

Listing, upgrading, and removing

PowerShell
winget list
winget upgrade
winget upgrade --all --include-unknown
winget uninstall --id 7zip.7zip

winget upgrade on its own shows what has a newer version. --include-unknown adds packages whose installed version winget cannot read, which is common for software that was installed before winget was.

The Internet Explorer prompt that blocks downloads

Open a browser on a fresh Windows Server, go to a download page, and you get a dialog headed Internet Explorer Enhanced Security Configuration is enabled telling you content from the site is blocked. Every site is untrusted, and you can add them to the trusted list one at a time forever without getting anywhere.

That setting, usually shortened to IE ESC, is on by default on Windows Server and it is on for a reason. Browsing the web from a server, signed in as an administrator, is how a server gets compromised by its owner. So the first two answers below are better than the third.

Use winget or PowerShell instead

If the software is in winget, install it with winget and the question does not arise. If it is not, fetch the installer directly:

PowerShell
Invoke-WebRequest -Uri "https://example.com/app.msi" -OutFile "C:\Temp\app.msi" -UseBasicParsing

-UseBasicParsing matters on Windows PowerShell 5.1. Without it, the command tries to use the Internet Explorer engine to parse the response and fails with a message about Internet Explorer's first-launch configuration not being complete, which is the same underlying setting wearing a different hat. On PowerShell 7 the switch is unnecessary and harmless.

Downloads arrive marked as coming from the internet, which makes some installers refuse to run. Clear the mark, and check what you fetched is what you meant to fetch, before running it:

PowerShell
Get-FileHash "C:\Temp\app.msi" -Algorithm SHA256
Unblock-File -Path "C:\Temp\app.msi"
msiexec /i "C:\Temp\app.msi" /qn /norestart

/qn runs the MSI with no interface and /norestart stops it rebooting the server underneath you.

Copy the file from your own machine

Remote Desktop can share a local drive with the server, which is often the quickest route for something you already have. Before connecting, in Remote Desktop Connection open Show Options, then Local Resources, then More, and tick the drive you want. It appears in the server's File Explorer under Redirected drives and folders. The clipboard works for small files and for text in the same way.

Turning IE ESC off, properly

If you genuinely need a browser on the server, turn the setting off for administrators only, do the download, and turn it back on in the same sitting.

In Server Manager, open Local Server, find IE Enhanced Security Configuration, and set Administrators to Off. Leave Users on.

From PowerShell:

PowerShell
$admins = "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A7-37EF-4b3f-8CFC-4F3A74704073}"
Set-ItemProperty -Path $admins -Name "IsInstalled" -Value 0

Sign out and back in for it to take effect. Put it back with the same command and -Value 1.

Installing common runtimes

Most software needs a runtime underneath it, and the failure when one is missing is rarely clear. A native application that closes instantly with a missing DLL message almost always wants the Visual C++ redistributables.

Package ids move as new major versions ship, so confirm with winget search before using one. These are the ids for the common runtimes:

Runtimewinget id
Visual C++ redistributablesMicrosoft.VCRedist.2015+.x64
.NET Desktop RuntimeMicrosoft.DotNet.DesktopRuntime.8
Node.js LTSOpenJS.NodeJS.LTS
Python 3Python.Python.3.12
Java, Eclipse TemurinEclipseAdoptium.Temurin.21.JDK
GitGit.Git
7-Zip7zip.7zip

Install the Visual C++ redistributables on any server that will run game servers, native applications, or anything distributed as a compiled binary. It costs nothing and it removes an entire class of confusing startup failure.

After installing a runtime, open a new PowerShell window before testing it. The installer adds the runtime to the system PATH, and a window that was already open keeps the old one, so node or python appears to be missing when it is installed correctly.

Windows roles and features

Roles are not packages, and on Windows Server they have their own cmdlets:

PowerShell
Get-WindowsFeature | Where-Object { $_.InstallState -eq "Installed" }
Install-WindowsFeature -Name Web-Server -IncludeManagementTools

-IncludeManagementTools is worth remembering. Without it you get IIS with no IIS Manager, which is a confusing thing to discover later.

Some features, .NET Framework 3.5 in particular, are not on the disk at all and need -Source pointed at the sxs folder of the matching Windows installation media.

Running an application as a service

This is the thing people most often get wrong. You install a game server, a bot, or an API, start it in a PowerShell window, confirm it works, close Remote Desktop, and some hours later it is gone.

Why it stops

Two different things happen depending on how you leave the session.

Closing the Remote Desktop window with the X leaves the session disconnected. Everything you started keeps running, which is exactly why this looks like it works.

Choosing Sign out ends the session and terminates every process started in it. So does a reboot, and so does the session being reaped after it has sat disconnected for a while.

That is the whole trap: the way people test leaves the process running, and the way the server eventually behaves does not. A disconnected session is also not supervision. It does not restart the application if it crashes, and it does not start it after a reboot.

Anything that needs to stay up needs to be a service or a startup task.

You close theRemote DesktopwindowYou sign outThe server rebootsThe applicationcrashesStarted by handIn a PowerShellwindowLooks fine:keeps runninguntil thesession isreapedTerminated withthe sessionGone, andnothing startsit againStays downA service or startup taskRegistered to run onits ownRuns,independent ofany sessionRunsStarts at boot,before anyonesigns inRestarts, onceyou set that upStarted by handIn a PowerShell windowYou close the Remote Desktop windowLooks fine: keeps running until the sessionis reapedYou sign outTerminated with the sessionThe server rebootsGone, and nothing starts it againThe application crashesStays downA service or startup taskRegistered to run on its ownYou close the Remote Desktop windowRuns, independent of any sessionYou sign outRunsThe server rebootsStarts at boot, before anyone signs inThe application crashesRestarts, once you set that up
The same application, started by hand and registered to run on its own. Closing the Remote Desktop window is the one case where starting it by hand looks fine, which is why it passes the first test; every other way the session or the program ends leaves it down. Illustration.

Check whether the software installs its own service

Read its documentation first. Databases, web servers, and most serious server software ship a service installer or an --install-service style flag, and using it is better than anything you build yourself. Skip the rest of this section if so.

Registering an existing service binary

If the executable is genuinely service-aware, register it:

PowerShell
New-Service -Name "myapp" -BinaryPathName "C:\apps\myapp.exe" -DisplayName "My App" -StartupType Automatic

The older tool does the same, and note that a space after each = is required, not optional:

PowerShell
sc.exe create myapp binPath= "C:\apps\myapp.exe" start= auto

To supervise a normal executable as a service you need a service wrapper such as NSSM or WinSW. Both are third-party tools you download and configure yourself, and neither is part of Windows.

An application that must stay upA game server, a bot or an APIDoes the software ship its own service installer?YesUse itBetter than anything you buildyourselfNoIs the executable service-aware?YesRegister itNew-Service, or sc.exe createNoA plain console programRegistered as a service it never reports back, so Windows killsit: Error 1053.InsteadA service wrapperNSSM or WinSW, third-partytools you download andconfigureA startup taskTask Scheduler, built in:starts at boot and canrestart itWhichever route: prove it with a rebootRestart-Computer, then check it is running without you startingitAn application that must stay upA game server, a bot or an APIDoes the software ship its own service installer?Yes: Use itBetter than anything you build yourselfNoIs the executable service-aware?Yes: Register itNew-Service, or sc.exe createNoA plain console programRegistered as a service it never reportsback, so Windows kills it: Error 1053.InsteadA service wrapperNSSM or WinSW, third-party tools you downloadand configureA startup taskTask Scheduler, built in: starts at boot andcan restart itWhichever route: prove it with a rebootRestart-Computer, then check it is runningwithout you starting it
The routes this section walks through, in the order to try them. Registering a plain console program as a service is the dead end that ends in Error 1053; a wrapper or a startup task is the way round it, and a reboot is the test for every route. Illustration.

Using a startup task instead

If you would rather not add a third-party wrapper, Task Scheduler is built in and covers the important parts: it starts at boot, before anyone signs in, it runs independently of any session, and it can restart the application if it exits.

PowerShell
$action = New-ScheduledTaskAction -Execute "C:\apps\myapp.exe" -WorkingDirectory "C:\apps"
$trigger = New-ScheduledTaskTrigger -AtStartup
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit (New-TimeSpan -Seconds 0)
Register-ScheduledTask -TaskName "myapp" -Action $action -Trigger $trigger -Principal $principal -Settings $settings

-ExecutionTimeLimit (New-TimeSpan -Seconds 0) means no time limit, which you need, because the default stops a long-running task after three days. -RestartCount and -RestartInterval have to be given together.

Start it now rather than waiting for a reboot, and read back what happened:

PowerShell
Start-ScheduledTask -TaskName "myapp"
Get-ScheduledTaskInfo -TaskName "myapp"

Three things about running as SYSTEM are worth knowing before you do:

  • There is no window. SYSTEM has no desktop, so a console application's output goes nowhere. The application must write a log file, or you will have no idea what it is doing.
  • Use absolute paths everywhere, in the task and in the application's own configuration. SYSTEM has a different profile directory and no mapped drives.
  • SYSTEM is highly privileged. For anything exposed to the internet, create a dedicated local account with only the access it needs and register the task under that instead, which requires supplying that account's password when you register it.

Recovery for a real service

A registered service can restart itself after a crash, but not by default:

PowerShell
sc.exe failure myapp reset= 86400 actions= restart/60000/restart/60000/restart/60000
Set-Service -Name myapp -StartupType Automatic

That restarts the service a minute after each of the first three failures, and resets the failure count after a day.

Prove it with a reboot

Whichever route you took, the only test that counts is a restart:

PowerShell
Restart-Computer

Reconnect and confirm the application is running without you having started it:

PowerShell
Get-Service -Name myapp
Get-Process -Name myapp

If a service failed to start, the reason is in the System event log:

PowerShell
Get-WinEvent -LogName System -MaxEvents 30 |
  Where-Object { $_.ProviderName -eq "Service Control Manager" } |
  Format-Table TimeCreated, Id, Message -Wrap

Let the traffic in

An application that is running is still unreachable until its port is open. Add the Windows Firewall rule with Using Windows Firewall, and add the matching Port rule in the panel so the port's rate limits and protocol checks apply to it.

Need a server, or a hand with one?

Pick a plan to deploy a server, or open a ticket in the panel. Support runs 24/7/365.