IIS is the one Windows Server role almost every hosted VM ends up running, and it is also the role people install by clicking through Server Manager and then never configure properly. This guide installs IIS from PowerShell, creates a dedicated application pool, publishes a site on a host header, opens the firewall correctly, and gets HTTPS working — the full path from a bare remote server to a live site.
Prerequisites
- A remote Windows Server (2019, 2022, or 2025) with administrator RDP access — see windows-vps.org if you still need a plan.
- PowerShell 5.1 or 7, running as Administrator.
- A DNS record you can edit, if you want a real hostname.
- Roughly 1.5 GB of free system disk for the role plus its logs.
Step 1 — Install the Web Server Role
# Full IIS with management tools, .NET, and common extras
Install-WindowsFeature -Name Web-Server, Web-Mgmt-Console, Web-Asp-Net45, Web-Net-Ext45, `
Web-Http-Logging, Web-Stat-Compression, Web-Dyn-Compression, Web-Filtering, `
Web-Basic-Auth, Web-Windows-Auth, Web-Mgmt-Service, Web-Request-Monitor `
-IncludeManagementTools
# Confirm
Get-WindowsFeature Web-Server | Select-Object Name, InstallState
On Windows 10/11 the equivalent is the DISM feature name:
Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServerRole, IIS-WebServer, IIS-ManagementConsole -All
Step 2 — Verify the Default Site Answers
Get-Service W3SVC | Select-Object Name, Status, StartType
Invoke-WebRequest http://localhost -UseBasicParsing | Select-Object StatusCode
(Get-Website -Name 'Default Web Site').State
A 200 response with the blue IIS welcome page means the service is healthy. If the service will not start, check the Windows Event Log under Application for WAS errors — a missing .NET feature is the usual culprit.
Step 3 — Create a Dedicated Application Pool and Site
$site = 'mysite.com'
$root = "C:\inetpub\$site"
$pool = "pool-$site"
New-Item -ItemType Directory -Path $root -Force | Out-Null
New-Item -ItemType Directory -Path "$root\logs" -Force | Out-Null
# Application pool: no managed code if the site is static/Node/PHP
New-WebAppPool -Name $pool
Set-ItemProperty "IIS:\AppPools\$pool" -Name managedRuntimeVersion -Value 'v4.0'
Set-ItemProperty "IIS:\AppPools\$pool" -Name processModel.idleTimeout -Value '00:00:00' # never idle out
Set-ItemProperty "IIS:\AppPools\$pool" -Name recycling.periodicRestart.time -Value '00:00:00' # recycle on schedule instead
# The site itself
New-Website -Name $site -PhysicalPath $root -ApplicationPool $pool -HostHeader $site -Port 80
# A plain test page so you have something to curl
Set-Content "$root\index.html" "<h1>$site is live on IIS</h1>"
Start-Website -Name $site
Get-Website | Format-Table Name, State, PhysicalPath, ApplicationPool
Two defaults are worth changing immediately. First, idleTimeout of 20 minutes unloads your app pool and makes the first request after a quiet period pay a multi-second warm-up — set it to zero. Second, the default periodic recycle every 1,740 minutes interrupts requests; schedule it during a known maintenance window using recycling.periodicRestart.schedule instead.
Step 4 — Open the Firewall Correctly
Installing the Web-Server feature creates an inbound rule, but it is bound to the “Public” profile only in some configurations and does not cover HTTPS. Define your own rule explicitly:
New-NetFirewallRule -DisplayName 'IIS HTTP 80' -Direction Inbound -Protocol TCP -LocalPort 80 -Action Allow -Profile Any
New-NetFirewallRule -DisplayName 'IIS HTTPS 443' -Direction Inbound -Protocol TCP -LocalPort 443 -Action Allow -Profile Any
# Verify what is listening
Get-NetTCPConnection -State Listen | Where-Object LocalPort -in 80,443 | Format-Table LocalAddress, LocalPort, OwningProcess
Get-NetFirewallRule -DisplayName 'IIS*' | Format-Table DisplayName, Enabled, Direction, Action
A common mistake is deleting the built-in rules instead of disabling them. Disable with Disable-NetFirewallRule so you can reverse the decision later.
Step 5 — Point a Domain at It
Create an A record for mysite.com (and a www CNAME) pointing at the server’s public IP. On the server, add a second binding so both names resolve to the same site:
New-WebBinding -Name 'mysite.com' -Protocol http -Port 80 -HostHeader 'www.mysite.com'
Get-WebBinding -Name 'mysite.com' | Format-Table protocol, bindingInformation
Host headers are what let one server host many sites on one IP. Without them, the second site you create will either collide on port 80 or require its own IP address. If you need to host several domains, plan the host-header table before you add bindings — retrofitting is more painful than doing it first.
Step 6 — Fix NTFS Permissions for the App Pool Identity
By default an app pool runs as ApplicationPoolIdentity, a virtual account named IIS AppPool\pool-mysite.com. Grant it read access to the content and nothing more:
icacls "C:\inetpub\mysite.com" /grant "IIS AppPool\pool-mysite.com:(OI)(CI)(RX)" /T
icacls "C:\inetpub\mysite.com\logs" /grant "IIS AppPool\pool-mysite.com:(OI)(CI)(M)" /T
# Inspect current ACLs
Get-Acl 'C:\inetpub\mysite.com' | Format-List
Never grant Everyone:Full or add the app pool to the Administrators group. If your app must write to a folder, give it Modify on that one folder only — that is the entire point of running the pool under a low-privilege identity.
Step 7 — Add HTTPS
# Import a PFX (keystore) certificate
$pw = ConvertTo-SecureString -String 'YourPfxPassword' -AsPlainText -Force
Import-PfxCertificate -FilePath C:\certs\mysite.pfx -CertStoreLocation Cert:\LocalMachine\My -Password $pw
# Bind it — must be SNI-enabled on modern servers
New-WebBinding -Name 'mysite.com' -Protocol https -Port 443 -HostHeader 'mysite.com' -SslFlags 1
Get-Item Cert:\LocalMachine\My | Select-Object Subject, Thumbprint, NotAfter
| SslFlags value | Meaning | When to use |
|---|---|---|
| 0 | No SNI, single cert on the IP | Legacy clients only |
| 1 | SNI enabled | Multiple HTTPS sites on one IP — the normal choice |
| 3 | SNI + Central Certificate Store | Farm / many-certificate deployments |
Troubleshooting Reference
| Symptom | Cause | Fix |
|---|---|---|
| HTTP 500.19 | Config section locked or malformed web.config | Unlock section in applicationHost.config; validate XML |
| HTTP 403.14 | Directory browsing disabled and no default document | Add a default document or publish index.html |
| HTTP 404.4 / 404.7 | No route/file, or extension not allowed | Check request filtering fileExtensions |
| HTTP 503 | App pool stopped or crashed repeatedly | Enable-WebAppPoolRapidFailProtection -Enable $false to see the real error |
| Site works on localhost, not from outside | Firewall or host header binding | Re-check the rule and add the public hostname binding |
Next Steps
- Enable request logging and set up Failed Request Tracing for 500s.
- Add response compression and a cache header policy for static assets.
- Harden the server: remove unused modules, disable directory browsing, set request limits.
- Automate the whole build as a PowerShell script you can re-run on a fresh VM.
Everything above is scriptable, which is the real advantage of a remote Windows server: the entire stack from a blank template to a TLS-terminating site is a single idempotent script. If you are sizing the machine for this workload, windows-vps.org covers what RAM and disk an IIS deployment realistically needs.


