مقالات › ابزار و DevOps

آموزش انتشار ASP.NET Core روی IIS در ویندوز سرور؛ قدم‌به‌قدم

انتشار ASP.NET Core و React روی IIS: نصب Hosting Bundle، Publish، Application Pool، گواهی HTTPS، مسیریابی SPA و رفع خطاهای 500.30 و 500.19.

بسیاری از شرکت‌ها — به‌خصوص در ایران — برنامه‌هایشان را روی سرورهای ویندوزی با IIS میزبانی می‌کنند. ASP.NET Core روی IIS بسیار خوب اجرا می‌شود، اما راه‌اندازی آن با دوران ASP.NET Framework فرق دارد: IIS نقش سرور جلویی را دارد و درخواست‌ها را از طریق ASP.NET Core Module (ANCM) به برنامه‌ی .NET شما می‌دهد. این راهنما یک استقرار کامل و آماده‌ی تولید را قدم‌به‌قدم شرح می‌دهد.

ASP.NET Core چطور روی IIS اجرا می‌شود

دو مدل میزبانی وجود دارد:

مدل نحوه‌ی کار توضیح
In-Process (پیش‌فرض) برنامه داخل پروسه‌ی Worker خود IIS (w3wp.exe) اجرا می‌شود سریع‌ترین؛ یک برنامه در هر Application Pool
Out-of-Process IIS درخواست‌ها را به برنامه‌ای که با Kestrel در پروسه‌ی جدا اجرا می‌شود پراکسی می‌کند برای جداسازی یا موارد خاص

در هر دو حالت، ماژول ASP.NET Core — که با Hosting Bundle نصب می‌شود — رابط بین IIS و .NET است.

گام ۱: آماده‌سازی سرور

  1. IIS را فعال کنید: Server Manager ← Add Roles and Features ← Web Server (IIS). اگر از SignalR استفاده می‌کنید WebSocket Protocol را هم انتخاب کنید.
  2. .NET Hosting Bundle را دقیقاً برای نسخه‌ی اصلی برنامه‌تان (مثلاً .NET 8) از صفحه‌ی رسمی دانلود .NET نصب کنید. این بسته Runtime و ماژول ASP.NET Core را نصب می‌کند.
  3. IIS را ری‌استارت کنید تا ماژول شناسایی شود:
net stop was /y
net start w3svc

هشدار: Hosting Bundle را بعد از فعال کردن IIS نصب کنید. اگر قبلاً نصبش کرده‌اید، نصب‌کننده را دوباره اجرا و گزینه‌ی Repair را انتخاب کنید؛ وگرنه ماژول ثبت نمی‌شود و خطای HTTP 500.19 می‌گیرید.

گام ۲: Publish برنامه

dotnet publish -c Release -o ./publish

گزینه‌های مهم:

  • وابسته به Framework (پیش‌فرض): خروجی کوچک و استفاده از Runtime نصب‌شده روی سرور.
  • Self-Contained (--self-contained -r win-x64): Runtime داخل خروجی است و نیازی به نصب آن نیست، اما حجم بیشتر است.

اگر فرانت‌اند React توسط همین برنامه سرو می‌شود، قبل از Publish آن را Build و خروجی را در wwwroot کپی کنید:

