Skip to content

Windows Server

Tested on Windows Server 2019 and 2022.

Before starting, read Before You Deploy and complete the standalone output setup.

  1. Install Node.js 22 (or newer) from nodejs.org using the MSI installer.
  2. Install IIS with the URL Rewrite and Application Request Routing (ARR) modules. These are what let IIS act as a reverse proxy.
  3. 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.
  1. On your build machine, run:

    Terminal window
    npm --prefix web ci
    npm --prefix web run build
  2. On the server, create a deployment folder (e.g. C:\apps\my-app\). Copy the contents of web\.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
  3. Create C:\apps\my-app\.env and fill in production values for every variable from web/.env.example.

  4. Register the app as a Windows Service. The standalone server.js doesn’t auto-load .env files, so use Node 22’s built-in --env-file flag 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.1
    New-Item -ItemType Directory -Force -Path "C:\apps\my-app\logs" | Out-Null
    nssm 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 1
    nssm set MyApp AppRotateBytes 10485760
    nssm start MyApp

    HOSTNAME=127.0.0.1 means the Node process only accepts connections from IIS. Nothing outside the server can reach port 3000 directly.

  5. 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 to http://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-Proto is hardcoded to https in 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.

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.exe interactively 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 .pfx via 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.

Open your site in a browser. It should load over HTTPS without a port number.

Terminal window
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.