Every Angular project eventually needs a home on a real server, and for a lot of our .NET shops that home is IIS. It is a five-minute job once you have done it a few times, but the first time trips people up in predictable places — a blank white screen, working routes on the home page that 404 on refresh, or a base href pointing at the wrong path. This is the checklist we actually use when we ship an Angular app to IIS.
Building for Production
Start with a production build, not the dev server output. The dev build is unminified, includes source maps by default, and is not something you want sitting on a public web server.
ng build --configuration production --base-href /myapp/
# Angular CLI 17+ places output under dist/<project-name>/browser
# older CLI versions place it directly under dist/<project-name>
The --base-href flag matters more with Angular than it does with most other SPA frameworks. Angular's router and its HttpClient use the value in the base href tag to resolve relative asset and route paths at runtime, so if the app is not hosted at the domain root — say it lives under /myapp/ — this has to be set correctly or you will get 404s on lazy-loaded chunks and images even though the routes themselves look fine. Copy the contents of the resulting dist folder (the browser subfolder if you are on CLI 17+) into your IIS site's physical path.
The URL Rewrite Rule
Angular's router uses the HTML5 History API by default, which means a URL like /myapp/dashboard is not a real file on disk — it only makes sense to Angular's client-side router. IIS does not know that, so a hard refresh or a bookmarked deep link results in a 404 from the server before Angular ever gets a chance to run. The fix is to install the URL Rewrite module and add a rewrite rule that redirects any unmatched request back to index.html, letting Angular's router take over from there.
<configuration>
<system.webServer>
<rewrite>
<rules>
<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>
<staticContent>
<remove fileExtension=".json" />
<mimeMap fileExtension=".json" mimeType="application/json" />
<remove fileExtension=".woff2" />
<mimeMap fileExtension=".woff2" mimeType="font/woff2" />
</staticContent>
</system.webServer>
</configuration>
Drop this web.config into the same folder as index.html. The IsFile/IsDirectory conditions are what make it safe — real files like main.js, styles.css, and image assets still get served directly, only unmatched paths fall through to index.html.
Common Gotchas
- Forgetting the URL Rewrite module itself is not installed on the IIS box — the web.config rule silently does nothing without it.
- Mismatched base href when the app is hosted in a sub-application under a site rather than at the root — it must exactly match the IIS virtual path.
- Browser caching an old index.html after a deploy — set explicit cache-control headers for index.html (no-cache) while letting hashed bundle filenames cache aggressively.
- Applying an Application Pool identity with no read access to the deployed folder, which shows up as a generic 500 rather than an obvious permissions error.
A React app hosted with Create React App or Vite usually gets away with relative paths because its router can be configured to not care about an absolute base. Angular is stricter — get base href wrong and you will see it in asset 404s, not just route 404s, so double check it before blaming the rewrite rule.
Wrapping Up
None of this is exotic, but it is easy to lose an afternoon to it if you have not hit it before. Get the production build right, match the base href to wherever the app actually lives, and let URL Rewrite hand unmatched requests back to Angular. After that it behaves like any other static site as far as IIS is concerned.