Start Debugging

Исправление: HTTP Error 500.30 - ASP.NET Core app failed to start после развёртывания на IIS

500.30 означает, что приложение выбросило исключение при запуске внутри w3wp.exe. Настоящее исключение уже находится в журнале приложений Windows под источником IIS AspNetCore Module V2. Сначала прочитайте его, а затем расставьте приоритеты: отсутствующий общий фреймворк, несоответствие разрядности пула приложений, отсутствующая конфигурация или права пула.

500.30 не является причиной, это сообщение IIS о том, что ASP.NET Core Module запустил CLR внутри w3wp.exe, а приложение выбросило исключение до того, как успело начать слушать. Настоящее исключение почти наверняка уже есть на сервере: откройте Просмотр событий, перейдите в Журналы Windows > Приложение и найдите последнюю запись с источником IIS AspNetCore Module V2. Когда stdoutLogEnabled равно false, модуль перехватывает ошибки запуска и записывает до 30 КБ из них в это событие, включая трассировку стека. Если запись даёт только exception code = '0xe0434352' и больше ничего, установите stdoutLogEnabled="true" в web.config и обратитесь к сайту снова. Всё остальное - это расстановка приоритетов среди четырёх причин, которые действительно это вызывают.

HTTP Error 500.30 - ASP.NET Core app failed to start

Более старые сборки ASP.NET Core Module отображают ровно тот же сбой как HTTP Error 500.30 - ANCM In-Process Start Failure, и именно эта строка до сих пор используется в таблицах ошибок документации Microsoft. Обе означают одно и то же. Всё изложенное ниже проверено на .NET 11 (Preview 6, SDK 11.0.100-preview.6.26359.118) с ANCM V2 из текущего .NET Hosting Bundle. Механизм не менялся с тех пор, как in-process размещение стало значением по умолчанию в ASP.NET Core 3.0, поэтому каждый шаг без изменений применим к развёртываниям net8.0, net9.0 и net10.0.

Почему 500.30 - это симптом, а не диагноз

Начиная с ASP.NET Core 3.0 приложения по умолчанию используют модель размещения in-process. Свойство MSBuild <AspNetCoreHostingModel> по умолчанию равно InProcess, и dotnet publish записывает hostingModel="inprocess" в web.config. В этой модели нет отдельного процесса dotnet.exe. aspnetcorev2.dll загружает обработчик запросов in-process в рабочий процесс IIS, запускает там CoreCLR, и ваш Program.cs выполняется внутри w3wp.exe с использованием IISHttpServer вместо Kestrel.

Это даёт один процесс вместо двух и заметный выигрыш в пропускной способности, но полностью разрушает отчётность об ошибках. Когда приложение выбрасывает исключение до того, как app.Run() достигнет состояния прослушивания, у модуля внутри собственного процесса остаётся мёртвая CLR и один байт информации для браузера: запуск не удался. Отсюда единый код состояния, покрывающий отсутствующую строку подключения, 32-разрядный двоичный файл в 64-разрядном рабочем процессе, неустановленную среду выполнения и DirectoryNotFoundException на связке ключей защиты данных.

Два следствия стоит усвоить до того, как что-то менять:

Минимальное воспроизведение

Наименьшее развёртывание, воспроизводящее ошибку, - это приложение, которое читает конфигурацию, существующую локально и отсутствующую на сервере:

// .NET 11 preview 6, C# 14. Program.cs
var builder = WebApplication.CreateBuilder(args);

string cs = builder.Configuration.GetConnectionString("Default")
    ?? throw new InvalidOperationException("Connection string 'Default' is missing.");

builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlServer(cs));

var app = builder.Build();
app.MapGet("/", () => "ok");
app.Run();

Локально это работает, потому что appsettings.Development.json содержит нужную секцию, а ASPNETCORE_ENVIRONMENT равно Development. На сервере окружение - Production, appsettings.Production.json никогда не попадал в результат публикации, и исключение возникает на строке 3. F5 работает, развёртывание отдаёт 500.30, и в приложении при этом ничего не сломано.

