Deploying Angular + Spring Boot on IIS (Windows)
This will guide you on how to setup the deployment of an Angular SPA and a Spring Boot API run together under a single IIS site.
IIS serves static Angular files directly and reverse-proxies /api/* traffic to the Spring Boot process.
A scheduled watchdog task handles zero-touch JAR updates.
Architecture overview
Browser
│
▼
IIS site (port 443 / 80)
├── /api/* ──► reverse proxy ──► localhost:6001/api/* (Spring Boot)
└── /* ──► index.html (Angular SPA)
C:\inetpub\wwwroot\
├── YourApplication\ ← Name of you application | Windows Service | Scheduled Task(Watchdog for jar re deployments)
├── frontend\ ← IIS site root (Angular files + web.config)
└── api\ ← Java service folder (JAR + WinSW + scripts)
Windows Task Scheduler
└── "YourApplication API Watchdog" (runs every 1 min as SYSTEM)
└── update-api.ps1 — detects api-update.jar and hot-swaps the JAR
The Spring Boot JAR runs as a Windows service managed by WinSW. IIS never spawns or stops Java — it only forwards requests. The API folder lives beside the site root, not inside it.
Prerequisites
Step 1 — Install IIS modules
Open Server Manager → Add Roles and Features and enable:
- Web Server (IIS) with Static Content, Default Document, HTTP Errors
- URL Rewrite — download from iis.net/downloads/microsoft/url-rewrite
- Application Request Routing (ARR) — download from iis.net/downloads/microsoft/application-request-routing
After installing ARR, enable the global proxy feature once (run as Administrator):
& "$env:SystemRoot\system32\inetsrv\appcmd.exe" set config `
-section:system.webServer/proxy /enabled:true
Or via IIS Manager: Application Request Routing Cache → Server Proxy Settings → Enable proxy.
Step 2 — Install Java
Download and install Amazon Corretto JDK 17+. The path used on this server:
C:\Program Files\Amazon Corretto\jdk25.0.2_10\bin\java.exe
Step 3 — Download WinSW
WinSW wraps any executable as a Windows service.
Download the latest WinSW-x64.exe from the GitHub releases page. Rename it to myapp.exe (the XML config file name must match the EXE name).
Folder structure
The deployment uses two separate directories:
C:\inetpub\wwwroot\YourApplication
├── frontend\ ← IIS site root
│ ├── index.html
│ ├── main-XXXXXXXX.js
│ ├── styles-XXXXXXXX.css
│ ├── chunk-*.js
│ └── web.config
│
└── api\ ← Java service folder
├── myapp.jar ← active Spring Boot JAR
├── prev-myapp.jar ← previous JAR (rollback copy)
├── myapp.exe ← WinSW binary
├── myapp.xml ← WinSW service descriptor
├── application.yml ← Spring Boot config
├── update-api.ps1 ← watchdog/hot-swap script
├── deploy.log ← watchdog log
└── logs\ ← WinSW + app logs
Step 4 — Create the application.yml
Place application.yml in the API folder (C:\inetpub\wwwroot\YourApplication\api\). Spring Boot picks it up automatically at startup because WinSW sets the working directory to this folder.
The two keys that tie everything together:
server:
port: 6001 # internal port — not exposed externally
servlet:
context-path: /api # must match the rewrite rule prefix in web.config
Full example (application.yml on this server):
spring:
application:
name: getinline-api
datasource:
url: jdbc:postgresql://hosanna-solutions.com:5433/getinline
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:secret}
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 10
minimum-idle: 2
connection-timeout: 30000
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
mail:
host: ${MAIL_HOST:smtp.gmail.com}
port: ${MAIL_PORT:587}
username: ${MAIL_USERNAME:you@gmail.com}
password: ${MAIL_PASSWORD:app-password}
server:
port: 6001
servlet:
context-path: /api
management:
endpoints:
web:
exposure:
include: health, info, metrics, loggers
logging:
level:
root: INFO
Step 5 — Create the WinSW service descriptor
Create myapp.xml in the same folder as myapp.exe. The file name must exactly match the EXE name.
<service>
<id>YourApplication</id>
<name>YourApplication</name>
<description>YourApplication for any purpose</description>
<executable>C:\Program Files\Amazon Corretto\jdk25.0.2_10\bin\java.exe</executable>
<arguments>-jar "C:\inetpub\wwwroot\YourApplication\api\myapp.jar"</arguments>
<workingdirectory>C:\inetpub\wwwroot\YourApplication\api</workingdirectory>
<logpath>C:\inetpub\wwwroot\YourApplication\api\logs</logpath>
<log mode="roll-by-size">
<sizeThreshold>10240</sizeThreshold> <!-- KB -->
<keepFiles>8</keepFiles>
</log>
<startmode>Automatic</startmode>
<stopparentprocessfirst>true</stopparentprocessfirst>
<onfailure action="restart" delay="60000"/>
<onfailure action="none"/>
<resetfailure>1 hour</resetfailure>
</service>
<workingdirectory> is critical — Spring Boot resolves relative paths (like the external application.yml) from here.
Step 6 — Install and start the Windows service
Run the following as Administrator from the API folder:
cd C:\inetpub\wwwroot\YourApplication\api
.\myapp.exe install # registers the Windows service
.\myapp.exe start # starts it
# Verify it is running
Get-Service YourApplication
Confirm the API is up before touching IIS:
Invoke-WebRequest http://localhost:6001/api/actuator/health -UseBasicParsing
To stop or uninstall later:
.\myapp.exe stop
.\myapp.exe uninstall
Step 7 — Create the IIS site
- Open IIS Manager.
- Right-click Sites → Add Website.
- Set Site name to
yourapplication.hosanna-solutions.com. - Set Physical path to
C:\inetpub\wwwroot\YourApplication\frontend\. - Bind to port 80 / 443 with the hostname
getinlineapp.hosanna-solutions.com. - If using HTTPS, assign the SSL certificate (see the SSL Certificates guide).
No application pool changes are needed — IIS serves static files and proxies requests; it runs no managed code.
Step 8 — Deploy Angular files and create web.config
Copy the Angular production build output into the site root:
# Build locally
ng build --configuration production
# Push to the server (adjust source path to your dist folder)
robocopy .\dist\YourApplication\browser\ C:\inetpub\wwwroot\YourApplication\frontend\ /MIR /XF web.config
Then place this web.config in the site root (same folder as index.html):
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<!-- Rule 1: forward /api/* to the Spring Boot process -->
<rule name="ReverseProxy to Java API" stopProcessing="true">
<match url="^api/(.*)" />
<action type="Rewrite" url="http://localhost:6001/api/{R:1}" appendQueryString="true" />
</rule>
<!-- Rule 2: Angular HTML5 routing — serve index.html for any
path that is not a real file or directory -->
<rule name="Angular Routes" stopProcessing="true">
<match url=".*" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Rewrite" url="/index.html" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>
Rule order matters: /api/* is matched first and proxied to Spring Boot; everything else that is not a real file falls through to index.html so Angular's router handles it.
Step 9 — Create the watchdog update script
Create update-api.ps1 in the API folder. The scheduled task runs this every minute. When it finds api-update.jar, it stops the service, rotates the JARs, and restarts — no manual SSH needed.
# update-api.ps1
# Run via Windows Task Scheduler every minute.
# Watches for api-update.jar; when found: swaps JARs and restarts the service.
$AppDir = "C:\inetpub\wwwroot\YourApplication\api"
$ServiceExe = Join-Path $AppDir "myapp.exe"
$UpdateJar = Join-Path $AppDir "api-update.jar"
$CurrentJar = Join-Path $AppDir "myapp.jar"
$PreviousJar = Join-Path $AppDir "prev-myapp.jar"
$LogFile = Join-Path $AppDir "deploy.log"
function Write-Log {
param([string]$Message)
$ts = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
Add-Content -Path $LogFile -Value "[$ts] $Message"
}
if (-not (Test-Path $UpdateJar)) { exit 0 }
Write-Log "api-update.jar detected - starting hot-swap"
try {
Write-Log "Stopping service..."
& $ServiceExe stop
Start-Sleep -Seconds 3
if (Test-Path $PreviousJar) {
Remove-Item $PreviousJar -Force
Write-Log "Removed prev-myapp.jar"
}
if (Test-Path $CurrentJar) {
Rename-Item -Path $CurrentJar -NewName "prev-myapp.jar" -Force
Write-Log "Renamed myapp.jar -> prev-myapp.jar"
}
Rename-Item -Path $UpdateJar -NewName "myapp.jar" -Force
Write-Log "Renamed api-update.jar -> myapp.jar"
Write-Log "Starting service..."
& $ServiceExe start
Write-Log "Hot-swap complete"
} catch {
Write-Log "ERROR: $_"
# Rollback if the current JAR is missing
if (-not (Test-Path $CurrentJar) -and (Test-Path $PreviousJar)) {
Write-Log "Rolling back to prev-myapp.jar..."
Rename-Item -Path $PreviousJar -NewName "myapp.jar" -Force
& $ServiceExe start
Write-Log "Rollback complete - previous version restored"
}
exit 1
}
Step 10 — Register the watchdog as a scheduled task
Run as Administrator. This creates the "YourApplication API Watchdog" task that runs update-api.ps1 every minute as SYSTEM with no login required.
$action = New-ScheduledTaskAction `
-Execute "powershell" `
-Argument "-ExecutionPolicy Bypass -NonInteractive -File C:\inetpub\wwwroot\getinlineapp-api\update-api.ps1"
$trigger = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Minutes 1) -Once `
-At (Get-Date)
$settings = New-ScheduledTaskSettingsSet `
-MultipleInstances IgnoreNew `
-ExecutionTimeLimit (New-TimeSpan -Minutes 5)
Register-ScheduledTask `
-TaskName "YourApplication API Watchdog" `
-Action $action `
-Trigger $trigger `
-Settings $settings `
-RunLevel Highest `
-User "SYSTEM"
Verify the task exists:
Get-ScheduledTask -TaskName "YourApplication API Watchdog"
To remove it later:
Unregister-ScheduledTask -TaskName "YourApplication API Watchdog" -Confirm:$false
Deploying updates
Manual
No service restart needed.
Just push new static files in frontend folder and the api-update.jar in api folder (Watchdog will run the update for you within a minute).
Important to never lose the web.config file.
C:\inetpub\wwwroot\YourApplication\frontend\*
C:\inetpub\wwwroot\YourApplication\api\api-update.jar
Automatic
Take a look at ftp-deploy - Deploying files with ftp-deploy
Automated setup with setup-app.ps1
Downloads
- myapp.exe — WinSW binary (current: v2.12.0)
- setup-app.ps1 — full bootstrap script
This script replaces Steps 4–10 in a single run. It uses the folder name as the service name and only asks for the port.
Before running
Create a folder named after your app and place these three files inside it:
C:\inetpub\wwwroot\YourApplication\
├── application.yml ← Spring Boot config (must have a server: block)
├── myapp.exe ← WinSW binary (download above)
└── setup-app.ps1 ← this script
Optionally include myapp.jar if you have a build ready. If not, the service is installed but not started — deploy the JAR later via the watchdog (drop api-update.jar).
Run it
# Run as Administrator from the app folder
cd C:\inetpub\wwwroot\YourApplication
.\setup-app.ps1
Single prompt:
Internal port (e.g. 8080): 6001
What it does
| Step | Action |
|---|---|
| 1 | Derives service name from the folder name (YourApplication) |
| 2 | Creates api\ and frontend\ subfolders |
| 3 | Copies application.yml and myapp.exe (and myapp.jar if present) into api\ |
| 4 | Auto-detects java.exe from PATH; falls back to the Corretto default path |
| 5 | Updates server.port in api\application.yml |
| 6 | Generates api\myapp.xml (WinSW service descriptor) |
| 7 | Generates api\update-api.ps1 (watchdog hot-swap script, paths hardcoded) |
| 8 | Generates frontend\web.config (reverse proxy + Angular routing rules) |
| 9 | Installs the Windows service (uninstalls first if it already exists) |
| 10 | Starts the service if myapp.jar is present |
| 11 | Registers the YourApplication API Watchdog scheduled task (every 1 min, SYSTEM) |
After the script
The resulting layout:
C:\inetpub\wwwroot\YourApplication\
├── application.yml ← original (kept)
├── myapp.exe ← original (kept)
├── setup-app.ps1 ← original (kept)
├── api\
│ ├── application.yml ← copied + server.port updated
│ ├── myapp.exe ← copied
│ ├── myapp.xml ← WinSW descriptor
│ ├── update-api.ps1 ← watchdog script
│ └── logs\
└── frontend\
└── web.config ← IIS rewrite rules
Two remaining manual steps:
-
IIS site — point the site's Physical Path to
frontend\:C:\inetpub\wwwroot\YourApplication\frontend\ -
Angular files — copy the production build into
frontend\:powershell ng build --configuration production robocopy .\dist\my-app\ C:\inetpub\wwwroot\YourApplication\frontend\ /MIR /XF web.config
Troubleshooting
| Symptom | Check |
|---|---|
502 Bad Gateway on /api/* |
Spring Boot not running — Get-Service YourApplication, check logs\YourApplication.out.log |
| Angular routes return 404 | The Angular Routes rule is missing or web.config is not in the site root |
| Blank page / JS 404 | <base href="/"> missing in Angular build, or files deployed to wrong path |
| Service won't start | Wrong java.exe path in myapp.xml, or port already in use (netstat -ano \| findstr :6001) |
| ARR proxy not working | ARR proxy not enabled — run the appcmd command from Step 1 |
| Watchdog not triggering | Check task status: Get-ScheduledTask "YourApplication API Watchdog", check deploy.log for errors |
| Watchdog ran but service failed | Check logs\YourApplication.out.log; the previous JAR is kept as prev-myapp.jar for manual rollback |