Windows Server
Tested on Windows Server 2019 and 2022.
Before starting, read Before You Deploy and complete the standalone output setup.
One-time server setup
Section titled “One-time server setup”- Install Node.js 22 (or newer) from nodejs.org using the MSI installer.
- Install IIS with the URL Rewrite and Application Request Routing (ARR) modules. These are what let IIS act as a reverse proxy.
- Install NSSM from nssm.cc. NSSM lets you run any program as a Windows Service, so your app starts automatically on boot and restarts if it crashes.
Deploy
Section titled “Deploy”-
On your build machine, run:
Terminal window npm --prefix web cinpm --prefix web run build -
On the server, create a deployment folder (e.g.
C:\apps\my-app\). Copy the contents ofweb\.next\standalone\(not the folder itself) into that root, then add the static and public folders:C:\apps\my-app\├── server.js├── package.json├── node_modules\├── .next\server\├── .next\static\ ← from web\.next\static\├── public\ ← from web\public\└── .env ← create this on the server -
Create
C:\apps\my-app\.envand fill in production values for every variable fromweb/.env.example. -
Register the app as a Windows Service. The standalone
server.jsdoesn’t auto-load.envfiles, so use Node 22’s built-in--env-fileflag to pass the values:Terminal window nssm install MyApp "C:\Program Files\nodejs\node.exe" "--env-file=C:\apps\my-app\.env" "C:\apps\my-app\server.js"nssm set MyApp AppDirectory "C:\apps\my-app"nssm set MyApp AppEnvironmentExtra +NODE_ENV=production +PORT=3000 +HOSTNAME=127.0.0.1New-Item -ItemType Directory -Force -Path "C:\apps\my-app\logs" | Out-Nullnssm set MyApp AppStdout "C:\apps\my-app\logs\stdout.log"nssm set MyApp AppStderr "C:\apps\my-app\logs\stderr.log"nssm set MyApp AppRotateFiles 1nssm set MyApp AppRotateBytes 10485760nssm start MyAppHOSTNAME=127.0.0.1means the Node process only accepts connections from IIS. Nothing outside the server can reach port 3000 directly. -
In IIS Manager, create a site bound to your hostname on port 443 with your TLS certificate. In the site’s
web.config, add a URL Rewrite rule that forwards all traffic tohttp://127.0.0.1:3000:<configuration><system.webServer><rewrite><rules><rule name="ProxyToNode" stopProcessing="true"><match url="(.*)" /><action type="Rewrite" url="http://127.0.0.1:3000/{R:1}" /><serverVariables><set name="HTTP_X_FORWARDED_PROTO" value="https" /><set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" /></serverVariables></rule></rules></rewrite><!-- preserveHostHeader passes the original Host header to Node. --><proxy enabled="true" preserveHostHeader="true" /></system.webServer></configuration>You may need to whitelist the
HTTP_X_FORWARDED_*variables under URL Rewrite → View Server Variables in IIS Manager before this takes effect.X-Forwarded-Protois hardcoded tohttpsin the rule above. This is safe only because the site is bound to port 443 alone. If you add a port 80 binding to redirect plain HTTP to HTTPS, put that redirect on a separate IIS site so this proxy rule never runs against plaintext requests.
TLS certificates
Section titled “TLS certificates”IIS doesn’t ship with a Let’s Encrypt client. The common options:
- win-acme (free, recommended): a CLI that fetches Let’s Encrypt certificates and installs them into IIS bindings. Run
wacs.exeinteractively the first time. It validates your domain, installs the certificate, binds it to port 443, and registers a scheduled task to auto-renew every 60 days. - Certify The Web: a GUI alternative. Free for up to five certificates.
- Commercial CA or internal PKI: import the issued
.pfxvia IIS Manager → Server Certificates → Import, then bind it to your site on port 443. Renewal is manual; set a reminder well before the expiry date.
Make sure renewal is automated and monitored. An expired certificate breaks the site for every user at once.
Verify
Section titled “Verify”Open your site in a browser. It should load over HTTPS without a port number.
Get-Service MyApp # is the Node process running?Get-Content "C:\apps\my-app\logs\stderr.log" -Tail 50 # any errors?IIS Manager’s Failed Request Tracing logs are useful for diagnosing proxy-level errors.