Эта форма покрывает значительную долю реальных сообщений о 500.30: сбой связан с окружением, а значит, по построению невидим на машине разработчика.

Чтение журнала приложений, которое обычно завершает расследование

Сделайте это до того, как трогать web.config. На сервере запустите Просмотр событий от имени администратора и откройте Журналы Windows > Приложение, либо выполните запрос напрямую:

# Windows Server 2022+, PowerShell 5.1 or 7.x. Run elevated on the web server.
Get-WinEvent -FilterHashtable @{
    LogName      = 'Application'
    ProviderName = 'IIS AspNetCore Module V2'
} -MaxEvents 5 | Format-List TimeCreated, Id, LevelDisplayName, Message

Вы ищете одну из трёх форм.

Форма 1, полезная. Полная управляемая трассировка стека. Модуль перехватил ваше необработанное исключение запуска и записал его в журнал событий, потому что stdoutLogEnabled равно false. Прочитайте тип исключения и верхний кадр, исправьте это, и вы закончили. Именно этот случай люди пропускают, потому что страница в браузере ничего им не сказала, и они решили, что сервер тоже не скажет.

Форма 2, непрозрачная:

Application '/LM/W3SVC/5/ROOT' with physical root 'C:\inetpub\wwwroot\myapp\'
hit unexpected managed exception, exception code = '0xe0434352'.
Please check the stderr logs for more information.
Application '/LM/W3SVC/5/ROOT' with physical root 'C:\inetpub\wwwroot\myapp\'
failed to load clr and managed application. CLR worker thread exited prematurely

0xe0434352 - это универсальный код Win32 для “управляемое исключение вышло наружу”, и не более того. Он не несёт ни типа, ни сообщения. Это документированная сигнатура x86-приложения в пуле, где не включены 32-разрядные приложения, но она также появляется всякий раз, когда исключение вышло там, где модуль не смог зафиксировать подробности. Переходите к журналу stdout.

Форма 3, вообще ничего. Ни одного события ANCM в течение минуты после вашего запроса. Обычно это значит, что модуль так и не добрался до запуска CLR, и на деле вы имеете дело с 500.0, 500.31 или 500.32, а не с исключением запуска. Смотрите раздел о вариантах в конце.

Включение журнала stdout

Правьте развёрнутый web.config на сервере, а не тот, что в проекте. Он перегенерируется при каждой публикации, что для временного диагностического переключателя как раз то, что нужно.

<?xml version="1.0" encoding="utf-8"?>
<!-- Deployed web.config, ASP.NET Core Module V2, .NET 11 -->
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath="dotnet"
                  arguments=".\MyApp.dll"
                  stdoutLogEnabled="true"
                  stdoutLogFile=".\logs\stdout"
                  hostingModel="inprocess" />
    </system.webServer>
  </location>
</configuration>

Сохранение web.config перезапускает пул приложений, так что просто обратитесь к сайту снова. Папку logs для stdoutLogFile модуль создаёт сам и записывает файл с меткой времени и идентификатором процесса в имени, например stdout_20260805184032_5412.log. Учётной записи пула приложений нужен доступ на запись в эту папку:

icacls "C:\inetpub\wwwroot\myapp\logs" /grant "IIS AppPool\MyAppPool":(OI)(CI)M

Три замечания по чтению, экономящие время:

Если stdout по-прежнему молчит, сбой находится ниже управляемого кода. Добавьте собственный отладочный журнал модуля:

<!-- ASP.NET Core Module V2 diagnostic logging. Remove after troubleshooting. -->
<aspNetCore processPath="dotnet"
            arguments=".\MyApp.dll"
            stdoutLogEnabled="false"
            stdoutLogFile=".\logs\stdout"
            hostingModel="inprocess">
  <handlerSettings>
    <handlerSetting name="debugFile" value=".\logs\aspnetcore-debug.log" />
    <handlerSetting name="debugLevel" value="FILE,TRACE" />
  </handlerSettings>
</aspNetCore>

В отличие от stdoutLogFile, для debugFile модуль папки не создаёт. Каталог logs должен уже существовать и быть доступен на запись учётной записи пула, иначе вы не получите ничего и сделаете неверный вывод. Этот журнал показывает разрешение hostfxr, какие версии фреймворка рассматривались и какая DLL не загрузилась.

Исправление 1: приложение выбросило исключение при запуске, и это большинство случаев

Если журнал событий или журнал stdout дал вам трассировку стека, это ваш случай. Группировка на практике:

  1. Конфигурация, присутствующая локально и отсутствующая на сервере. appsettings.Production.json не попал в результат публикации, значение из User Secrets, у которого никогда не было продуктового аналога, переменная окружения, заданная только на вашей машине. Это сбой из-за отсутствующей строки подключения в его развёрточной форме.
  2. Сбои графа внедрения зависимостей в builder.Build(). ASP.NET Core проверяет области и граф сервисов при построении в Development, и любая проблема вида Unable to resolve service for type или захваченной зависимости проявляется как 500.30 вместо полезной страницы. Смотрите unable to resolve service for type while attempting to activate и cannot consume scoped service from singleton.
  3. Внешние зависимости, к которым обращаются при запуске. Key Vault с политикой доступа, не покрывающей управляемое удостоверение пула приложений, - это случай, который Microsoft прямо называет для 500.30. Миграция, выполняемая при старте, провайдер конфигурации, обращающийся к базе данных, загрузка документа обнаружения OIDC на сервере без исходящего доступа: всё это превращает сетевую проблему в сбой запуска.
  4. Доступ к сертификатам и защите данных. Загрузка сертификата X.509 из хранилища компьютера или сохранение связки ключей защиты данных по пути, недоступному учётной записи пула на запись, выбрасывает исключение до первого запроса.

Структурное решение для всей этой категории - сделать сбои запуска явными и читаемыми, а не случайными. Проверка конфигурации при старте через IValidateOptions<T> и ValidateOnStart превращает “приложение отдаёт 500.30” в именованное OptionsValidationException, перечисляющее ровно те настройки, которых не хватает, а это разница между пятиминутным исправлением и потерянным днём.

Чтобы увидеть исходное исключение в браузере на тестовом сервере, добавьте переменную окружения в web.config, и никогда не делайте этого на публичном сервере:

<!-- Staging and test servers only. Do not ship this to an internet-facing host. -->
<aspNetCore processPath="dotnet" arguments=".\MyApp.dll" hostingModel="inprocess">
  <environmentVariables>
    <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
    <environmentVariable name="ASPNETCORE_DETAILEDERRORS" value="true" />
  </environmentVariables>
</aspNetCore>

Исправление 2: общий фреймворк, на который нацелено приложение, не установлен

Microsoft ставит эту причину первой среди причин 500.30: приложение нацелено на версию общего фреймворка ASP.NET Core, которой нет на машине. Проверьте, что на сервере есть на самом деле:

dotnet --list-runtimes

Вам нужна строка Microsoft.AspNetCore.App, чья мажорная версия совпадает с вашим TargetFramework, и нужна она в той же разрядности, что и пул приложений. Если приложение - net11.0, а на сервере максимум Microsoft.AspNetCore.App 10.0.x, это и есть ответ, потому что ASP.NET Core по умолчанию не выполняет roll forward между мажорными версиями.

Установите .NET Hosting Bundle, который ставит среду выполнения, общий фреймворк ASP.NET Core и ANCM одним пакетом. Два правила установки вызывают больше случаев 500.30, чем сама загрузка:

net stop was /y
net start w3svc

Полный iisreset тоже подойдёт. Пропуск этого шага и есть причина того, что “я установил среду выполнения, а оно всё равно падает” - настолько частое продолжение разговора.

Исправление 3: приложение и пул приложений расходятся в разрядности