cd frontend && npm ci && npm run build
cp -r dist/* ../backend/wwwroot/
cd ../backend && dotnet publish -c Release -o ./publish

خروجی Publish یک web.config تولیدشده دارد:

<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="false" stdoutLogFile=".\logs\stdout"
                  hostingModel="inprocess" />
    </system.webServer>
  </location>
</configuration>

گام ۳: ساخت سایت و Application Pool

  1. پوشه‌ی Publish را روی سرور کپی کنید؛ مثلاً C:\inetpub\sites\myapp.
  2. در IIS Manager ← Application Pools ← Add: نام MyAppPool، ‏.NET CLR version: No Managed Code (ASP.NET Core از یکپارچگی CLR در IIS استفاده نمی‌کند) و Pipeline از نوع Integrated.
  3. Sites ← Add Website: مسیر فیزیکی C:\inetpub\sites\myapp، Application Pool برابر MyAppPool و Binding با نام دامنه (مثلاً example.com روی پورت 80).

یا با اسکریپت:

Import-Module WebAdministration
New-WebAppPool -Name "MyAppPool"
Set-ItemProperty IIS:\AppPools\MyAppPool -Name managedRuntimeVersion -Value ""
Set-ItemProperty IIS:\AppPools\MyAppPool -Name startMode -Value "AlwaysRunning"
New-Website -Name "MyApp" -PhysicalPath "C:\inetpub\sites\myapp" -ApplicationPool "MyAppPool" -HostHeader "example.com" -Port 80

دسترسی پوشه‌ها

Application Pool با هویت IIS AppPool\MyAppPool اجرا می‌شود. روی پوشه‌ی سایت دسترسی خواندن و فقط روی پوشه‌هایی که برنامه در آن‌ها می‌نویسد — آپلودها، لاگ‌ها، دیتابیس SQLite — دسترسی تغییر بدهید:

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

نکته: دیتابیس و فایل‌های آپلودی کاربران را خارج از پوشه‌ی استقرار نگه دارید (مثلاً C:\ProgramData\MyApp) و از طریق پیکربندی به آن‌ها اشاره کنید. در این صورت استقرار مجدد هرگز داده‌ها را بازنویسی نمی‌کند.

گام ۴: پیکربندی و اطلاعات محرمانه

محیط به‌طور پیش‌فرض Production است. متغیرهای محیطی هر سایت را در web.config (یا Configuration Editor در IIS) تنظیم کنید:

<aspNetCore processPath="dotnet" arguments=".\MyApp.dll" hostingModel="inprocess">
  <environmentVariables>
    <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production" />
    <environmentVariable name="ConnectionStrings__Default" value="Server=.;Database=MyApp;Trusted_Connection=True;TrustServerCertificate=True" />
  </environmentVariables>
</aspNetCore>

برای تنظیمات غیرمحرمانه از appsettings.Production.json استفاده کنید. برای اطلاعات محرمانه، متغیرهای محیطی سطح ماشین یا یک Secret Store را به فایل‌های داخل پوشه‌ی سایت ترجیح دهید. اگر با احراز هویت ویندوز به SQL Server وصل می‌شوید، برای هویت Application Pool یک Login در SQL Server بسازید.

گام ۵: HTTPS

  1. یک گواهی بگیرید؛ از ارائه‌دهنده‌ی خود یا رایگان از Let's Encrypt با یک کلاینت ACME ویندوزی مثل win-acme که تمدید خودکار هم انجام می‌دهد.
  2. یک Binding از نوع https روی پورت 443 با گواهی اضافه کنید و اگر چند سایت یک IP مشترک دارند، Server Name Indication را فعال کنید.
  3. HTTP را به HTTPS هدایت و HSTS را در برنامه فعال کنید:
if (!app.Environment.IsDevelopment())
{
    app.UseHsts();
    app.UseHttpsRedirection();
}

گام ۶: مسیریابی SPA و فایل‌های استاتیک

وقتی React Router مسیرهایی مثل /fa/articles/my-post را مدیریت می‌کند، رفرش صفحه همان مسیر را از سرور می‌خواهد. برنامه باید برای مسیرهای ناشناخته‌ی غیر API فایل index.html را برگرداند:

app.UseDefaultFiles();
app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        // فایل‌های هش‌دار برای همیشه کش می‌شوند؛ index.html نباید کش شود
        if (ctx.File.Name != "index.html")
            ctx.Context.Response.Headers.CacheControl = "public,max-age=31536000,immutable";
    }
});
app.MapControllers();
app.MapFallbackToFile("index.html");

هشدار: Build ‏Vite ممکن است یک web.config جداگانه با قوانین URL Rewrite برای میزبانی استاتیک داشته باشد. وقتی فرانت‌اند توسط ASP.NET Core سرو می‌شود، آن را از wwwroot حذف کنید؛ یک web.config دوم داخل سایت می‌تواند با Handler ‏ASP.NET Core تداخل داشته باشد.

فشرده‌سازی پاسخ را فعال کنید (Dynamic Compression در IIS یا app.UseResponseCompression()) و مطمئن شوید نوع‌های MIME مدرن مثل .webp، .avif و .woff2 سرو می‌شوند (میان‌افزار فایل استاتیک ASP.NET Core آن‌ها را می‌شناسد).

گام ۷: همیشه گرم و پایدار

IIS به‌طور پیش‌فرض Application Poolهای بیکار را بعد از ۲۰ دقیقه متوقف می‌کند و بازدیدکننده‌ی بعدی منتظر شروع سرد می‌ماند. برای تولید:

  • Application Pool ← Start Mode: AlwaysRunning و Idle Time-out: 0.
  • سایت ← Preload Enabled: True (به قابلیت Application Initialization نیاز دارد).
  • بازیافت دوره‌ای پیش‌فرض (هر ۱۷۴۰ دقیقه) را غیرفعال کنید یا برای ساعت خلوت زمان‌بندی کنید.
  • برای SignalR قابلیت WebSocket Protocol را فعال کنید.

استقرار تقریباً بدون قطعی

جایگزینی DLLها هنگام اجرای برنامه شکست می‌خورد، چون فایل‌ها قفل‌اند. یک فایل app_offline.htm در پوشه‌ی سایت بگذارید — ANCM برنامه را با ظرافت متوقف و آن صفحه را نمایش می‌دهد — فایل‌های جدید را کپی و سپس آن را حذف کنید:

Copy-Item .\maintenance.htm C:\inetpub\sites\myapp\app_offline.htm
robocopy .\publish C:\inetpub\sites\myapp /MIR /XD uploads logs /XF app_offline.htm
Remove-Item C:\inetpub\sites\myapp\app_offline.htm

/MIR پوشه را آینه می‌کند و /XD پوشه‌های دارای داده‌ی زمان اجرا را مستثنا می‌کند. برای استقرار واقعاً بدون قطعی، دو سایت پشت ARR یا Load Balancer اجرا و بین آن‌ها جابه‌جا شوید.

عیب‌یابی

خطا علت معمول راه‌حل
500.19 ANCM نصب نیست یا web.config نامعتبر است نصب یا Repair ‏Hosting Bundle
500.30 — شکست شروع In-Process برنامه هنگام شروع کرش کرده (پیکربندی اشتباه، نبود دیتابیس، Exception در Program.cs) بررسی لاگ‌ها (پایین)
500.31 Runtime مورد نیاز .NET نصب نیست نصب نسخه‌ی درست Hosting Bundle
500.32 / 500.35 ناهمخوانی ۳۲/۶۴ بیتی / دو برنامه‌ی In-Process در یک Pool هماهنگی بیت؛ یک برنامه در هر Pool
502.5 برنامه‌ی Out-of-Process شروع نشد مثل 500.30
403.14 Directory Browsing؛ برنامه درخواست را مدیریت نمی‌کند Handler تعریف نشده؛ web.config را بررسی کنید

برای دیدن خطاهای شروع:

  1. Event Viewer ← Windows Logs ← Application با منبع IIS AspNetCore Module V2.
  2. موقتاً در web.config مقدار stdoutLogEnabled="true" بگذارید (و پوشه‌ی logs را با دسترسی نوشتن بسازید)، خطا را بازتولید کنید، لاگ را بخوانید و دوباره آن را خاموش کنید.
  3. برنامه را مستقیم روی سرور اجرا کنید: dotnet .\MyApp.dll؛ Exceptionها در کنسول چاپ می‌شوند.
  4. لاگ ساختاریافته (Serilog به فایل یا Seq) اضافه کنید تا مشکلات تولید بعد از شروع هم قابل مشاهده باشند.

جمع‌بندی

برای میزبانی ASP.NET Core روی IIS: IIS را فعال کنید، Hosting Bundle هم‌نسخه را نصب کنید، Publish بگیرید (اگر SPA را از همان برنامه سرو می‌کنید، Build ری‌اکت را در wwwroot بگذارید)، یک Application Pool از نوع No Managed Code بسازید و حداقل دسترسی پوشه را به آن بدهید، محیط و اطلاعات محرمانه را پیکربندی کنید، HTTPS اضافه کنید، از Fallback برای SPA استفاده کنید، Pool را همیشه در حال اجرا نگه دارید و با app_offline.htm مستقر کنید. وقتی مشکلی پیش آمد، Event Viewer و لاگ stdout علت را نشان می‌دهند.

پرسش‌های متداول

IIS یا لینوکس با Nginx؟

هر دو آماده‌ی تولیدند. اگر زیرساخت، SQL Server و تیم شما ویندوزی است IIS انتخاب طبیعی است؛ لینوکس با Nginx (یا کانتینر) برای ابر و میزبانی کم‌هزینه رایج است. کد برنامه یکسان است.

آیا می‌توان چند برنامه‌ی ASP.NET Core را روی یک سرور IIS میزبانی کرد؟

بله. به هر برنامه Application Pool جداگانه (برای میزبانی In-Process الزامی است) و سایت یا Virtual Application خودش را بدهید.

آیا باید .NET SDK را روی سرور نصب کنم؟

خیر. سرور فقط به Hosting Bundle (Runtime + ماژول) نیاز دارد. Build و Publish را روی سیستم توسعه یا CI انجام دهید.