Most broken IIS deploys are not bad builds. They are mid-copy collisions: Web Deploy or a pipeline overwrites DLLs while the app pool still serves traffic, workers hold file locks, and users hit yellow screens or 500.30s for a few ugly minutes. On Windows shared and VPS hosts you rarely get true blue-green slots, so you need a deliberate cutover signal. That signal is still app_offline.htm.
For ASP.NET Core on IIS (in-process or out-of-process with the ASP.NET Core Module), a root app_offline.htm tells the module to shut the app down and serve that static page until the file is removed. Pair it with a short smoke check and you get a zero-downtime-ish deploy: brief maintenance page, clean file replace, controlled warm-up—not a rolling failure.
#What the file actually does
Place app_offline.htm in the site root (the same folder as web.config). The ASP.NET Core Module detects it, stops processing new requests for that app, and returns the HTML contents with a 503-style offline response behavior. Existing requests drain according to shutdown timeouts; new ones never touch your half-written bin folder. When the file is deleted, the next request starts the app again.
Keep the page self-contained: inline CSS, no links to your own static files or APIs, and a clear retry message. You are offline; do not depend on the app you just took down.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Maintenance</title>
<style>
body { font-family: system-ui, sans-serif; margin: 3rem; max-width: 36rem; }
</style>
</head>
<body>
<h1>Brief maintenance</h1>
<p>We are deploying an update. Try again in a minute.</p>
</body>
</html>
#Drop it from CI, then publish
Whether you use Web Deploy, a mapped drive, or a hardened copy step on a Windows VPS, the order matters more than the tool: (1) write app_offline.htm, (2) wait a few seconds for drain, (3) publish or sync files, (4) run a smoke request against a warm URL, (5) delete app_offline.htm. Do not delete the offline file until the new bits are complete and web.config is valid.
A minimal PowerShell pattern many pipelines use against a Windows host (WinRM, self-hosted runner, or post-sync script) looks like this:
$root = "C:\inetpub\wwwroot\myapp"
$offline = Join-Path $root "app_offline.htm"
Copy-Item ".\app_offline.htm" $offline -Force
Start-Sleep -Seconds 5
# Your publish/sync step here (msdeploy, robocopy, etc.)
# msdeploy -verb:sync -source:package=app.zip -dest:auto,...
# Optional: hit a cheap endpoint while still offline is useless;
# remove offline first, then smoke the live site.
Remove-Item $offline -Force
$r = Invoke-WebRequest -Uri "https://www.example.com/healthz" -UseBasicParsing
if ($r.StatusCode -ne 200) { throw "Smoke failed: $($r.StatusCode)" }
If you deploy with Web Deploy only, ship app_offline.htm as the first content file in the package or run a pre-sync provider that creates it, then a post-sync step that deletes it after verification. Avoid leaving the file in source control as a permanent root artifact unless you intentionally gate production with a release toggle.
#Shared hosting vs VPS realities
- Shared IIS: you usually cannot recycle arbitrary pools or rearrange bindings. app_offline.htm is often the only supported drain switch inside your site root.
- Windows VPS: you can add a second site or hostname for staging, publish there first, smoke it, then either swap physical paths or repeat the offline-publish-online sequence on production.
- App pool identity must be allowed to delete app_offline.htm after deploy; if a pipeline runs as a different user, grant modify on that one file path or delete via the same credential that created it.
- In-process hosting still respects app_offline through ANCM. Out-of-process does too—verify once on your .NET 10 stack so you are not surprised by an old module path.
#Pitfalls that keep deploys noisy
Long-running requests (big uploads, report exports, SignalR) may outlive a five-second sleep. Raise the wait to match your shutdown timeout, or drain via your reverse proxy first on a VPS. Do not put app_offline.htm only in a virtual directory if the app root is the parent site—ANCM watches the app’s content root. Skip locking web.config mid-copy; publish a complete folder or package. Finally, treat smoke tests as mandatory after removal: hit a cheap authenticated or health endpoint, not just the marketing homepage, so a missing connection string fails the job instead of your users.
Practical takeaway: bake a three-step cutover into every Windows IIS publish—write app_offline.htm, sync the new .NET 10 build, delete app_offline.htm only after a scripted smoke check. It is older than containers and still the most reliable zero-downtime-ish lever on shared and VPS IIS hosts when you cannot afford half-deployed assemblies.
Comments
No comments yet