Размещение in-process требует, чтобы архитектура приложения и установленной среды выполнения совпадала с архитектурой пула приложений. Слоя адаптации нет. 32-разрядный двоичный файл не может запустить CoreCLR внутри 64-разрядного w3wp.exe.

В диспетчере IIS выберите пул приложений, откройте Дополнительные параметры и задайте Разрешены 32-разрядные приложения:

Или из командной строки:

%windir%\system32\inetsrv\appcmd set apppool /apppool.name:MyAppPool /enable32BitAppOnWin64:false

Пока вы там, задайте Версию среды CLR .NET равной Без управляемого кода в основных параметрах. ASP.NET Core запускает CoreCLR самостоятельно и никогда не нуждается в настольной CLR внутри рабочего процесса. Это документировано как необязательное, но рекомендуемое, и убирает целый класс запутанных взаимодействий с устаревшими модулями.

Отдельная ловушка Hosting Bundle: если вы установили его с OPT_NO_X86=1, на этой машине вообще нет 32-разрядной среды выполнения, и x86-приложение упадёт независимо от настроек пула.

Исправление 4: учётная запись пула не может прочитать то, что ей нужно

Стандартная ApplicationPoolIdentity - это виртуальная учётная запись, и любой 500.30, вызванный правами доступа, выглядит точно так же, как любой другой 500.30. Если удостоверение было изменено с ApplicationPoolIdentity на доменную или служебную учётную запись, проверьте, что у неё есть доступ на чтение к папке развёртывания и на запись всюду, куда приложение пишет. Выдайте права на папку по имени пула:

icacls "C:\inetpub\wwwroot\myapp" /grant "IIS AppPool\MyAppPool":(OI)(CI)RX

Два случая стоит проверить напрямую: чтение закрытого ключа сертификата из хранилища компьютера требует ACL на контейнер ключей, а любой код, обращающийся к %USERPROFILE%, требует Загрузить профиль пользователя равным True у пула приложений. По умолчанию там True, и в защищённых окружениях его часто отключают.

Сократите область поиска вдвое, запустив приложение вне IIS

Прежде чем тратить ещё час на конфигурацию IIS, зайдите на сервер, откройте консоль в папке развёртывания и запустите приложение напрямую:

cd C:\inetpub\wwwroot\myapp
set ASPNETCORE_ENVIRONMENT=Production
dotnet MyApp.dll

Исключение печатается в консоль с полной трассировкой стека и без какой-либо настройки журналирования. Если оно возникает здесь, проблема в вашем приложении или его конфигурации, а IIS ни при чём, и это ведёт вас прямо к Исправлению 1. Если приложение стартует чисто и обслуживает http://localhost:5000, проблема в слое размещения: разрядность, права или модуль, и это ведёт к Исправлению 2, 3 или 4. Одна эта команда решает, какая половина статьи вам нужна.

Обратите внимание на переменную окружения. Запуск под вашей собственной учётной записью с вашим окружением - это не то же самое, что запуск под учётной записью пула, поэтому чистый прогон здесь не доказывает, что права на файлы верны. Он доказывает, что верны код и развёрнутые файлы конфигурации.

Соседние коды, которые не являются 500.30

Поисковый трафик по 500.30 собирает множество похожих случаев. Если на вашей странице написано что-то другое, это другая проблема с другим решением:

Переход на размещение out-of-process - это законный диагностический приём, а не решение. Установка hostingModel="outofprocess" в web.config перезапускает рабочий процесс и выполняет ваше приложение как дочерний dotnet.exe, где сбои запуска наблюдать намного проще, а requestTimeout и rapidFailsPerMinute снова применяются. Используйте это, чтобы получить читаемую ошибку, а затем вернитесь к in-process ради производительности.

Общая форма расследования 500.30 коротка, если идти по порядку: журнал событий, затем запуск из консоли, затем разрядность и среда выполнения. Долгим днём это становится только тогда, когда вы начинаете со страницы в браузере и пытаетесь угадать.

Похожие материалы

Источники

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад