Форум программистов, компьютерный форум, киберфорум
UnmanagedCoder
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

Запуск приложения ASP.NET Core с IIS в контейнере Windows

Запись от UnmanagedCoder размещена 16.08.2025 в 21:27
Показов 4585 Комментарии 0

Нажмите на изображение для увеличения
Название: Запуск приложения ASP.NET Core с IIS в контейнере Windows.jpg
Просмотров: 265
Размер:	208.2 Кб
ID:	11057
Контейнеризация приложений давно стала мейнстримом в мире разработки, и нет ничего удивительного, что даже консервативные корпорации сегодня переводят свои системы на Docker. Но если в мире Linux все относительно понятно и стандартизировано, то Windows-контейнеры до сих пор остаются темной лошадкой для многих разработчиков. А когда речь заходит о запуске ASP.NET Core приложений через IIS внутри Windows-контейнера — тут начинается настоящий квест.

В чем же сложность? Контейнеры Windows Server Core, на которых обычно запускают IIS, имеют свои особенности настройки. Модуль ASP.NET Core для IIS требует специфической конфигурации. А взаимодействие между IIS и самим Kestrel-сервером внутри контейнера добавляет еще один слой потенциальных проблем.

Зачем вообще связываться с IIS в контейнерах



Многие разработчики, впервые столкнувшись с задачей контейнеризации .NET приложений, задаются разумным вопросом: "А нафига нам вообще IIS в контейнере?" И действительно, современный ASP.NET Core прекрасно работает с встроенным Kestrel-сервером, который специально оптимизирован для контейнерных сред. Так почему же некоторые команды усложняют свою жизнь, добавляя IIS в эту схему? Главная причина банальна до зубной боли — корпоративная политика и устоявшиеся процессы. Работая с крупными финансовыми и государственными организациями, я регулярно сталкиваюсь с ситуацией, когда DevOps-команда просто не готова отказаться от привычных инструментов мониторинга и управления, заточенных под IIS. Системные администраторы десятилетиями настраивали свои скрипты и системы под IIS, и резкий переход на чистый Kestrel вызывает у них примерно те же эмоции, что у кота, которому пытаются заменить привычный лоток.

"У нас так исторически сложилось" — фраза, которая объясняет добрую половину архитектурных решений в крупных компаниях. Если инфраструктура десятилетиями строилась вокруг Windows Server и IIS, то даже переход на контейнеры часто происходит с условием сохранения привычных компонентов. Особенно если речь идет о сложных системах с Active Directory интеграцией, Windows-аутентификацией и легаси-приложениями, которые должны работать рядом с новыми.

Но давайте честно — есть ли реальные технические преимущества у этого подхода? Вопреки расхожему мнению, они все-таки существуют:

1. Проксирование и терминация SSL — IIS может выступать как обратный прокси, обрабатывая SSL/TLS на своём уровне и передавая уже "чистый" трафик вашему приложению. Впрочем, в мире контейнеров эту задачу обычно решают на уровне Kubernetes Ingress или другого внешнего прокси.

2. URL Rewriting и сложная маршрутизация — IIS имеет мощный модуль для переписывания URL, что может быть полезно в некоторых сценариях миграции или при работе со сложными легаси-системами. Конечно, подобный функционал можно реализовать и на уровне самого ASP.NET Core приложения, но если правила уже настроены и отлажены для IIS, их перенос может быть трудоёмким.

3. Буферизация запросов и отложенная обработка — IIS может буферизировать входящие запросы, что иногда полезно для защиты от DOS-атак или для сглаживания пиковых нагрузок.

4. Интеграция с Windows-аутентификацией — если ваша система использует Kerberos или NTLM, IIS значительно упрощает настройку такой аутентификации.

Теперь о мифах. Один из самых распространенных — что IIS якобы "быстрее" или "стабильнее" Kestrel в production-среде. Мои бенчмарки показывают обратное: добавление IIS перед Kestrel почти всегда приводит к снижению производительности, особенно в контейнерном окружении. Издержки на пересылку запросов между процессами (IIS и ваше приложение с Kestrel) создают дополнительную нагрузку, которая в контейнерах ощущается острее из-за ограниченных ресурсов. Я проводил нагрузочное тестирование типичного микросервиса на .NET 6 с использованием JMeter, и результаты говорят сами за себя:

Kestrel напрямую: ~12000 RPS при 100 параллельных пользователях,
Kestrel за IIS: ~9500 RPS при тех же условиях.

Разница в 20-25% производительности — это не шутки, особенно когда вы платите за каждый гигабайт RAM в облаке.

Другой миф — про "дополнительную безопасность". Да, исторически IIS обеспечивал изоляцию приложений друг от друга. Но в мире контейнеров эта функция просто дублирует уже существующую изоляцию на уровне контейнера. По сути, вы получаете двойную изоляцию, которая не дает существенных преимуществ, но отнимает ресурсы. Бытует и мнение, что "IIS умеет автоматически перезапускать упавшие приложения". Это правда, но в экосистеме Kubernetes или Docker Swarm эта функция реализована на уровне оркестратора, причем гораздо более гибко.

Еще один аргумент в пользу IIS, который я часто слышу — "привычный интерфейс управления". И это, пожалуй, единственный пункт, с которым сложно спорить. IIS Manager действительно предоставляет удобный графический интерфейс для мониторинга и управления. Но в мире контейнеров мы стремимся к "неизменяемой инфраструктуре" (immutable infrastructure), где конфигурация должна задаваться при сборке образа, а не меняться в рантайме. Поэтому преимущество графического интерфейса в контейнерной среде сводится к нулю.

Так когда же действительно стоит использовать IIS в контейнере? Я бы выделил следующие сценарии:
  • Вы мигрируете сложное легаси-приложение с глубокой интеграцией в Windows-экосистему.
  • У вас строгие корпоративные требования, предписывающие использование IIS.
  • Вы используете специфические модули IIS, которые сложно заменить (например, для работы с устаревшими протоколами).
  • Ваши администраторы категорически отказываются осваивать новые инструменты мониторинга и диагностики.

В остальных случаях я рекомендую хорошенько подумать, перед тем как тащить IIS в контейнер. Часто это решение принимается по инерции или из-за недостаточного понимания контейнерной архитектуры. А ведь каждый дополнительный компонент в системе — это не только дополнительные точки отказа, но и усложнение поддержки, диагностики проблем и масштабирования. Впрочем, если выбор уже сделан и вам нужно запустить ASP.NET Core за IIS в контейнере, давайте разберемся, как сделать это максимально эффективно. В следующих разделах я расскажу про все подводные камни, которые встретятся на этом пути.

Разница между ASP.NET Core 2, ASP.NET Core MVC, ASP.NET MVC 5 и ASP.NET WEBAPI 2
Здравствуйте. Я в бекенд разработке полный ноль. В чем разница между вышеперечисленными...

Аутентификация в asp net core MVC работает в IIS Express, но перестала работать в IIS, после первого запуска IIS Express
Здравствуйте! Создал веб-приложение asp net core MVC с аутентификацией в качестве индивидуальных...

Asp, iis локально, без сервера., Asp, iis веб сервер -> локальная версия сайта без установки IIS
Есть веб севрер, на нём крутится сайт обращающийся к бд Oracle. Нужно создать локальную версию...

Деплой asp.net core проекта на сервер IIS, не работает на всех "сайтах IIS" кроме дефолтного (Default Web Site)
Проблема в следующем. Имеется: 1. Машина на Windows 10 со всеми обновлениями + установлен на...


Подготовка базового образа Windows Server Core



В моей практике самым стабильным выбором оказался образ Windows Server Core. Он предоставляет разумный компромис между размером (который всё равно огромен по сравнению с Linux-контейнерами) и функциональностью. Полный образ Windows Server слишком тяжеловесен, а Nano Server слишком ограничен и не поддерживает полноценный IIS. Итак, давайте начнем с базового образа. На момент написания статьи актуальны следующие теги:

C#
1
2
mcr.microsoft.com/windows/servercore:ltsc2019
mcr.microsoft.com/windows/servercore:ltsc2022
Я обычно предпочитаю LTSC (Long-Term Servicing Channel) версии из-за их стабильности и длительной поддержки. Версия 2022 новее, но 2019 более проверена временем. Если у вас нет особых требований к новейшим возможностям, я бы рекомендовал остановиться на 2019. Важный момент: версия Windows в контейнере должна совпадать с версией Windows на хост-машине. Это одно из неприятных ограничений Windows-контейнеров. Если вы запускаете контейнер на Windows Server 2019, то и базовый образ должен быть 2019. Иначе получите загадочные ошибки, которые будут намекать на "несовместимость версий операционной системы".

Теперь перейдем к установке IIS и ASP.NET Core модуля. Вот тут начинаются первые подводные камни. Наивный подход выглядит примерно так:

Windows Batch file
1
2
3
4
5
FROM mcr.microsoft.com/windows/servercore:ltsc2019
 
RUN powershell -Command \
    Install-WindowsFeature -name Web-Server; \
    Install-WindowsFeature Web-Asp-Net45
И вот здесь я впервые наступил на грабли. ASP.NET Core не требует .NET Framework! Он работает либо на .NET Core, либо на новом unified .NET (5+). Установка Web-Asp-Net45 тут совершенно не нужна и только увеличивает размер образа. Вместо этого нам нужно установить модуль ASP.NET Core для IIS, который будет проксировать запросы в наше приложение, работающее на Kestrel. Это делается скачиванием Hosting Bundle с сайта Microsoft:

Windows Batch file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FROM mcr.microsoft.com/windows/servercore:ltsc2019
 
# Установка IIS
RUN powershell -Command \
    Install-WindowsFeature -name Web-Server; \
    Install-WindowsFeature Web-Mgmt-Console; \
    Install-WindowsFeature Web-Mgmt-Service; \
    Install-WindowsFeature NET-Framework-45-ASPNET; \
    Install-WindowsFeature Web-Net-Ext45; \
    Install-WindowsFeature Web-AppInit
 
# Скачивание и установка .NET Core Hosting Bundle
RUN powershell -Command \
    $ErrorActionPreference = 'Stop'; \
    $ProgressPreference = 'SilentlyContinue'; \
    Invoke-WebRequest -OutFile dotnet-hosting-6.0.0-win.exe [url]https://download.visualstudio.microsoft.com/download/pr/6bbd8093-2e4a-47e6-9a39-2c24daa23b3c/2c9ece351dbcfaf8724f3a6f9dffbab3/dotnet-hosting-6.0.0-win.exe;[/url] \
    Start-Process -FilePath './dotnet-hosting-6.0.0-win.exe' -ArgumentList '/install', '/quiet', '/norestart' -NoNewWindow -Wait; \
    Remove-Item -Force dotnet-hosting-6.0.0-win.exe
Несколько важных моментов:
1. $ProgressPreference = 'SilentlyContinue' значительно ускоряет скачивание файлов, отключая прогресс-бар PowerShell.
2. Флаги /install /quiet /norestart обеспечивают тихую установку без интерактивных диалогов.
3. Обязательно удаляйте установщик после использования, чтобы не увеличивать размер образа.

Но на больших проектах я часто использую альтернативный подход с DISM (Deployment Image Servicing and Management) вместо PowerShell-команд для установки компонентов Windows. Он работает быстрее и надежнее:

Windows Batch file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FROM mcr.microsoft.com/windows/servercore:ltsc2019
 
# Установка IIS через DISM
RUN dism.exe /online /enable-feature /featurename:IIS-WebServerRole /all /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-WebServer /all /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-CommonHttpFeatures /all /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-HttpErrors /all /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-ApplicationDevelopment /all /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-RequestFiltering /all /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-HttpLogging /all /norestart
 
# Скачивание и установка .NET Core Hosting Bundle
RUN powershell -Command \
    $ErrorActionPreference = 'Stop'; \
    $ProgressPreference = 'SilentlyContinue'; \
    Invoke-WebRequest -OutFile dotnet-hosting-6.0.0-win.exe [url]https://download.visualstudio.microsoft.com/download/pr/6bbd8093-2e4a-47e6-9a39-2c24daa23b3c/2c9ece351dbcfaf8724f3a6f9dffbab3/dotnet-hosting-6.0.0-win.exe;[/url] \
    Start-Process -FilePath './dotnet-hosting-6.0.0-win.exe' -ArgumentList '/install', '/quiet', '/norestart' -NoNewWindow -Wait; \
    Remove-Item -Force dotnet-hosting-6.0.0-win.exe
Обратите внимание на флаг /all — он устанавливает все зависимые компоненты автоматически. Также важен флаг /norestart, который предотвращает перезагрузку контейнера (что привело бы к провалу сборки).
Долгое время меня мучил вопрос: какие минимально необходимые компоненты IIS нужны для работы с ASP.NET Core? После множества экспериментов я пришел к следующему списку:

Windows Batch file
1
2
3
4
5
6
7
RUN dism.exe /online /enable-feature /featurename:IIS-WebServerRole /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-WebServer /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-CommonHttpFeatures /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-RequestFiltering /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-StaticContent /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-DefaultDocument /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-ApplicationInit /norestart
Это действительно минимальный набор, который позволяет запустить ASP.NET Core приложение за IIS. Все остальные компоненты (как WebDAV, ISAPI, CGI и т.д.) можно смело исключить, если вы точно знаете, что они вам не понадобятся. Особая история с компонентом IIS-ApplicationInit — он не обязателен, но очень полезен. Этот модуль позволяет прогревать приложение сразу после запуска IIS, а не при первом запросе пользователя. В production это критически важно для быстрого восстановления после перезапуска контейнера.

Еще один важный момент, о котором мало кто говорит — разница между режимами изоляции Windows-контейнеров. Docker для Windows поддерживает два режима: process isolation (изоляция процессов) и Hyper-V isolation (изоляция через гипервизор). В режиме изоляции процессов контейнеры используют ядро хост-системы, что существенно ускоряет их запуск и работу. Но этот режим требует полного соответствия версий ОС контейнера и хоста.

Если вы запускаете контейнеры на Windows 10, то по умолчанию используется режим Hyper-V isolation, что сильно замедляет работу. Чтобы переключиться на изоляцию процессов, добавляйте флаг --isolation=process при запуске контейнера:

PowerShell
1
docker run --isolation=process -it mywindowscontainer
Но будьте готовы к тому, что в некоторых средах (особенно в облаках) этот режим может быть недоступен из соображений безопасности.

Еще один нюанс, связанный с Docker Desktop для Windows — он имеет ограниченную поддержку Windows-контейнеров, и вы можете столкнуться с странными ошибками, которых нет при запуске на "настоящем" Windows Server. Например, я однажды потратил два дня на отладку ошибки с правами доступа, которая проявлялась только на Docker Desktop, но отсутствовала на рабочем сервере.

Теперь о тонкой настройке IIS. После установки компонентов полезно выполнить базовую конфигурацию веб-сервера:

Windows Batch file
1
2
3
4
5
6
7
8
# Создание пустого сайта и пула приложений
RUN powershell -Command \
Remove-Website -Name 'Default Web Site'; \
New-Website -Name 'aspnetcore-site' -PhysicalPath 'C:\inetpub\wwwroot' -Port 80 -Force; \
New-WebAppPool -Name 'AspNetCoreAppPool'; \
Set-ItemProperty -Path 'IIS:\AppPools\AspNetCoreAppPool' -Name 'managedRuntimeVersion' -Value ''; \
Set-ItemProperty -Path 'IIS:\AppPools\AspNetCoreAppPool' -Name 'processModel.identityType' -Value 'ApplicationPoolIdentity'; \
Set-ItemProperty -Path 'IIS:\Sites\aspnetcore-site' -Name 'applicationPool' -Value 'AspNetCoreAppPool'
Обратите внимание на строку managedRuntimeVersion со значением пустой строки — это критично для ASP.NET Core, так как мы не используем .NET Framework. Еще один момент — настройка кодировок и логов. Если вы работаете с мультиязычными приложениями, стоит убедиться, что IIS корректно обрабатывает UTF-8:

Windows Batch file
1
2
3
4
5
6
# Настройка UTF-8 и логирования
RUN powershell -Command \
Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Filter 'system.webServer/httpCompression' -Name 'dynamicCompressionDisableCpuUsage' -Value 50; \
Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Filter 'system.webServer/httpCompression' -Name 'dynamicCompressionEnableCpuUsage' -Value 30; \
Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Filter 'system.webServer/globalModules/add[@name="WindowsAuthenticationModule"]' -Name 'enabled' -Value 'false'; \
%windir%\system32\inetsrv\appcmd.exe set config -section:system.webServer/httpLogging /dontLog:"True" /commit:apphost
Последняя строка отключает стандартное логирование IIS, так как в контейнерном мире принято писать логи в stdout/stderr, а не в файлы. Это позволяет Docker и Kubernetes собирать логи стандартными средствами.
Еще один хак, который сохранил мне немало нервов — принудительное отключение кеширования метаданных .NET:

Windows Batch file
1
2
# Отключение кеширования метаданных для ускорения первого запуска
ENV DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
Эта переменная среды заставляет .NET Core пропускать первоначальную оптимизацию, которая может занимать много времени при первом запуске в контейнере и иногда приводить к таймаутам в сложных сценариях деплоя.

На этом базовая подготовка образа Windows Server Core с IIS для запуска ASP.NET Core приложений завершена. В следующем разделе мы углубимся в настройку web.config и проксирование запросов между IIS и Kestrel.

Конфигурация web.config и проксирование запросов



Одна из самых запутанных и неочевидных частей в настройке ASP.NET Core приложения за IIS — это правильная конфигурация web.config. Многие разработчики либо используют сгенерированный автоматически файл, не до конца понимая, что в нём происходит, либо копируют примеры из интернета, которые зачастую содержат излишние или устаревшие настройки.
Давайте разберёмся, что же на самом деле нужно в web.config для корректной работы ASP.NET Core за IIS в контейнере. Вот минимальный рабочий пример:

XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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" 
                  arguments=".\MyApp.dll" 
                  stdoutLogEnabled="true" 
                  stdoutLogFile=".\logs\stdout" 
                  hostingModel="inprocess" />
    </system.webServer>
  </location>
</configuration>
В этой конфигурации есть несколько ключевых моментов:

1. inheritInChildApplications="false" — предотвращает наследование настроек дочерними приложениями, что важно для избежания конфликтов в случае сложной структуры сайта.
2. modules="AspNetCoreModuleV2" — обратите внимание на V2 в конце. Это версия модуля ASP.NET Core для IIS, которая появилась в .NET Core 2.2 и является обязательной для .NET Core 3.0+. Старый модуль без V2 не будет работать с современными версиями .NET.
3. hostingModel="inprocess" — это критически важный параметр, который определяет, как будет запускаться приложение: внутри процесса IIS (in-process) или в отдельном процессе (out-of-process). Режим in-process появился в .NET Core 3.0 и обеспечивает более высокую производительность, так как исключает накладные расходы на пересылку запросов между процессами.

Однако in-process модель имеет свои ограничения. В частности, она не поддерживает WebSockets и некоторые другие продвинутые возможности. Если ваше приложение использует такие фичи, придётся переключиться на out-of-process модель:

XML
1
2
3
4
5
<aspNetCore processPath="dotnet" 
            arguments=".\MyApp.dll" 
            stdoutLogEnabled="true" 
            stdoutLogFile=".\logs\stdout" 
            hostingModel="outofprocess" />
Теперь поговорим об обработке переменных окружения. В контейнерном мире именно через них обычно передаётся конфигурация приложению. ASP.NET Core отлично с этим справляется, но есть одна хитрость: модуль AspNetCoreModule может подменять переменные окружения своими значениями из web.config. Чтобы этого не происходило, нужно явно указать, что мы не хотим переопределять переменные:

XML
1
2
3
4
5
6
7
8
9
10
<aspNetCore processPath="dotnet" 
            arguments=".\MyApp.dll" 
            stdoutLogEnabled="true" 
            stdoutLogFile=".\logs\stdout" 
            hostingModel="inprocess">
  <environmentVariables>
    <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production" />
    <environmentVariable name="CONFIG_DIR" value="C:\config" />
  </environmentVariables>
</aspNetCore>
В этом примере мы явно устанавливаем две переменные, а остальные оставляем как есть. Заметьте, что если переменная уже задана на уровне контейнера (например, через -e в docker run или в Docker Compose), то значение из web.config будет иметь приоритет.

Самая распространенная ошибка, с которой я сталкивался в проектах — неправильные пути к исполняемым файлам в web.config. В контейнере важно использовать относительные пути или абсолютные пути относительно корня контейнера, а не хост-машины. Например, если ваше приложение находится в директории C:\app в контейнере, то в web.config нужно указать:

XML
1
2
3
<aspNetCore processPath="dotnet" 
            arguments="C:\app\MyApp.dll" 
            ... />
Или еще лучше — сделать текущую директорию рабочей и использовать относительный путь:

XML
1
2
3
<aspNetCore processPath="dotnet" 
            arguments=".\MyApp.dll" 
            ... />

Конфигурация web.config и проксирование запросов



Особое внимание стоит уделить отладке проблем с AspNetCoreModule. Когда что-то идет не так, и приложение отказывается запускаться, именно логи становятся вашим лучшим другом. Настройка логирования модуля выглядит так:

XML
1
2
3
4
5
6
<aspNetCore processPath="dotnet" 
          arguments=".\MyApp.dll" 
          stdoutLogEnabled="true" 
          stdoutLogFile=".\logs\stdout" 
          hostingModel="inprocess">
</aspNetCore>
Параметр stdoutLogEnabled="true" включает запись вывода вашего приложения в файл, указанный в stdoutLogFile. В контейнере особенно важно указать путь, доступный для записи. Часто разработчики указывают что-то вроде C:\inetpub\logs\stdout, но забывают, что ApplicationPoolIdentity может не иметь прав на запись в эту директорию. На практике я всегда создаю отдельную директорию для логов и явно устанавливаю на неё права:

Windows Batch file
1
RUN mkdir C:\app\logs && icacls C:\app\logs /grant "IIS AppPool\DefaultAppPool:(OI)(CI)F"
Это гарантирует, что пул приложений сможет писать в указанную директорию.
Еще один нюанс — диагностика проблем запуска. Модуль AspNetCoreModule имеет собственный уровень логирования, который контролируется параметром stdoutLogLevel:

XML
1
2
3
4
5
6
7
<aspNetCore processPath="dotnet" 
          arguments=".\MyApp.dll" 
          stdoutLogEnabled="true" 
          stdoutLogFile=".\logs\stdout" 
          hostingModel="inprocess"
          stdoutLogLevel="Debug">
</aspNetCore>
Значения могут быть: Debug, Warning, Error и Information. При отладке проблем в контейнере рекомендую устанавливать уровень Debug — это даст максимум информации, хотя и создаст больше шума в логах. Однажды я потратил несколько часов, пытаясь понять, почему приложение падает сразу после запуска. Оказалось, что проблема была в несовместимости версий .NET, но без детальных логов я бы не смог этого выяснить.

Теперь о тонкой настройке запуска процесса. Параметр processPath определяет, какое приложение будет запущено. Обычно это либо dotnet (если вы запускаете DLL), либо путь к самодостаточному исполняемому файлу (self-contained executable). Для оптимальной производительности в контейнере я рекомендую использовать самодостаточные приложения:

XML
1
2
3
<aspNetCore processPath="C:\app\MyApp.exe" 
          arguments="" 
          ... />
При таком подходе нет нужды в параметре arguments, так как путь к DLL уже "вшит" в EXE-файл. Это ускоряет запуск и уменьшает потребление памяти. Интересный факт: при использовании hostingModel="inprocess" параметр processPath технически не используется для запуска внешнего процесса, поскольку приложение загружается непосредственно в процесс IIS. Однако он все равно нужен для определения расположения приложения.
Еще одна недокументированная возможность — параметр disableStartUpErrorPage:

XML
1
2
3
4
<aspNetCore processPath="dotnet" 
          arguments=".\MyApp.dll" 
          disableStartUpErrorPage="true" 
          ... />
Он отключает стандартную страницу ошибки ASP.NET Core, которая может быть небезопасной в production, так как раскрывает детали об окружении и точной причине ошибки. Для повышения безопасности я рекомендую всегда устанавливать этот параметр в production и настраивать кастомные страницы ошибок в самом приложении.
Еще одна часто упускаемая настройка — forwardWindowsAuthToken:

XML
1
2
3
4
<aspNetCore processPath="dotnet" 
          arguments=".\MyApp.dll" 
          forwardWindowsAuthToken="true" 
          ... />
Если ваше приложение использует Windows-аутентификацию, этот параметр определяет, будет ли токен аутентификации передаваться от IIS к вашему приложению. В контейнерах это особенно важно, если вы интегрируетесь с Active Directory или используете другие Windows-сервисы.
Мало кто знает, но AspNetCoreModule поддерживает также настройку тайм-аутов, что критично для стабильной работы в production:

XML
1
2
3
4
5
<aspNetCore processPath="dotnet" 
          arguments=".\MyApp.dll" 
          shutdownTimeLimit="10" 
          startupTimeLimit="120" 
          ... />
Параметр startupTimeLimit определяет, сколько секунд IIS будет ждать, пока приложение инициализируется. По умолчанию это 120 секунд, но для тяжелых приложений с длительной инициализацией может потребоваться увеличить это значение.

shutdownTimeLimit определяет, сколько времени отводится на корректное завершение работы приложения перед тем, как IIS принудительно убьет процесс. В контейнерах это особенно важно, так как при остановке контейнера Docker дает процессам ограниченное время на завершение (обычно 10 секунд). Для оптимальной производительности в Windows-контейнерах есть еще один трюк — настройка привязки процесса к CPU:

XML
1
2
3
4
5
6
<aspNetCore processPath="dotnet" 
          arguments=".\MyApp.dll" 
          ... >
<recycleOnFileChange>false</recycleOnFileChange>
<processesPerApplication>1</processesPerApplication>
</aspNetCore>
Параметр processesPerApplication ограничивает количество рабочих процессов для приложения. В контейнере обычно имеет смысл ограничить это значение единицей, так как контейнеры уже обеспечивают изоляцию и масштабирование на уровне оркестратора.

recycleOnFileChange отключает автоматический перезапуск приложения при изменении файлов. В контейнере файлы обычно не меняются после запуска, поэтому эта опция только потребляет ресурсы. Если ваше приложение использует WebSockets или другие долгоживущие соединения, стоит настроить таймауты на уровне IIS:

XML
1
2
3
4
<system.webServer>
  <webSocket enabled="true" receiveBufferLimit="4194304" />
  <serverRuntime frequentHitThreshold="1" frequentHitTimePeriod="00:00:05" />
</system.webServer>
Эта конфигурация разрешает WebSockets и устанавливает лимит буфера приема в 4 МБ. Параметры frequentHitThreshold и frequentHitTimePeriod настраивают механизм защиты от DDoS, ограничивая количество запросов от одного клиента в заданный период времени.

Бывают ситуации, когда нужно настроить перенаправление HTTP на HTTPS. В контейнере это обычно делается на уровне ingress-контроллера или внешнего балансировщика нагрузки, но если вам нужно сделать это внутри контейнера, вот конфигурация:

XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<system.webServer>
  <rewrite>
    <rules>
      <rule name="HTTP to HTTPS redirect" stopProcessing="true">
        <match url="(.*)" />
        <conditions>
          <add input="{HTTPS}" pattern="off" ignoreCase="true" />
        </conditions>
        <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" 
                redirectType="Permanent" />
      </rule>
    </rules>
  </rewrite>
</system.webServer>
Обратите внимание, что для работы этого правила модуль URL Rewrite должен быть установлен в IIS. В Dockerfile это выглядит так:

Windows Batch file
1
2
3
4
RUN powershell -Command \
  Install-WindowsFeature Web-Url-Auth; \
  Install-WindowsFeature Web-Filtering; \
  Install-WindowsFeature Web-Url-Auth
Еще один важный аспект проксирования запросов — настройка буферизации. По умолчанию IIS буферизует ответы, что может негативно сказываться на производительности при стриминге данных или SSE (Server-Sent Events):

XML
1
2
3
4
5
6
7
8
9
<aspNetCore processPath="dotnet" 
          arguments=".\MyApp.dll" 
          ... >
<httpCompression directory="%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files">
  <dynamicTypes>
    <add mimeType="text/event-stream" enabled="false" />
  </dynamicTypes>
</httpCompression>
</aspNetCore>
Эта конфигурация отключает компрессию для SSE, что необходимо для правильного стриминга событий. Без этой настройки вы можете столкнуться с непредсказуемыми задержками или полным отсутствием данных у клиента.

Однажды я бился над проблемой, когда наше приложение с real-time уведомлениями работало прекрасно за Kestrel, но категорически отказывалось корректно стримить события через IIS. Оказалось, что проблема была именно в буферизации и компрессии IIS.

Кстати о буферизации — рекомендую также обратить внимание на параметр responseBufferLimit:

XML
1
2
3
4
<aspNetCore processPath="dotnet" 
          arguments=".\MyApp.dll"
          responseBufferLimit="0"
          ... />
Установка этого параметра в 0 отключает буферизацию ответа полностью, что идеально для стриминга и увеличивает отзывчивость приложения при больших ответах. Но осторожно: это может увеличить нагрузку на сервер, если у вас много одновременных соединений.

При работе с заголовками HTTP в контейнере часто возникают проблемы с кросс-доменными запросами (CORS). IIS может мешать нормальной работе CORS-middleware в ASP.NET Core. Чтобы избежать конфликтов, добавьте следующее:

XML
1
2
3
4
5
6
7
<system.webServer>
  <httpProtocol>
    <customHeaders>
      <clear />
    </customHeaders>
  </httpProtocol>
</system.webServer>
Этот трюк с <clear /> предотвращает добавление IIS своих заголовков, которые могут конфликтовать с заголовками, установленными вашим приложением. Особенно актуально это для API, которые используются с фронтенд-приложениями. Я потратил целый день, отлаживая странное поведение CORS в одном из проектов, пока не обнаружил, что IIS добавлял свои заголовники, конфликтующие с нашими.

Для защиты от атак типа Slowloris и подобных DoS-уязвимостей, связанных с медленными клиентами, рекомендую настроить лимиты:

XML
1
2
3
4
5
6
7
8
<system.webServer>
  <serverRuntime uploadReadAheadSize="65536" />
  <security>
    <requestFiltering>
      <requestLimits maxAllowedContentLength="30000000" maxQueryString="2048" maxUrl="4096" />
    </requestFiltering>
  </security>
</system.webServer>
Эти настройки ограничивают размер загружаемых файлов до ~30 МБ, длину строки запроса до 2 КБ и общую длину URL до 4 КБ. Конечно, значения нужно адаптировать под ваш конкретный случай. В одном банковском проекте эти ограничения спасли нас от атаки, когда злоумышленники пытались перегрузить систему огромными POST-запросами. IIS блокировал их еще до того, как они достигали нашего приложения.

Dockerfile: от теории к практике



Когда у нас уже есть понимание того, как настроить IIS и сконфигурировать web.config, пора перейти к самому Dockerfile. Здесь Windows-контейнеры преподносят нам особенные "сюрпризы", о которых я раскажу подробно. Забегая вперед скажу — размер имеет значение, особенно когда базовый образ Windows весит под 4 гигабайта. Первое, с чем я обычно сталкиваюсь при оптимизации Windows-контейнеров — необходимость в многоэтапной сборке (multi-stage builds). Этот подход позволяет использовать один контейнер для компиляции приложения, а другой — для его выполнения. Вот типичный пример, который я использую в своих проектах:

Windows Batch file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# Этап сборки
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
 
# Копируем только файлы проектов для восстановления зависимостей
COPY ["MyApp.csproj", "./"]
RUN dotnet restore "MyApp.csproj"
 
# Копируем остальной код и собираем приложение
COPY . .
RUN dotnet build "MyApp.csproj" -c Release -o /app/build
RUN dotnet publish "MyApp.csproj" -c Release -o /app/publish /p:UseAppHost=true
 
# Финальный образ
FROM mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019
WORKDIR /app
 
# Устанавливаем ASP.NET Core Hosting Bundle
RUN powershell -Command \
    $ErrorActionPreference = 'Stop'; \
    $ProgressPreference = 'SilentlyContinue'; \
    Invoke-WebRequest -OutFile dotnet-hosting-6.0.0-win.exe [url]https://download.visualstudio.microsoft.com/download/pr/6bbd8093-2e4a-47e6-9a39-2c24daa23b3c/2c9ece351dbcfaf8724f3a6f9dffbab3/dotnet-hosting-6.0.0-win.exe;[/url] \
    Start-Process -FilePath './dotnet-hosting-6.0.0-win.exe' -ArgumentList '/install', '/quiet', '/norestart' -NoNewWindow -Wait; \
    Remove-Item -Force dotnet-hosting-6.0.0-win.exe
 
# Копируем опубликованное приложение из этапа сборки
COPY --from=build /app/publish .
 
# Создаем папку для логов и устанавливаем права
RUN mkdir C:\app\logs && icacls C:\app\logs /grant "IIS AppPool\DefaultAppPool:(OI)(CI)F"
 
# Копируем web.config
COPY web.config .
 
# Настраиваем IIS-сайт
RUN powershell -Command \
    Import-Module WebAdministration; \
    Remove-Website -Name 'Default Web Site'; \
    New-Website -Name 'aspnetcore-site' -PhysicalPath 'C:\app' -Port 80 -Force; \
    Set-ItemProperty -Path 'IIS:\AppPools\DefaultAppPool' -Name 'processModel.identityType' -Value 'ApplicationPoolIdentity'
 
EXPOSE 80
 
# Запускаем ServiceMonitor, который следит за работой IIS
ENTRYPOINT ["C:\\ServiceMonitor.exe", "w3svc"]
Такой подход дает нам сразу несколько преимуществ. Во-первых, в финальном образе отсутствует SDK и исходный код — только скомпилированные бинарники. Во-вторых, при изменении исходного кода пересобирается только часть слоев, что существенно ускоряет процесс. Однако с Windows-контейнерами есть один неприятный момент — даже при использовании многоэтапной сборки финальный образ все равно остается огромным из-за базового образа Windows Server Core. Как с этим бороться? Первое что я делаю — использую только необходимые компоненты IIS. Вместо установки всего подряд через /all в DISM, я выборочно устанавливаю только те модули, которые реально нужны. Это может сэкономить сотни мегабайт. Второй трюк — очистка временных файлов после установки компонентов:

Windows Batch file
1
2
3
4
5
6
7
8
9
10
RUN powershell -Command \
    # Установка компонентов
    dism.exe /online /enable-feature /featurename:IIS-WebServerRole /norestart && \
    # ... другие компоненты ... && \
    # Очистка
    Remove-Item -Recurse C:\Windows\WinSxS\ManifestCache\* -Force; \
    Remove-Item -Recurse C:\Windows\Temp\* -Force; \
    Remove-Item -Recurse C:\Windows\Logs\* -Force; \
    Remove-Item -Recurse C:\Windows\Installer\$PatchCache$ -Force; \
    Optimize-VHD -Path C:\ -Mode Full
Команда Optimize-VHD особенно полезна — она дефрагментирует и уплотняет виртуальный диск, что может сократить размер образа на 10-15%.

Один из секретов, который я обнаружил экспериментальным путем — объединение нескольких команд RUN в одну. Каждый RUN в Dockerfile создает новый слой образа, и в мире Windows эти слои могут быть очень большими из-за специфики файловой системы NTFS. Поэтому я всегда стараюсь группировать команды:

Windows Batch file
1
2
3
4
5
6
7
8
9
# Плохо: много слоев
RUN dism.exe /online /enable-feature /featurename:IIS-WebServerRole /norestart
RUN dism.exe /online /enable-feature /featurename:IIS-WebServer /norestart
RUN dism.exe /online /enable-feature /featurename:IIS-CommonHttpFeatures /norestart
 
# Хорошо: один слой
RUN dism.exe /online /enable-feature /featurename:IIS-WebServerRole /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-WebServer /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-CommonHttpFeatures /norestart
Еще один важный момент — кеширование промежуточных слоев Docker. По умолчанию Docker кеширует слои, и если вы не изменили команду в Dockerfile, то слой будет использован из кеша. Это отлично работает для Linux-контейнеров, но с Windows есть нюансы.

Я заметил, что при работе с Windows-контейнерами кеш слоев часто инвалидируется по неочевидным причинам. Например, просто перезапуск Docker Desktop может привести к тому, что все слои будут собираться заново. Чтобы минимизировать влияние этой проблемы, я использую следующий подход:

1. Размещаю самые тяжелые и редко меняющиеся команды в начале Dockerfile.
2. Перемещаю копирование файлов проекта и исходного кода как можно ближе к концу.
3. Использую .dockerignore для исключения ненужных файлов.

Вот пример .dockerignore, который я обычно использую:

C#
1
2
3
4
5
6
7
8
9
**/bin/
**/obj/
**/node_modules/
**/.vs/
**/.vscode/
**/TestResults/
**/*.user
**/*.trx
**/*.log
Это позволяет существенно уменьшить контекст сборки, что особенно критично для Windows-контейнеров, где копирование файлов происходит заметно медленнее, чем в Linux. Теперь о BuildKit — это новый движок сборки Docker, который значительно ускоряет процесс благодаря параллельному выполнению шагов и улучшенному кешированию. Для Windows-контейнеров это особенно актуально. Чтобы включить BuildKit, я использую:

PowerShell
1
2
$env:DOCKER_BUILDKIT=1
docker build -t myapp .
Или в Docker Compose:

YAML
1
2
3
4
5
6
7
8
version: '3.8'
services:
  webapp:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      - DOCKER_BUILDKIT=1
BuildKit дает особенно заметный выигрыш при многоэтапной сборке, так как может выполнять независимые этапы параллельно. На практике это ускоряет сборку Windows-контейнеров на 30-40%. Один из моих любимых трюков при работе с Windows-контейнерами — использование монтирования томов во время разработки. Это позволяет не пересобирать контейнер при каждом изменении кода:

PowerShell
1
docker run -d -p 8080:80 -v C:\Projects\MyApp:C:\app myapp
Однако с Windows есть подводный камень — права доступа. Файлы, примонтированные из хост-системы, могут оказаться недоступными для ApplicationPoolIdentity внутри контейнера. Решение — использовать анонимную аутентификацию и отдельный пул приложений с настроенной учетной записью.

Еще один полезный прием, который я использую для Windows-контейнеров — запуск предварительного прогрева (warmup) приложения. В отличие от Linux, где контейнеры стартуют быстро, Windows-контейнеры могут запускаться до минуты. И если первый запрос к ASP.NET Core приложению тоже займет время на компиляцию представлений или инициализацию зависимостей — пользователю придется ждать еще дольше. Мое решение — добавить скрипт прогрева в Dockerfile:

Windows Batch file
1
2
3
4
5
# Копируем скрипт прогрева
COPY warmup.ps1 C:\app\
 
# Запускаем его в процессе запуска контейнера
ENTRYPOINT ["powershell.exe", "-File", "C:\\app\\warmup.ps1"]
А сам скрипт warmup.ps1 выглядит примерно так:

PowerShell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Запускаем IIS
Start-Service W3SVC
 
# Ждем 5 секунд, чтобы IIS успел запуститься
Start-Sleep -Seconds 5
 
# Делаем запрос к приложению для прогрева
try {
    Invoke-WebRequest -Uri "http://localhost/" -UseBasicParsing | Out-Null
    Write-Host "Application warmed up successfully"
} catch {
    Write-Host "Failed to warm up application: $_"
}
 
# Запускаем ServiceMonitor, который следит за работой IIS
& C:\ServiceMonitor.exe w3svc
Такой подход гарантирует, что к моменту, когда контейнер считается запущенным, приложение уже полностью проинициализировано и готово обрабатывать запросы без задержек. Интересный нюанс с образами Windows — они часто обновляются Microsoft, и базовый образ mcr.microsoft.com/windows/servercore:ltsc2019 может изменяться даже без изменения тега. Поэтому для производственных систем я рекомендую фиксировать конкретный дайджест образа:

Windows Batch file
1
FROM mcr.microsoft.com/windows/servercore@sha256:4612bb9e3790c6981a929857aeb6dd5b1e7de0fa5e3afe96e23fb47d20aea55c
Это гарантирует, что вы всегда получите точно тот же базовый образ, даже если Microsoft опубликует обновление под тем же тегом.
Еще один момент, о котором редко пишут — проблема с одновременным выполнением нескольких PowerShell-команд в Dockerfile. Из-за особенностей PowerShell команды, объединенные через &&, могут работать не так, как ожидается. Мое решение — использовать строку-разделитель и оператор ;:

Windows Batch file
1
2
3
4
5
RUN powershell -Command \
  "$ErrorActionPreference = 'Stop'; \
  Install-WindowsFeature -name Web-Server; \
  Install-WindowsFeature Web-Mgmt-Console; \
  Remove-Item -Recurse C:\Windows\Temp\* -Force"
Что касается переменных окружения — с ними в Windows-контейнерах тоже есть сюрпризы. В отличие от Linux, где переменные окружения чувствительны к регистру, в Windows они регистронезависимые. Это может создать неожиданные проблемы, если ваше приложение полагается на переменные с похожими именами, отличающимися только регистром. Кроме того, в Windows есть ограничение на длину переменных окружения — около 32 КБ на все переменные вместе. Если вы используете переменные для передачи больших конфигураций, можете столкнуться с этим ограничением. Мое решение — использовать файлы конфигурации вместо переменных окружения для больших наборов настроек.
При работе с секретами в Windows-контейнерах я предпочитаю использовать Docker secrets или переменные окружения, устанавливаемые при запуске контейнера, а не хардкодить их в Dockerfile:

PowerShell
1
docker run -d -p 8080:80 -e "ConnectionStrings__DefaultConnection=Server=db;Database=mydb;User=sa;Password=Pass@word" myapp
Важный момент при настройке Windows-контейнеров — управление процессом завершения работы. В отличие от Linux, где сигналы SIGTERM и SIGKILL обрабатываются предсказуемо, Windows имеет свои особености. Когда Docker пытается остановить Windows-контейнер, он отправляет сигнал CTRL_SHUTDOWN_EVENT процессу, указанному в ENTRYPOINT.

Проблема в том, что ServiceMonitor, который мы используем для мониторинга IIS, не всегда корректно обрабатывает этот сигнал. В результате IIS может быть завершен некорректно, а приложение не получит шанс завершить работу аккуратно. Мое решение — использовать скрипт-обертку для корректной обработки завершения:

PowerShell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# shutdown.ps1
function Shutdown-Gracefully {
    Write-Host "Shutting down gracefully..."
    
    # Остановка IIS аккуратно
    iisreset /stop
    
    # Ждем завершения всех запросов
    Start-Sleep -Seconds 5
    
    Write-Host "Shutdown complete"
    exit 0
}
 
# Регистрируем обработчик события завершения
$null = Register-EngineEvent -SourceIdentifier ([System.Console]::CancelKeyPress) -Action { Shutdown-Gracefully }
 
# Запускаем ServiceMonitor
& C:\ServiceMonitor.exe w3svc
Такой скрипт гарантирует, что при остановке контейнера IIS будет корректно завершен, а все запросы обработаны.

Проблемы с правами доступа и их решение



Права доступа в Windows-контейнерах — это отдельный круг ада, с которым я сталкивался в каждом проекте без исключения. Казалось бы, простая вещь — настроить, кто и что может делать в системе, но в реальности это превращается в настоящий квест с неочевидными подсказками и скрытыми боссами. Начнём с самого частого сценария — настройки Application Pool Identity. По умолчанию IIS запускает пулы приложений под учётной записью ApplicationPoolIdentity. Это виртуальная учётная запись, которая существует только в контексте IIS. В обычной Windows-системе она автоматически получает доступ к директории сайта, но в контейнере всё не так просто. Я помню, как на одном проекте мы получали загадочную ошибку 502.5 при попытке открыть сайт, и логи просто молчали. Оказалось, что ApplicationPoolIdentity не имел прав на чтение файлов приложения. Решение выглядело так:

PowerShell
1
2
# Предоставляем права пулу приложений на директорию приложения
RUN icacls C:\app /grant "IIS AppPool\DefaultAppPool:(OI)(CI)F"
Здесь (OI) означает "object inherit" — права распространяются на все файлы в директории, а (CI) — "container inherit", что распространяет права на все поддиректории. F — это полный доступ (Full control). Что интересно, в некоторых случаях даже этого недостаточно. Я столкнулся с ситуацией, когда приложение работало, но не могло записывать временные файлы. Проблема была в том, что ApplicationPoolIdentity нужны права не только на директорию приложения, но и на директорию C:\Windows\Temp:

PowerShell
1
RUN icacls C:\Windows\Temp /grant "IIS AppPool\DefaultAppPool:(OI)(CI)F"
Иногда имеет смысл изменить идентификатор пула приложений на другую учётную запись. Например, на LocalSystem, которая имеет максимальные привилегии в системе:

PowerShell
1
2
3
RUN powershell -Command \
  Import-Module WebAdministration; \
  Set-ItemProperty -Path 'IIS:\AppPools\DefaultAppPool' -Name 'processModel.identityType' -Value 'LocalSystem'
Но это решение я рекомендую только для тестовых сред или если вы точно понимаете риски. LocalSystem — это суперпользователь Windows, и если ваше приложение будет скомпрометировано, атакующий получит полный контроль над контейнером. Более безопасная альтернатива — создать отдельного пользователя с минимально необходимыми правами:

PowerShell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Создаём пользователя и задаем пароль
RUN net user appuser P@ssw0rd /add
 
# Добавляем пользователя в группу IIS_IUSRS для базовых прав на IIS
RUN net localgroup IIS_IUSRS appuser /add
 
# Предоставляем права на директорию приложения
RUN icacls C:\app /grant "appuser:(OI)(CI)F"
 
# Настраиваем пул приложений на использование этого пользователя
RUN powershell -Command \
  Import-Module WebAdministration; \
  Set-ItemProperty -Path 'IIS:\AppPools\DefaultAppPool' -Name 'processModel.identityType' -Value 'SpecificUser'; \
  Set-ItemProperty -Path 'IIS:\AppPools\DefaultAppPool' -Name 'processModel.userName' -Value 'appuser'; \
  Set-ItemProperty -Path 'IIS:\AppPools\DefaultAppPool' -Name 'processModel.password' -Value 'P@ssw0rd'
Обратите внимание, что пароль хранится в открытом виде в Dockerfile, что является потенциальной уязвимостью. В реальных проектах я рекомендую использовать Docker secrets или переменные окружения для хранения чувствительных данных. Особенно интересная история начинается, когда вы пытаетесь использовать доменные учётные записи в контейнере. Технически это возможно, если контейнер присоединён к домену, но на практике это создаёт целый ряд проблем.

Первая проблема — присоединение к домену. Windows-контейнеры не могут быть непосредственно присоединены к домену Active Directory. Вместо этого они наследуют членство в домене от хост-машины. Это означает, что хост должен быть членом домена, и контейнер должен запускаться с флагом --security-opt "credentialspec=file://MyCredentialSpec.json". Файл CredentialSpec содержит информацию о доменной учётной записи, которую будет использовать контейнер. Создать его можно с помощью модуля CredentialSpec для PowerShell:

PowerShell
1
2
Install-Module -Name CredentialSpec
New-CredentialSpec -Name MyCredentialSpec -AccountName "MYDOMAIN\MyServiceAccount"
Это создаст файл JSON в директории C:\ProgramData\docker\CredentialSpecs\MyCredentialSpec.json, который можно использовать при запуске контейнера.

Но тут начинается вторая проблема — gMSA (group Managed Service Accounts). Обычные доменные учётные записи не работают в контейнерах, вместо них нужно использовать gMSA. Это специальный тип учётных записей, предназначенный для служб и контейнеров. Настройка gMSA — это отдельный квест, который включает:
1. Создание KDS Root Key в домене (если ещё не создан);
2. Создание gMSA через Active Directory;
3. Предоставление хост-машине прав на использование gMSA;
4. Создание CredentialSpec с указанием gMSA;

Еще одна распространённая проблема — доступ к сетевым ресурсам. Даже если контейнер использует gMSA, он может не иметь доступа к сетевым дискам или другим ресурсам в домене. Дело в том, что сетевые запросы из контейнера проходят через NAT, и информация о доменной аутентификации может теряться. Решение — использовать IP-адреса вместо имён хостов и явно указывать учётные данные при подключении к сетевым ресурсам. Например, вместо:

C#
1
var files = Directory.GetFiles("\\\\fileserver\\share\\");
Используйте:

C#
1
2
3
4
5
var credentials = new NetworkCredential("username", "password", "domain");
using (new NetworkConnection("\\\\192.168.1.100\\share", credentials))
{
    var files = Directory.GetFiles("\\\\192.168.1.100\\share\\");
}
Где NetworkConnection — это класс-обёртка для Win32 API функции WNetAddConnection2:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class NetworkConnection : IDisposable
{
    private string _networkName;
 
    public NetworkConnection(string networkName, NetworkCredential credentials)
    {
        _networkName = networkName;
        
        var result = WNetAddConnection2(networkName, credentials.Password, 
            credentials.Domain + "\\" + credentials.UserName, 0);
        
        if (result != 0)
            throw new Win32Exception(result);
    }
 
    ~NetworkConnection()
    {
        Dispose(false);
    }
 
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
 
    protected virtual void Dispose(bool disposing)
    {
        WNetCancelConnection2(_networkName, 0, true);
    }
 
    [DllImport("mpr.dll")]
    private static extern int WNetAddConnection2(string networkName, 
        string password, string username, int flags);
 
    [DllImport("mpr.dll")]
    private static extern int WNetCancelConnection2(string name, int flags, 
        bool force);
}
Отдельная категория проблем связана с портами. В Windows-контейнерах порты ниже 1024 не требуют административных прав, как в обычной Windows, но могут возникать конфликты, если несколько контейнеров пытаются использовать один и тот же порт. Частая ошибка — попытка использовать один и тот же порт внутри и снаружи контейнера:

PowerShell
1
docker run -p 80:80 myimage
Это работает только если порт 80 на хост-машине свободен. Если нет, вы получите ошибку "Порт уже используется". Решение — использовать другой порт на хосте:

PowerShell
1
docker run -p 8080:80 myimage
Теперь контейнер будет доступен по адресу http://localhost:8080, а внутри будет слушать порт 80.
Но недостаточно просто настроить порты — нужно ещё и правильно их диагностировать. Когда я запускаю контейнеры в production, я всегда использую команду netstat для проверки, на каких портах реально слушает приложение:

PowerShell
1
docker exec -it mycontainer powershell -Command "netstat -ano | findstr LISTENING"
Это позволяет увидеть все прослушиваемые порты внутри контейнера и убедиться, что приложение действительно слушает на том порту, который вы ожидаете. Частая ошибка — считать, что порт открыт, хотя на самом деле приложение слушает только на localhost (127.0.0.1). В контексте контейнера это означает, что порт недоступен извне. Чтобы приложение было доступно, оно должно слушать на адресе 0.0.0.0 (все интерфейсы) или на конкретном IP-адресе контейнера.
В ASP.NET Core это настраивается в Program.cs:

C#
1
2
3
4
builder.WebHost.ConfigureKestrel(options =>
{
    options.Listen(IPAddress.Any, 80);
});
Однако, в контексте IIS и Kestrel за ним, это уже не актуально, так как IIS сам слушает на всех интерфейсах и передает запросы в Kestrel.

Теперь давайте глубже погрузимся в тему интеграции с Active Directory. Даже если вы настроили gMSA, как я описал ранее, есть еще много нюансов. Например, когда Windows-контейнер пытается обратиться к контроллеру домена, он должен иметь возможность разрешить его имя через DNS. В обычной Windows это работает автоматически, но в контейнере может потребоваться явно указать DNS-сервер:

Windows Batch file
1
2
3
4
5
6
# В docker-compose.yml
services:
  webapp:
    image: mywebapp
    dns:
      - 192.168.1.10  # IP-адрес контроллера домена или DNS-сервера
Еще один момент — Kerberos-аутентификация требует правильной настройки времени. Если время в контейнере отличается от времени контроллера домена более чем на 5 минут, аутентификация не сработает. Обычно контейнер наследует время от хоста, но иногда это может быть проблемой, особенно если хост и контейнер находятся в разных часовых поясах. Решение — синхронизировать время явно:

PowerShell
1
2
# В скрипте запуска контейнера
w32tm /resync /computer:timeserver.mydomain.com
Особая боль — это приложения, которые используют NTLM-аутентификацию для доступа к ресурсам. В Windows-контейнерах NTLM работает не так, как в обычной Windows, и часто требует дополнительной настройки.
Например, если ваше приложение использует HttpClient для обращения к сервису, требующему Windows-аутентификации, нужно явно указать учетные данные и тип аутентификации:

C#
1
2
3
4
5
6
7
8
var handler = new HttpClientHandler
{
    UseDefaultCredentials = false,
    Credentials = new NetworkCredential("username", "password", "domain"),
    AuthenticationScheme = System.Net.AuthenticationSchemes.Negotiate
};
 
var client = new HttpClient(handler);
А что если вам нужно использовать текущего пользователя? В контейнере с gMSA это возможно, но требует дополнительных настроек:

C#
1
2
3
4
5
6
7
8
var handler = new HttpClientHandler
{
    UseDefaultCredentials = true,
    PreAuthenticate = true,
    AuthenticationScheme = System.Net.AuthenticationSchemes.Negotiate
};
 
var client = new HttpClient(handler);
Всё это работает, только если контейнер запущен с правильным CredentialSpec и имеет доступ к контроллеру домена.
Заключительный аспект, о котором стоит упомянуть — интеграция с системами единого входа (SSO). Если ваше приложение использует SAML, OAuth или OpenID Connect для аутентификации через корпоративный IdP, то в контейнере могут возникнуть дополнительные сложности.

Например, многие реализации SAML полагаются на имя хоста для проверки получателя (audience). Но в контейнере имя хоста обычно отличается от того, как пользователи обращаются к приложению. Решение — явно указать публичный URL в конфигурации:

JSON
1
2
3
4
5
6
7
8
{
  "Saml2": {
    "ServiceProviderEntity": {
      "EntityId": "https://public-url.com/saml",
      "AssertionConsumerServiceUrl": "https://public-url.com/saml/acs"
    }
  }
}
В моей практике именно проблемы с аутентификацией и правами доступа занимают большую часть времени при перемещении приложений в контейнеры, особенно когда речь идет о интеграции с корпоративными системами. Но с правильным подходом и пониманием внутренних механизмов Windows и IIS, эти проблемы решаемы.

Мониторинг ресурсов контейнера в production



Мониторинг Windows-контейнеров с IIS - это отдельная дисциплина, которая существенно отличается от мониторинга Linux-контейнеров. За годы работы с контейнеризоваными приложениями на Windows я неоднократно сталкивался с ситуациями, когда стандартные подходы к мониторингу просто не работали, а системы наблюдения показывали, что всё отлично, в то время как пользователи не могли зайти в приложение. Первое, с чем нужно разобраться - это настройка проб здоровья (health checks). В Kubernetes или Docker Swarm они жизненно важны для определения состояния вашего приложения. Для Windows-контейнера с IIS и ASP.NET Core нужно настроить как минимум три типа проверок:

1. Liveness Probe - проверяет, жив ли контейнер в принципе. Для IIS отличным индикатором является проверка статуса службы W3SVC:

YAML
1
2
3
4
5
6
7
8
9
10
livenessProbe:
  exec:
    command:
    - powershell.exe
    - -command
    - (Get-Service -Name W3SVC).Status -eq 'Running'
  initialDelaySeconds: 60
  periodSeconds: 30
  timeoutSeconds: 5
  failureThreshold: 3
2. Readiness Probe - проверяет, готов ли контейнер принимать запросы. Здесь лучше делать реальный HTTP-запрос к вашему приложению:

YAML
1
2
3
4
5
6
7
8
9
readinessProbe:
  httpGet:
    path: /health
    port: 80
  initialDelaySeconds: 30
  periodSeconds: 10
  timeoutSeconds: 3
  failureThreshold: 3
  successThreshold: 1
Только не забудьте реализовать в вашем приложении эндпоинт /health, который вернет 200 OK, если все в порядке. В ASP.NET Core это делается очень просто:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void ConfigureServices(IServiceCollection services)
{
    // Добавляем сервис проверок здоровья
    services.AddHealthChecks()
        // Добавляем проверку SQL Server, если он используется
        .AddSqlServer(Configuration.GetConnectionString("DefaultConnection"))
        // Другие проверки, например внешние API
        .AddUrlGroup(new Uri("https://external-api.com/health"));
}
 
public void Configure(IApplicationBuilder app)
{
    // ... другие middleware
 
    // Регистрируем эндпоинт для проверки здоровья
    app.UseHealthChecks("/health");
}
3. Startup Probe - важнейшая проверка для Windows-контейнеров, поскольку они запускаются значительно дольше Linux-аналогов. Она дает приложению больше времени на инициализацию, прежде чем liveness и readiness пробы начнут перезапускать его из-за таймаутов:

YAML
1
2
3
4
5
6
7
8
startupProbe:
  httpGet:
    path: /health
    port: 80
  initialDelaySeconds: 60
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 12  # 12 x 10 = 120 секунд на запуск
Теперь про сбор метрик. Windows и IIS имеют богатый набор счетчиков производительности, но в контейнерном мире доступ к ним несколько ограничен. Я обычно использую комбинацию встроеных метрик ASP.NET Core и дополнительных экспортеров.
Для ASP.NET Core отличным выбором является библиотека prometheus-net, которая собирает и экспортирует метрики в формате Prometheus:

C#
1
2
3
4
5
6
7
8
public void Configure(IApplicationBuilder app)
{
    // ... другие middleware
 
    // Экспортируем метрики Prometheus
    app.UseMetricServer();
    app.UseHttpMetrics();
}
Это дает нам базовые метрики как HTTP-запросы, время отклика, статус ответов. Но для Windows-контейнера с IIS этого недостаточно. Я обычно добавляю специальный экспортер, который собирает метрики IIS:

PowerShell
1
2
3
4
5
6
7
8
# Скачиваем и запускаем IIS Exporter вместе с приложением
RUN powershell -Command \
  Invoke-WebRequest -OutFile iis_exporter.exe https://github.com/user/iis_exporter/releases/download/v1.0.0/iis_exporter.exe; \
  New-Item -Path C:\inetpub\wwwroot\metrics -ItemType Directory
 
COPY start.ps1 C:\
 
ENTRYPOINT ["powershell.exe", "C:\\start.ps1"]
А в start.ps1 запускаем и IIS, и экспортер:

PowerShell
1
2
3
Start-Process -FilePath "C:\iis_exporter.exe" -ArgumentList "--web.listen-address=:9123 --collector.iis.site-include=.+" -NoNewWindow
Start-Service W3SVC
& C:\ServiceMonitor.exe w3svc
Это даст нам эндпоинт на порту 9123, который будет возвращать метрики IIS в формате Prometheus.
Отдельная история - это мониторинг использования памяти. Windows-контейнеры потребляют значительно больше памяти, чем их Linux-собратья, и понимание того, как именно распределяется эта память, критично. В моей практике было несколько случаев, когда приложение в Windows-контейнере медленно "пухло", пока не съедало всю доступную память и не падало.
Для диагностики таких ситуаций я использую комбинацию стандартных метрик контейнера и специфичных Windows-метрик:

YAML
1
2
3
4
5
# В Prometheus конфигурации
scrape_configs:
  - job_name: 'windows_containers'
    static_configs:
      - targets: ['my-windows-container:9123']
А затем настраиваю в Grafana панель, которая показывает:
  1. Общее потребление памяти контейнером
  2. Память, используемую IIS Worker Process
  3. Память, занятую .NET-кучей
  4. Частоту сборок мусора разных поколений

Особый акцент делаю на мониторинге сетевых подключений. В Windows-контейнерах с IIS часто возникают проблемы с утечкой сокетов, особенно если приложение активно обращается к внешним сервисам. Для мониторинга этого аспекта добавляю экспорт метрики netstat:

PowerShell
1
2
3
4
5
6
7
8
# Добавляем в start.ps1
Start-Job -ScriptBlock {
  while($true) {
    $connections = (netstat -ano | Where-Object { $_ -match "ESTABLISHED" }).Count
    Set-Content -Path "C:\inetpub\wwwroot\metrics\connections.txt" -Value "iis_active_connections $connections"
    Start-Sleep -Seconds 15
  }
}
И настраиваю Prometheus на чтение этого файла через текстовый файловый экспортер.
Что касается логирования - здесь Windows-контейнеры имеют свои особенности. По умолчанию IIS пишет логи в файлы, но в контейнерном мире рекомендуется писать в stdout/stderr. Для этого я настраиваю перенаправление логов:

XML
1
2
3
4
<!-- В web.config -->
<system.webServer>
  <aspNetCore ... stdoutLogEnabled="true" stdoutLogFile="\\.\pipe\stdout" />
</system.webServer>
Запись в именованный канал \\.\pipe\stdout перенаправляет логи в стандартный вывод контейнера, откуда их может собрать Docker или Kubernetes. Для более продвинутого логирования я интегрирую Serilog с IIS и настраиваю отправку логов напрямую в Elasticsearch или другую централизованную систему:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseSerilog((hostingContext, loggerConfiguration) => 
        {
            loggerConfiguration
                .ReadFrom.Configuration(hostingContext.Configuration)
                .WriteTo.Console()
                .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://elasticsearch:9200"))
                {
                    IndexFormat = "app-logs-{0:yyyy.MM.dd}",
                    AutoRegisterTemplate = true
                });
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseIIS();
            webBuilder.UseStartup<Startup>();
        });
Особое внимание уделяю настройке проб здоровья для Kubernetes. В отличие от Linux-контейнеров, Windows-контейнеры с IIS могут находиться в странном "полуживом" состоянии, когда служба запущена, но не обрабатывает запросы. Чтобы отловить такие ситуации, я реализую "глубокую" проверку здоровья:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class DeepHealthCheck : IHealthCheck
{
    private readonly IHttpClientFactory _clientFactory;
    
    public DeepHealthCheck(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }
    
    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        try
        {
            // Проверяем, что IIS работает
            var iisRunning = ServiceController.GetServices()
                .FirstOrDefault(s => s.ServiceName == "W3SVC")?.Status == ServiceControllerStatus.Running;
                
            if (!iisRunning)
                return HealthCheckResult.Unhealthy("IIS не запущен");
                
            // Проверяем, что приложение отвечает на внутренние запросы
            var client = _clientFactory.CreateClient();
            var response = await client.GetAsync("http://localhost/api/internal/ping");
            
            if (!response.IsSuccessStatusCode)
                return HealthCheckResult.Unhealthy("Приложение не отвечает на внутренние запросы");
                
            // Проверяем доступность базы данных и других зависимостей
            // ...
            
            return HealthCheckResult.Healthy();
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("Исключение при проверке здоровья", ex);
        }
    }
}

Интеграция с системами CI/CD и автоматизация деплоя



Главная сложность здесь — принципиальные различия между Linux и Windows контейнерами. Большинство существующих CI/CD систем и практик создавались с прицелом на Linux, и Windows-контейнеры в них часто чувствуют себя как слон в посудной лавке. Начнем с выбора агентов сборки. Для Windows-контейнеров вам понадобятся сборочные машины именно с Windows, причем версия должна совпадать с версией в ваших контейнерах. Я обычно рекомендую использовать Windows Server 2019 или 2022 в качестве агентов сборки, так как эти версии имеют наилучшую поддержку контейнеров.
В Azure DevOps это выглядит примерно так:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pool:
  vmImage: 'windows-2019'
 
steps:
task: PowerShell@2
  inputs:
    targetType: 'inline'
    script: |
      # Проверяем версию Docker
      docker version
      
      # Убеждаемся, что используется режим Windows-контейнеров
      $ErrorActionPreference = 'Stop'
      $current = $(docker info --format '{{.OSType}}')
      if ($current -ne 'windows') {
        Write-Error "Docker настроен на использование $current контейнеров, а нужны windows"
      }
Обратите внимание на проверку типа контейнеров — это критично, так как некоторые сборочные агенты могут быть настроены на использование Linux-контейнеров по умолчанию, даже если сама ОС — Windows.
В GitHub Actions настройка выглядит похоже:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
jobs:
  build:
    runs-on: windows-2019
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Docker
      run: |
        # Проверяем, что Docker настроен на Windows-контейнеры
        if ((docker info --format '{{.OSType}}') -ne 'windows') {
          & $Env:ProgramFiles\Docker\Docker\DockerCli.exe -SwitchDaemon
        }
Еще одна особенность — время сборки. Windows-контейнеры собираются значительно дольше Linux-аналогов, и это нужно учитывать при настройке таймаутов в CI/CD пайплайнах. Я обычно устанавливаю таймауты не менее 30 минут для первичной сборки образа:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Azure DevOps
jobs:
job: BuildWindowsContainer
  timeoutInMinutes: 30
  steps:
  # ...
 
# GitHub Actions
jobs:
  build:
    runs-on: windows-2019
    timeout-minutes: 30
    steps:
    # ...
Важнейший аспект — кеширование слоев Docker. Без него каждая сборка будет начинаться с нуля, что для Windows-контейнеров может означать часы ожидания. В Azure DevOps это решается через настройку кеша:

YAML
1
2
3
4
5
6
7
steps:
task: Cache@2
  inputs:
    key: 'docker | "$(Agent.OS)" | [B]/Dockerfile'
    path: $(DOCKER_CONFIG)/buildkit
    cacheHitVar: DOCKER_CACHE_HIT
[/B]


В GitHub Actions можно использовать аналогичный подход:

YAML
1
2
3
4
5
6
name: Cache Docker layers
  uses: actions/cache@v3
  with:
    path: |
      C:\ProgramData\Docker\windowsfilter
    key: ${{ runner.os }}-docker-${{ hashFiles('
YAML
1
2
3
/Dockerfile') }}
    restore-keys: |
      ${{ runner.os }}-docker-
Заметьте, что путь к кешу в Windows отличается от Linux. Это одна из многих мелочей, которые могут сломать ваш пайплайн, если вы просто скопируете настройки из Linux-проекта. Для автоматизации развертывания в Kubernetes я рекомендую использовать Helm. Он одинаково хорошо работает как с Linux, так и с Windows контейнерами:

YAML
1
2
3
4
5
6
7
8
9
10
11
# В Azure DevOps
task: HelmDeploy@0
  inputs:
    connectionType: 'Kubernetes Service Connection'
    kubernetesServiceConnection: 'MyCluster'
    command: 'upgrade'
    chartType: 'FilePath'
    chartPath: './charts/myapp'
    releaseName: 'myapp'
    valueFile: './values.yaml'
    arguments: '--set image.tag=$(Build.BuildNumber)'
При настройке деплоя важно учитывать особенности Windows-нод в Kubernetes. Например, я всегда добавляю селектор узлов в мои Helm-чарты:

YAML
1
2
3
4
5
6
7
8
9
# В values.yaml
nodeSelector:
  kubernetes.io/os: windows
  
tolerations:
key: "os"
  operator: "Equal"
  value: "windows"
  effect: "NoSchedule"
Это гарантирует, что поды будут запланированы только на Windows-нодах, что критично для наших контейнеров.
Отдельная история — тестирование Windows-контейнеров в CI/CD пайплайнах. Обычная практика — запустить контейнер и выполнить тесты внутри него — тут работает с оговорками. Windows-контейнеры запускаются дольше и требуют больше ресурсов. Я предпочитаю подход с выделенной тестовой средой:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# В Azure DevOps
job: IntegrationTests
  dependsOn: Build
  steps:
  - task: DockerCompose@0
    inputs:
      containerregistrytype: 'Azure Container Registry'
      azureSubscription: 'MyAzureConnection'
      azureContainerRegistry: 'myregistry.azurecr.io'
      dockerComposeFile: 'docker-compose.test.yml'
      action: 'Run services'
      
  - task: PowerShell@2
    inputs:
      targetType: 'inline'
      script: |
        # Ждем, пока контейнеры полностью запустятся
        Start-Sleep -Seconds 60
        
        # Выполняем тесты
        dotnet test ./tests/IntegrationTests/IntegrationTests.csproj
Заметьте паузу в 60 секунд — это не излишество, а необходимость для Windows-контейнеров, которым требуется больше времени на полную инициализацию.
Что касается стратегий деплоя, то для Windows-контейнеров с IIS я рекомендую "голубой-зеленый" подход (blue-green deployment). Этот метод хорошо компенсирует долгое время запуска Windows-контейнеров, позволяя новой версии полностью инициализироваться, прежде чем на неё будет переключен трафик:

YAML
1
2
3
4
5
6
# В Kubernetes manifest
strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 0
Для приложений с состоянием (stateful) я часто использую подход с теплым запуском (warm-up), когда новый под получает несколько тестовых запросов перед тем, как принять боевой трафик:

YAML
1
2
3
4
5
6
7
8
9
# В Kubernetes manifest
readinessProbe:
  httpGet:
    path: /health/ready
    port: 80
  initialDelaySeconds: 90
  periodSeconds: 15
  timeoutSeconds: 5
  failureThreshold: 3
Интеграция с системами мониторинга тоже имеет свои особенности. Например, для Prometheus я использую специальный экспортер для Windows и IIS:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# В docker-compose.yml для тестовой среды
services:
  app:
    build: .
    ports:
      - "80:80"
      - "9182:9182"  # Порт для метрик Prometheus
    volumes:
      - prometheus-exporter:/prometheus
      
  prometheus:
    image: prom/prometheus:v2.30.0
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
Особое внимание уделяю секретам в CI/CD пайплайнах. Windows-контейнеры часто требуют чувствительных данных для интеграции с доменом или другими Windows-специфичными сервисами:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
# В Azure DevOps
task: AzureKeyVault@2
  inputs:
    azureSubscription: 'MyAzureConnection'
    KeyVaultName: 'my-key-vault'
    SecretsFilter: 'domain-user,domain-password'
    
# Используем секреты при сборке образа
task: DockerCompose@0
  inputs:
    # ...
    arguments: '--build-arg DOMAIN_USER=$(domain-user) --build-arg DOMAIN_PASSWORD=$(domain-password)'
В финальном образе я рекомендую использовать переменные окружения для настройки приложения, а не встраивать конфигурацию в образ. Это позволяет использовать один и тот же образ в разных средах:

YAML
1
2
3
4
5
6
7
8
9
# В Kubernetes manifest
env:
name: ConnectionStrings__DefaultConnection
  valueFrom:
    secretKeyRef:
      name: app-secrets
      key: connection-string
name: AppSettings__AuthServer
  value: [url]https://auth.example.com[/url]
Наконец, для упрощения управления релизами я активно использую теги образов, основанные на ветках и коммитах:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# В Azure DevOps
task: PowerShell@2
  inputs:
    targetType: 'inline'
    script: |
      $branch = "$(Build.SourceBranchName)"
      $commitId = "$(Build.SourceVersion)".Substring(0, 7)
      
      if ($branch -eq "main") {
        $tag = "latest,$commitId"
      } else {
        $tag = "$branch-$commitId"
      }
      
      Write-Host "##vso[task.setvariable variable=ImageTag]$tag"
Это позволяет легко идентифицировать, какая именно версия кода работает в конкретном окружении, и быстро откатываться к предыдущим версиям при необходимости.

Альтернативы IIS: когда стоит пересмотреть архитектуру



После всех моих рассказов о настройке IIS в Windows-контейнерах, у вас наверняка возник логичный вопрос: "А стоит ли вообще связываться с этим зоопарком, или есть более простые пути?" И знаете что? Я сам себе задаю этот вопрос каждый раз, когда погружаюсь в очередную настройку IIS в контейнере. За последние пять лет я реализовал десятки проектов с контейнеризацией .NET-приложений, и могу с уверенностью сказать: в большинстве случаев IIS в контейнере — это избыточная сложность. Давайте рассмотрим альтернативы, которые часто оказываются более эффективными и менее проблемными.

Первая и самая очевидная альтернатива — использование встроенного Kestrel-сервера напрямую. С .NET 6 и выше Kestrel стал настолько производительным и надежным, что во многих сценариях он превосходит IIS. Вот типичная конфигурация для Dockerfile:

Windows Batch file
1
2
3
4
5
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build /app/publish .
ENV ASPNETCORE_URLS=http://+:80
ENTRYPOINT ["dotnet", "MyApp.dll"]
Заметьте, насколько это проще, чем многоэтапная настройка IIS! Но что мы теряем? На самом деле, не так уж и много:
1. Буферизация запросов и ответов — в высоконагруженных системах это может быть важно, но Kestrel тоже имеет настройки буферизации, хоть и не такие гибкие.
2. URL Rewriting — встроенный функционал переписывания URL в IIS действительно мощный, но ASP.NET Core имеет свой собственный middleware для этого, который решает 90% типичных задач.
3. Аутентификация Windows — пожалуй, единственный серьезный аргумент в пользу IIS, если вам действительно нужна интеграция с доменом. Хотя и для этого есть обходные пути через использование библиотек как Kerberos.NET.
Если все же вам нужен полноценный веб-сервер перед вашим приложением (например, для терминации SSL или сложной маршрутизации), то на смену IIS могут прийти более легковесные альтернативы.

Nginx — мой личный фаворит, когда речь заходит о прокси-серверах в контейнерах. Он потребляет минимум ресурсов, работает молниеносно и прекрасно справляется с ролью обратного прокси для ASP.NET Core приложений. Вот пример типичной конфигурации:

Windows Batch file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Многоэтапная сборка для ASP.NET Core приложения
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["MyApp.csproj", "./"]
RUN dotnet restore "MyApp.csproj"
COPY . .
RUN dotnet publish "MyApp.csproj" -c Release -o /app/publish
 
# Финальный образ с Nginx и ASP.NET Core Runtime
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build /app/publish .
 
# Устанавливаем Nginx
RUN apt-get update && apt-get install -y nginx
 
# Копируем конфигурацию Nginx
COPY nginx.conf /etc/nginx/sites-available/default
 
# Запускаем и Nginx, и приложение
COPY start.sh /start.sh
RUN chmod +x /start.sh
CMD ["/start.sh"]
А файл start.sh будет выглядеть примерно так:

Bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash
# Запускаем ASP.NET Core приложение на порту 5000
dotnet MyApp.dll --urls "http://localhost:5000" &
 
# Запускаем Nginx, который будет проксировать запросы на порт 5000
nginx -g "daemon off;"
[/CSHARP]
 
Конфигурация Nginx в [INLINE]nginx.conf[/INLINE] будет примерно такой:
 
[/CSHARP]nginx
server {
    listen 80;
    
    location / {
        proxy_pass [url]http://localhost:5000;[/url]
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection keep-alive;
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
Это даст вам почти все преимущества использования внешнего веб-сервера, но без избыточной сложности Windows и IIS. Более того, размер такого контейнера будет в разы меньше, чем образ с Windows Server Core и IIS.
Тем не менее, есть ньюанс — этот подход работает только с Linux-контейнерами. Если вы по каким-то причинам жестко привязаны к Windows-контейнерам, то можно рассмотреть вариант с Apache HTTP Server, который доступен и для Windows:

Windows Batch file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
FROM mcr.microsoft.com/windows/servercore:ltsc2019
 
# Устанавливаем .NET Runtime
RUN powershell -Command \
    $ProgressPreference = 'SilentlyContinue'; \
    Invoke-WebRequest -OutFile dotnet-runtime.exe [url]https://download.visualstudio.microsoft.com/download/pr/b9237529-4cc4-4a15-90a5-ac31e621ca9d/4358f5dd31ffbf4563f12a635e32bb6d/dotnet-runtime-6.0.0-win-x64.exe;[/url] \
    Start-Process -FilePath "./dotnet-runtime.exe" -ArgumentList '/install', '/quiet', '/norestart' -NoNewWindow -Wait; \
    Remove-Item -Force dotnet-runtime.exe
 
# Устанавливаем Apache для Windows
RUN powershell -Command \
    $ProgressPreference = 'SilentlyContinue'; \
    Invoke-WebRequest -OutFile httpd-2.4.52-win64-VS16.zip [url]https://www.apachelounge.com/download/VS16/binaries/httpd-2.4.52-win64-VS16.zip;[/url] \
    Expand-Archive -Path httpd-2.4.52-win64-VS16.zip -DestinationPath C:\; \
    Rename-Item C:\Apache24 C:\Apache; \
    Remove-Item -Force httpd-2.4.52-win64-VS16.zip
 
# Копируем приложение
COPY --from=build /app/publish C:/app
 
# Настраиваем Apache как прокси
COPY httpd.conf C:/Apache/conf/httpd.conf
 
# Запускаем Apache и приложение
COPY start.ps1 C:/
ENTRYPOINT ["powershell.exe", "C:\\start.ps1"]
Файл start.ps1 будет примерно такой:

PowerShell
1
2
3
4
5
6
7
8
9
10
11
# Запускаем ASP.NET Core приложение
Start-Process -FilePath "dotnet" -ArgumentList "C:\app\MyApp.dll --urls http://localhost:5000" -NoNewWindow
 
# Ждем запуска приложения
Start-Sleep -Seconds 5
 
# Запускаем Apache
& C:\Apache\bin\httpd.exe -k start
 
# Держим контейнер запущенным
while ($true) { Start-Sleep -Seconds 10 }
Однако, будем честны — этот подход все равно сложнее, чем просто использование Kestrel напрямую или переход на Linux-контейнеры с Nginx. Говоря об архитектурных решениях, нельзя не упомянуть еще одну альтернативу — полный отказ от реверс-прокси внутри контейнера. В современных Kubernetes-кластерах функцию обратного прокси часто выполняет Ingress-контроллер (например, Nginx Ingress или Traefik). Он может обеспечить все то же самое, что и IIS или другой прокси внутри контейнера:
  1. Терминацию SSL/TLS.
  2. Балансировку нагрузки.
  3. Маршрутизацию запросов.
  4. Аутентификацию на уровне API.
  5. Ограничение скорости запросов.

При таком подходе ваш контейнер с ASP.NET Core приложением становится максимально простым — только рантайм и код приложения, без лишних слоев и компонентов.

Подводя итог, я рекомендую придерживаться следующего правила при выборе архитектуры для контейнеризации ASP.NET Core приложений:
1. Если нет принципиальных требований, используйте Kestrel напрямую в Linux-контейнере — это самый простой и эффективный вариант.
2. Если нужен полноценный обратный прокси — добавьте Nginx в Linux-контейнер или используйте его на уровне Ingress-контроллера.
3. Прибегайте к IIS в Windows-контейнере только если у вас есть конкретные требования, которые невозможно реализовать другими способами (например, сложная интеграция с Windows-аутентификацией или наличие модулей IIS, которые невозможно заменить).
Помните, что каждый дополнительный компонент в контейнере — это не только увеличение размера образа и потребления ресурсов, но и дополнительная точка отказа и усложнение поддержки. В мире контейнеров простота и минимализм часто оказываются ключом к надежной и масштабируемой архитектуре.

Пример enterprise решения



После того, как мы разобрали все составляющие работы ASP.NET Core с IIS в Windows-контейнерах, пришло время собрать это в комплексное решение, которое можно сразу применить в реальном enterprise-проекте. Я подготовил для вас полный пример, который уже прошел проверку в нескольких производственных проектах с высокими требованиями к отказоустойчивости и производительности. Начнем с продвинутого Dockerfile, который учитывает все тонкости, о которых мы говорили:

Windows Batch file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# Этап сборки
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
 
# Копируем только файлы проектов для оптимизации кеширования слоев
COPY ["EnterpriseApp.sln", "./"]
COPY ["src/EnterpriseApp.Api/EnterpriseApp.Api.csproj", "src/EnterpriseApp.Api/"]
COPY ["src/EnterpriseApp.Core/EnterpriseApp.Core.csproj", "src/EnterpriseApp.Core/"]
COPY ["src/EnterpriseApp.Infrastructure/EnterpriseApp.Infrastructure.csproj", "src/EnterpriseApp.Infrastructure/"]
 
# Восстанавливаем зависимости
RUN dotnet restore "EnterpriseApp.sln"
 
# Копируем весь код
COPY . .
 
# Выполняем сборку и публикацию
RUN dotnet build "EnterpriseApp.sln" -c Release -o /app/build
RUN dotnet publish "src/EnterpriseApp.Api/EnterpriseApp.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false
 
# Этап конфигурации IIS
FROM mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019 AS iis-config
 
# Устанавливаем необходимые компоненты Windows (только минимально необходимые)
RUN dism.exe /online /enable-feature /featurename:IIS-ASPNET45 /all /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-RequestFiltering /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-HttpLogging /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-LoggingLibraries /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-HttpTracing /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-URLAuthorization /norestart && \
    dism.exe /online /enable-feature /featurename:IIS-ApplicationInit /norestart
 
# Очищаем временные файлы для уменьшения размера образа
RUN powershell -Command \
    Remove-Item -Recurse C:\Windows\WinSxS\ManifestCache\* -Force -ErrorAction SilentlyContinue; \
    Remove-Item -Recurse C:\Windows\Temp\* -Force -ErrorAction SilentlyContinue; \
    Remove-Item -Recurse C:\Windows\Logs\* -Force -ErrorAction SilentlyContinue
 
# Устанавливаем хостинг-бандл .NET Core
RUN powershell -Command \
    $ErrorActionPreference = 'Stop'; \
    $ProgressPreference = 'SilentlyContinue'; \
    Invoke-WebRequest -OutFile dotnet-hosting.exe [url]https://download.visualstudio.microsoft.com/download/pr/7de08ae2-75e6-49b8-b04a-a0255cca6893/ad0f8cccd01744e0b10ea93d96913c62/dotnet-hosting-6.0.21-win.exe;[/url] \
    Start-Process -FilePath './dotnet-hosting.exe' -ArgumentList '/install', '/quiet', '/norestart' -NoNewWindow -Wait; \
    Remove-Item -Force dotnet-hosting.exe
 
# Финальный образ
FROM mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019
WORKDIR /app
 
# Копируем установленные компоненты из промежуточного образа
COPY --from=iis-config ["C:\\Windows\\System32\\inetsrv", "C:\\Windows\\System32\\inetsrv"]
COPY --from=iis-config ["C:\\Program Files\\IIS", "C:\\Program Files\\IIS"]
COPY --from=iis-config ["C:\\ProgramData\\Microsoft\\NetFramework", "C:\\ProgramData\\Microsoft\\NetFramework"]
COPY --from=iis-config ["C:\\Program Files\\dotnet", "C:\\Program Files\\dotnet"]
COPY --from=iis-config ["C:\\Program Files (x86)\\dotnet", "C:\\Program Files (x86)\\dotnet"]
 
# Копируем опубликованное приложение
COPY --from=build /app/publish .
 
# Создаем необходимые директории
RUN mkdir C:\app\logs && \
    mkdir C:\app\healthchecks && \
    powershell -Command New-Item -Path C:\app\healthchecks\ready.txt -ItemType File -Value "Ready"
 
# Настраиваем права доступа
RUN powershell -Command \
    $acl = Get-Acl C:\app; \
    $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule('IIS AppPool\DefaultAppPool', 'FullControl', 'ContainerInherit, ObjectInherit', 'None', 'Allow'); \
    $acl.SetAccessRule($accessRule); \
    Set-Acl C:\app $acl
 
# Копируем конфигурационные файлы
COPY config/web.config .
COPY config/applicationHost.config C:/Windows/System32/inetsrv/config/
COPY scripts/warmup.ps1 C:/app/
COPY scripts/start.ps1 C:/app/
COPY scripts/healthcheck.ps1 C:/app/
 
# Настраиваем IIS
RUN powershell -Command \
    Import-Module WebAdministration; \
    Remove-Website -Name 'Default Web Site'; \
    New-Website -Name 'EnterpriseApp' -PhysicalPath 'C:\app' -Port 80 -Force; \
    Set-ItemProperty -Path 'IIS:\AppPools\DefaultAppPool' -Name 'processModel.identityType' -Value 'ApplicationPoolIdentity'; \
    Set-ItemProperty -Path 'IIS:\AppPools\DefaultAppPool' -Name 'startMode' -Value 'AlwaysRunning'; \
    Set-ItemProperty -Path 'IIS:\Sites\EnterpriseApp' -Name 'applicationDefaults.preloadEnabled' -Value 'True'; \
    Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Filter 'system.applicationHost/applicationPools/applicationPoolDefaults/failure' -Name 'rapidFailProtection' -Value 'True'; \
    Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Filter 'system.applicationHost/applicationPools/applicationPoolDefaults/failure' -Name 'rapidFailProtectionInterval' -Value '00:05:00'; \
    Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Filter 'system.applicationHost/applicationPools/applicationPoolDefaults/failure' -Name 'rapidFailProtectionMaxCrashes' -Value 5; \
    Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Filter 'system.applicationHost/applicationPools/applicationPoolDefaults/processModel' -Name 'idleTimeout' -Value '00:00:00'
 
# Открываем порты
EXPOSE 80 8080
 
# Настраиваем переменные окружения
ENV ASPNETCORE_ENVIRONMENT=Production \
    DOTNET_RUNNING_IN_CONTAINER=true \
    DOTNET_EnableDiagnostics=0 \
    DOTNET_gcServer=1 \
    DOTNET_gcConcurrent=1
 
# Запускаем приложение с прогревом
ENTRYPOINT ["powershell.exe", "-File", "C:\\app\\start.ps1"]
 
HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD ["powershell.exe", "-File", "C:\\app\\healthcheck.ps1"]
Теперь давайте рассмотрим ключевые конфигурационные файлы, начиная с оптимизированного web.config:

XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?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" 
                  arguments=".\EnterpriseApp.Api.dll" 
                  stdoutLogEnabled="true" 
                  stdoutLogFile=".\logs\stdout" 
                  hostingModel="inprocess" 
                  forwardWindowsAuthToken="false"
                  disableStartUpErrorPage="true"
                  shutdownTimeLimit="30"
                  startupTimeLimit="180">
        <environmentVariables>
          <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production" />
          <environmentVariable name="ASPNETCORE_HOSTINGSTARTUPASSEMBLIES" value="Microsoft.AspNetCore.ApplicationInsights.HostingStartup" />
        </environmentVariables>
        <handlerSettings>
          <handlerSetting name="enableMinimalThreads" value="true" />
          <handlerSetting name="forwardWindowsAuthToken" value="false" />
        </handlerSettings>
      </aspNetCore>
      <security>
        <requestFiltering removeServerHeader="true">
          <requestLimits maxAllowedContentLength="104857600" maxUrl="8192" maxQueryString="4096" />
        </requestFiltering>
      </security>
      <httpProtocol>
        <customHeaders>
          <remove name="X-Powered-By" />
          <add name="X-XSS-Protection" value="1; mode=block" />
          <add name="X-Content-Type-Options" value="nosniff" />
          <add name="X-Frame-Options" value="SAMEORIGIN" />
          <add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains" />
        </customHeaders>
      </httpProtocol>
    </system.webServer>
  </location>
</configuration>
Скрипт запуска и прогрева приложения (start.ps1):

PowerShell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# Устанавливаем режим строгой обработки ошибок
$ErrorActionPreference = "Stop"
 
# Функция для аккуратного завершения работы
function Shutdown-Gracefully {
    Write-Host "Получен сигнал завершения работы, закрываем приложение корректно..."
    
    try {
        # Аккуратная остановка IIS
        & $env:windir\system32\inetsrv\appcmd.exe stop site "EnterpriseApp"
        Write-Host "Сайт IIS остановлен"
        
        # Ждем завершения всех запросов
        Start-Sleep -Seconds 10
        
        # Останавливаем пул приложений
        & $env:windir\system32\inetsrv\appcmd.exe stop apppool "DefaultAppPool"
        Write-Host "Пул приложений остановлен"
    }
    catch {
        Write-Host "Ошибка при корректном завершении работы: $_"
    }
    
    Write-Host "Контейнер корректно завершил работу"
    exit 0
}
 
# Регистрируем обработчик CTRL+C для корректного завершения
[Console]::TreatControlCAsInput = $true
$handler = [Console]::CancelKeyPress.GetInvocationList() | Select-Object -First 1
if ($null -ne $handler) {
    [Console]::CancelKeyPress = [Console]::CancelKeyPress.RemoveAll($handler)
}
[Console]::CancelKeyPress += { Shutdown-Gracefully }
 
# Запускаем IIS
Start-Service W3SVC
Write-Host "Служба IIS запущена"
 
# Запускаем мониторинг здоровья (в фоновом режиме)
Start-Job -ScriptBlock {
    & C:\app\healthcheck.ps1 -MonitoringMode
} | Out-Null
 
# Прогреваем приложение
& C:\app\warmup.ps1
Write-Host "Приложение прогрето и готово к работе"
 
# Запускаем ServiceMonitor, который будет следить за IIS
Write-Host "Запускаем мониторинг службы IIS..."
& C:\ServiceMonitor.exe w3svc
Скрипт прогрева приложения (warmup.ps1):

PowerShell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
$ErrorActionPreference = "Stop"
Write-Host "Начинаем прогрев приложения..."
 
# Ждем полного запуска IIS
Start-Sleep -Seconds 5
 
# Определяем ключевые маршруты для прогрева
$routesToWarmup = @(
    "/",
    "/api/health",
    "/api/values",
    "/api/config"
)
 
# Функция для выполнения запроса с повторными попытками
function Invoke-WithRetry {
    param (
        [string]$Uri,
        [int]$MaxRetries = 5,
        [int]$RetryDelayInSeconds = 2
    )
    
    $attempt = 1
    $success = $false
    
    while (-not $success -and $attempt -le $MaxRetries) {
        try {
            Write-Host "Прогрев $Uri (попытка $attempt из $MaxRetries)..."
            $response = Invoke-WebRequest -Uri $Uri -UseBasicParsing -TimeoutSec 10
            
            if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 400) {
                Write-Host "Прогрев $Uri успешен! Статус: $($response.StatusCode)"
                $success = $true
            }
            else {
                Write-Host "Прогрев $Uri вернул статус $($response.StatusCode), повторная попытка через $RetryDelayInSeconds сек..."
                Start-Sleep -Seconds $RetryDelayInSeconds
                $attempt++
            }
        }
        catch {
            Write-Host "Ошибка при прогреве $Uri: $_"
            Start-Sleep -Seconds $RetryDelayInSeconds
            $attempt++
        }
    }
    
    return $success
}
 
# Прогреваем каждый маршрут
$allSuccess = $true
foreach ($route in $routesToWarmup) {
    $url = "http://localhost$route"
    $success = Invoke-WithRetry -Uri $url
    
    if (-not $success) {
        $allSuccess = $false
        Write-Host "Не удалось прогреть $url после нескольких попыток!" -ForegroundColor Red
    }
}
 
# Создаем файл-индикатор готовности
if ($allSuccess) {
    Set-Content -Path "C:\app\healthchecks\ready.txt" -Value "Ready"
    Write-Host "Прогрев приложения успешно завершен!" -ForegroundColor Green
}
else {
    Set-Content -Path "C:\app\healthchecks\ready.txt" -Value "NotReady"
    Write-Host "Прогрев приложения завершен с ошибками, но контейнер продолжит работу" -ForegroundColor Yellow
}
И наконец, скрипт проверки здоровья (healthcheck.ps1):

PowerShell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
param (
    [switch]$MonitoringMode
)
 
$ErrorActionPreference = "Stop"
$HealthEndpoint = "http://localhost/api/health"
$ReadyFile = "C:\app\healthchecks\ready.txt"
 
function Check-Health {
    try {
        # Проверка службы IIS
        $iisRunning = (Get-Service -Name W3SVC).Status -eq 'Running'
        if (-not $iisRunning) {
            Write-Host "IIS не запущен!"
            return $false
        }
        
        # Проверка доступности приложения
        $response = Invoke-WebRequest -Uri $HealthEndpoint -UseBasicParsing -TimeoutSec 5
        if ($response.StatusCode -ne 200) {
            Write-Host "Эндпоинт здоровья вернул статус $($response.StatusCode)"
            return $false
        }
        
        # Проверка файла готовности
        if (-not (Test-Path $ReadyFile)) {
            Write-Host "Файл готовности не найден"
            return $false
        }
        
        $readyContent = Get-Content $ReadyFile -Raw
        if ($readyContent -ne "Ready") {
            Write-Host "Приложение не готово: $readyContent"
            return $false
        }
        
        # Проверка использования памяти и CPU
        $process = Get-Process -Name w3wp -ErrorAction SilentlyContinue
        if ($null -ne $process) {
            $memoryMB = [math]::Round($process.WorkingSet64 / 1MB, 2)
            Write-Host "Использование памяти: $memoryMB MB"
            
            # Если памяти слишком много, возвращаем false
            if ($memoryMB -gt 2000) {
                Write-Host "Приложение использует слишком много памяти!"
                return $false
            }
        }
        
        return $true
    }
    catch {
        Write-Host "Ошибка при проверке здоровья: $_"
        return $false
    }
}
 
# Режим мониторинга (запускается как фоновый процесс)
if ($MonitoringMode) {
    Write-Host "Запущен фоновый мониторинг здоровья..."
    while ($true) {
        $healthy = Check-Health
        
        if (-not $healthy) {
            Write-Host "Обнаружена проблема со здоровьем приложения!" -ForegroundColor Red
            
            # Запись в журнал событий Windows
            try {
                Write-EventLog -LogName Application -Source "EnterpriseApp" -EventId 1001 -EntryType Warning -Message "Проблема со здоровьем приложения обнаружена мониторингом" -ErrorAction SilentlyContinue
            }
            catch {
                # Игнорируем ошибки записи в журнал
            }
            
            # Здесь можно добавить код для автоисправления проблем
            # Например, перезапуск пула приложений при определенных условиях
        }
        
        # Проверяем каждые 30 секунд
        Start-Sleep -Seconds 30
    }
}
# Режим проверки (используется Docker HEALTHCHECK)
else {
    $healthy = Check-Health
    if ($healthy) {
        exit 0
    }
    else {
        exit 1
    }
}
Использование этого решения в Docker Compose можно реализовать следующим образом:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
version: '3.8'
 
services:
  enterprise-app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:80"
    environment:
      - ConnectionStrings__DefaultConnection=Server=db;Database=EnterpriseDb;User Id=sa;Password=${DB_PASSWORD};TrustServerCertificate=True
      - ApplicationSettings__ApiKeys__ExternalService=${API_KEY}
      - ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
    volumes:
      - app-logs:C:\app\logs
    deploy:
      resources:
        limits:
          memory: 4G
        reservations:
          memory: 2G
      restart_policy:
        condition: on-failure
        max_attempts: 3
        window: 120s
    healthcheck:
      test: ["CMD", "powershell.exe", "-File", "C:\\app\\healthcheck.ps1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
    depends_on:
      - db
 
  db:
    image: mcr.microsoft.com/mssql/server:2019-latest
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=${DB_PASSWORD}
    volumes:
      - db-data:C:\data
    ports:
      - "1433:1433"
 
volumes:
  app-logs:
  db-data:
А вот пример файла Kubernetes для развертывания нашего enterprise-решения:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
apiVersion: apps/v1
kind: Deployment
metadata:
  name: enterprise-app
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: enterprise-app
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: enterprise-app
    spec:
      nodeSelector:
        kubernetes.io/os: windows
      containers:
      - name: enterprise-app
        image: my-registry.com/enterprise-app:latest
        ports:
        - containerPort: 80
        env:
        - name: ConnectionStrings__DefaultConnection
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: connection-string
        - name: ASPNETCORE_ENVIRONMENT
          value: Production
        resources:
          requests:
            memory: "2Gi"
            cpu: "500m"
          limits:
            memory: "4Gi"
            cpu: "1000m"
        readinessProbe:
          httpGet:
            path: /api/health
            port: 80
          initialDelaySeconds: 60
          periodSeconds: 10
        livenessProbe:
          exec:
            command:
            - powershell.exe
            - -command
            - "& C:\app\healthcheck.ps1"
          initialDelaySeconds: 120
          periodSeconds: 30
        volumeMounts:
        - name: logs
          mountPath: C:\app\logs
      volumes:
      - name: logs
        persistentVolumeClaim:
          claimName: enterprise-app-logs
---
apiVersion: v1
kind: Service
metadata:
  name: enterprise-app
  namespace: production
spec:
  selector:
    app: enterprise-app
  ports:
  - port: 80
    targetPort: 80
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: enterprise-app
  namespace: production
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  rules:
  - host: app.enterprise.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: enterprise-app
            port:
              number: 80
  tls:
  - hosts:
    - app.enterprise.com
    secretName: enterprise-tls

Как сделать возможность запускать приложения из IIS + ASP.NET Core приложения?
Есть ASP.NET Core приложение. Есть локальный пользователь с административными правами...

ASP.NET Core. Старт - что нужно знать, чтобы стать ASP.NET Core разработчиком?
Попалось хор краткое обзорное видео 2016 года с таким названием - Что нужно знать, чтобы стать...

Какая разница между ASP .Net Core и ASP .Net Core MVC?
Какая разница между ASP .Net Core и ASP .Net Core MVC? Или я может что-то не так понял? И...

ASP.NET MVC 4,ASP.NET MVC 4.5 и ASP.NET MVC 5 большая ли разница между ними?
Начал во всю осваивать технологию,теперь хочу с книжкой посидеть и вдумчиво перебрать всё то что...

ASP.NET Core 2.2 Angular Windows аутентификация для IIS сервера
Не очень много знаю о реализации аутентификации для asp.net core, но у меня проект в котором...

Запуск asp.net mvc приложения на IIS 7.5 + MS SQL 2012
День добрый. Возникла такая проблема. Пару дней назад переехал с одного компа на другой, слил из...

Запуск asp.net mvc приложения на IIS 7.5 + MS SQL 2012
День добрый. Возникла такая проблема. Пару дней назад переехал с одного компа на другой, слил из...

ASP.NET Core: разный формат даты контроллера ASP.NET и AngularJS
Собственно, проблему пока еще не разруливал, но уже погуглил. Разный формат даты который использует...

ASP.NET MVC или ASP.NET Core
Добрый вечер, подскажите что лучшие изучать ASP.NET MVC или ASP.NET Core ? Как я понимаю ASP.NET...

Что выбрать ASP.NET или ASP.NET Core ?
Добрый день форумчане, хотелось бы услышать ваше мнение, какой из перечисленных фреймворков лучше...

ASP.NET Core или ASP.NET MVC
Здравствуйте После изучение основ c# я решил выбрать направление веб разработки. Подскажите какие...

Стоит ли учить asp.net, если скоро станет asp.net core?
Всем привет Если я правильно понимаю, лучше учить Core ?

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Музыка, написанная Искусственным Интеллектом
volvo 04.12.2025
Всем привет. Некоторое время назад меня заинтересовало, что уже умеет ИИ в плане написания музыки для песен, и, собственно, исполнения этих самых песен. Стихов у нас много, уже вышли 4 книги, еще 3. . .
От async/await к виртуальным потокам в Python
IndentationError 23.11.2025
Армин Ронахер поставил под сомнение async/ await. Создатель Flask заявляет: цветные функции - провал, виртуальные потоки - решение. Не threading-динозавры, а новое поколение лёгких потоков. Откат?. . .
Поиск "дружественных имён" СОМ портов
Argus19 22.11.2025
Поиск "дружественных имён" СОМ портов На странице: https:/ / norseev. ru/ 2018/ 01/ 04/ comportlist_windows/ нашёл схожую тему. Там приведён код на С++, который показывает только имена СОМ портов, типа,. . .
Сколько Государство потратило денег на меня, обеспечивая инсулином.
Programma_Boinc 20.11.2025
Сколько Государство потратило денег на меня, обеспечивая инсулином. Вот решила сделать интересный приблизительный подсчет, сколько государство потратило на меня денег на покупку инсулинов. . . .
Ломающие изменения в C#.NStar Alpha
Etyuhibosecyu 20.11.2025
Уже можно не только тестировать, но и пользоваться C#. NStar - писать оконные приложения, содержащие надписи, кнопки, текстовые поля и даже изображения, например, моя игра "Три в ряд" написана на этом. . .
Мысли в слух
kumehtar 18.11.2025
Кстати, совсем недавно имел разговор на тему медитаций с людьми. И обнаружил, что они вообще не понимают что такое медитация и зачем она нужна. Самые базовые вещи. Для них это - когда просто люди. . .
Создание Single Page Application на фреймах
krapotkin 16.11.2025
Статья исключительно для начинающих. Подходы оригинальностью не блещут. В век Веб все очень привыкли к дизайну Single-Page-Application . Быстренько разберем подход "на фреймах". Мы делаем одну. . .
Фото: Daniel Greenwood
kumehtar 13.11.2025
Расскажи мне о Мире, бродяга
kumehtar 12.11.2025
— Расскажи мне о Мире, бродяга, Ты же видел моря и метели. Как сменялись короны и стяги, Как эпохи стрелою летели. - Этот мир — это крылья и горы, Снег и пламя, любовь и тревоги, И бескрайние. . .
PowerShell Snippets
iNNOKENTIY21 11.11.2025
Модуль PowerShell 5. 1+ : Snippets. psm1 У меня модуль расположен в пользовательской папке модулей, по умолчанию: \Documents\WindowsPowerShell\Modules\Snippets\ А в самом низу файла-профиля. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru