Deploying ASP.NET Core to Plesk on Windows (IIS), step by step
Short answer: publish the app (framework-dependent if the host has the .NET 10 Hosting Bundle, self-contained win-x64 if not), give the site its own application pool with "No Managed Code", keep a web.config that runs the ASP.NET Core Module in-process, put secrets in environment variables in the server's web.config, and deploy by uploading app_offline.htm first, then the files, then running migrations, then removing app_offline.htm. Most failures after that are 500.30 (the app crashed at startup: read the stdout log) or 500.31 (the runtime is missing).
A real example: "it works on my machine, 500 on the host"
The app runs locally, the files are uploaded, and the site shows HTTP Error 500.30 – ASP.NET Core app failed to start. Nothing in the browser says why. The usual causes are dull: no connection string on the server, a database the app can't reach, a folder it can't write to, or a migration that never ran. Each one takes five minutes to fix once you can see the error, and much longer if you can't.
This guide is the order of operations that makes those errors visible and avoidable.
1. Publish
# Framework-dependent: small, needs the .NET 10 Hosting Bundle on the server
dotnet publish src/Site.Web -c Release -o artifacts/publish
# Self-contained fallback (HTTP 500.31): ships the runtime, about 100 MB
dotnet publish src/Site.Web -c Release -r win-x64 --self-contained -o artifacts/publish-sc
The SDK writes the right processPath into the published web.config: dotnet plus .\Site.Web.dll for framework-dependent, .\Site.Web.exe for self-contained.
2. The web.config
Keep a web.config in the project so the module settings are versioned:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet"
stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout"
hostingModel="inprocess" />
</system.webServer>
</location>
</configuration>
hostingModel="inprocess" runs the app inside the IIS worker process, which is faster than proxying to a separate Kestrel process.
3. Plesk settings
- Dedicated application pool, with .NET CLR version: No Managed Code (ASP.NET Core doesn't use the classic CLR), Integrated pipeline, and Enable 32-bit applications: False (required for a
win-x64build). - One ASP.NET Core app per pool. Two in-process apps in one pool (say, the main domain and a
beta.subdomain) give HTTP 500.35. - SSL: issue a Let's Encrypt certificate and enable the permanent 301 redirect from HTTP to HTTPS.
4. Secrets: environment variables in the server's web.config
Shared hosting rarely has a secrets store. The ASP.NET Core Module can inject environment variables, and ASP.NET Core reads __ as the : separator:
<aspNetCore processPath="dotnet" arguments=".\Site.Web.dll" hostingModel="inprocess">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production" />
<environmentVariable name="ConnectionStrings__Default"
value="Server=...;Database=...;User Id=...;Password=...;Encrypt=True;TrustServerCertificate=True" />
</environmentVariables>
</aspNetCore>
Edit this in Plesk's File Manager, on the server only, and exclude web.config from your deploy sync so a deploy never overwrites it. TrustServerCertificate=True is often needed because the host's SQL Server uses a self-signed certificate and Microsoft.Data.SqlClient encrypts by default.
5. Deploy without file locks: app_offline.htm
While the app runs, IIS holds its DLLs open. The module watches for a file named app_offline.htm: when it appears, the app shuts down and the file's contents are served instead.
# 1. Take the app offline (upload a small "updating" page)
# 2. Upload everything from artifacts/publish EXCEPT web.config
# 3. Run pending migrations (next section)
# 4. Delete app_offline.htm; the next request starts the new version
Web Deploy or FTP both work. Script it, and run it from a machine that can reach the host.
6. Migrations as their own step
Don't call Database.Migrate() at startup in production; a failed migration then takes the site down. Run migrations explicitly:
# A: migration bundle from your PC (needs remote SQL access to the host)
dotnet ef migrations bundle --project src/Site.Infrastructure --startup-project src/Site.Web -r win-x64 --self-contained -o efbundle.exe --force
./efbundle.exe --connection "<production connection string>"
# B: idempotent SQL script, run in the host's database web tool
dotnet ef migrations script --idempotent --project src/Site.Infrastructure --startup-project src/Site.Web -o migrate.sql
7. Permissions and logs
Give the app pool's user Modify access only on the folders the app writes: logs and uploads (keep uploads outside wwwroot). Nothing else should be writable.
Troubleshooting table
| Error | Meaning | Fix |
|---|---|---|
| 500.30 | The app threw during startup | Set stdoutLogEnabled="true", make sure logs\ exists and is writable, reproduce, read logs\stdout_*.log, then turn it off again. Usually a missing connection string, an unreachable database, a pending migration or a missing write permission. |
| 500.31 | Runtime not found | Install the matching Hosting Bundle, or deploy self-contained. |
| 500.32 | Bitness mismatch | Disable 32-bit applications in the app pool. |
| 500.35 | Two in-process apps in one pool | Give each site its own pool. |
| 500.19 | Invalid config, or the module isn't installed | Validate the XML; confirm the Hosting Bundle with the host. |
Pros and cons of shared Windows hosting
Pros
- Cheap, with SQL Server, mail, DNS and SSL in one panel.
- IIS in-process hosting performs well for a typical site.
- A familiar model: files, an app pool, a database.
Cons
- Limited control: you can't install runtimes or change server settings yourself.
- No secrets store, so the server's
web.configbecomes the place for configuration. - Deploys are file copies; you have to build atomicity (
app_offline.htm, migrations first) yourself.
Stuck on a 500.30 or planning a move to Plesk/IIS? See how I can help.
Frequently asked questions
What causes HTTP Error 500.31 on Plesk?
The server doesn't have the ASP.NET Core runtime your app targets. Ask the host to install the matching Hosting Bundle, or publish self-contained for win-x64 so the runtime ships with the app.
Where do secrets go on shared hosting without a key vault?
In environment variables that the ASP.NET Core Module injects from the server's own web.config (the environmentVariables element). Keep that file on the server only and exclude it from your deploy sync, so the secrets never enter the repository.
Should the app run database migrations at startup?
Not in production. Run them as a separate deploy step: an EF Core migration bundle against the server, or an idempotent SQL script run in the host's database tool. A failed migration then stops the deploy instead of crashing the site.