Levelwise
فارسی
زیرساخت و DevOps

Docker و Image کوچک برای .NET

برنامه را با همه چیزهایی که لازم دارد در یک Image بسته‌بندی کن تا همه جا یکسان اجرا شود. با Multi-stage build و ترتیب درست لایه‌ها، Image کوچک، امن و سریع ساخته می‌شود.

بازبینی نشدهبا کمک AI نوشته شدهزمان خواندن: ۱۴ دقیقهمثال سرویس سفارش فروشگاه اینترنتیکد Dockerfile و .NET 10

نویسنده: bezzad

مشکل: «روی سیستم من کار می‌کند»

فروشگاه اینترنتی ما یک سرویس سفارش دارد. اسم پروژه Shop.Orders.Api است. برنامه‌نویس آن را روی لپ‌تاپ خودش اجرا می‌کند و همه چیز درست است. ولی روی سرور خطا می‌دهد.

چرا؟ چون دو محیط با هم فرق دارند:

  1. نسخه runtime دات‌نت روی سرور قدیمی‌تر است.
  2. یک کتابخانه سیستمی روی سرور نصب نیست.
  3. تنظیم منطقه زمانی یا زبان سیستم فرق دارد.

هر بار که سرور جدید اضافه می‌کنیم، باید همه این‌ها را دستی درست کنیم. این کار کند است و خطا دارد.

ایده: برنامه و محیطش را با هم بسته‌بندی کن

با Docker، برنامه را همراه همه چیزهایی که لازم دارد در یک بسته می‌گذاریم. به این بسته Image می‌گوییم. وقتی Image اجرا می‌شود، اسمش Container است.

  • یک Image مثل یک قالب است. ثابت است و عوض نمی‌شود.
  • هر Container یک نسخه در حال اجرا از آن قالب است. از یک Image می‌شود ده Container ساخت.

پس همان Image که روی لپ‌تاپ تست شد، روی سرور هم اجرا می‌شود. محیط دیگر فرق ندارد.

فرق کانتینر با ماشین مجازی

ماشین مجازیکانتینرOrders APICatalog APIPayment APIGuest OSGuest OSGuest OSHypervisorHost OSServerOrders APICatalog APIPayment APIContainer RuntimeHost OS (Linux kernel)Serverهر برنامه یک سیستم‌عامل کامل دارد: سنگین و کندهمه یک هسته مشترک دارند: سبک و سریع
ماشین مجازی برای هر برنامه یک سیستم‌عامل کامل دارد. کانتینرها فقط یک هسته مشترک دارند.
  1. هر ماشین مجازی یک سیستم‌عامل کامل دارد. پس حافظه زیادی می‌گیرد و دیر روشن می‌شود.
  2. کانتینرها هسته سیستم‌عامل میزبان را شریک می‌شوند. هر کانتینر فقط برنامه و کتابخانه‌هایش را دارد.
  3. نتیجه. کانتینر سبک‌تر است و سریع‌تر بالا می‌آید. برای همین Kubernetes و سرویس‌های ابری با کانتینر کار می‌کنند.
یک هزینه: چون هسته مشترک است، جداسازی کانتینرها از ماشین مجازی ضعیف‌تر است. پس برنامه را با کاربر root اجرا نکن (پایین‌تر می‌بینیم).

لایه‌ها: چرا ترتیب دستورها مهم است

فایل Dockerfile دستور ساخت Image است. هر دستور در آن یک لایه (Layer) می‌سازد. لایه‌ها روی هم قرار می‌گیرند.

نکته مهم این است که Docker لایه‌ها را cache می‌کند:

  1. ابزار Docker دستورها را از بالا به پایین بررسی می‌کند.
  2. اگر دستور و فایل‌های ورودی آن عوض نشده باشد، Docker لایه را از cache برمی‌دارد.
  3. اولین لایه‌ای که عوض شده باشد، دوباره ساخته می‌شود.
  4. همه لایه‌های بعد از آن هم دوباره ساخته می‌شوند، حتی اگر خودشان تغییری نداشته باشند.
لایه‌های مرحله ساختFROM sdk:10.0COPY *.csprojRUN dotnet restoreCOPY . .RUN dotnet publishترتیب ساخت: از پایین به بالافقط یک فایل کد عوض شددوباره ساخته می‌شودچون فایل‌های کد عوض شده‌انداز کش می‌آیدپکیج‌ها دوباره دانلود نمی‌شوند
فایل پروژه جدا و قبل از کد کپی شده است. پس با تغییر کد، دانلود پکیج‌ها از cache می‌آید.

حالا ببین چرا ترتیب مهم است. دانلود پکیج‌های NuGet (دستور restore) کند است. فایل‌های کد را چند بار در روز عوض می‌کنیم، ولی فایل پروژه (csproj) کمتر عوض می‌شود. پس:

  • ترتیب خوب. اول فقط فایل پروژه را کپی کن و restore را اجرا کن. بعد بقیه کد را کپی کن.
  • ترتیب بد. اول همه کد را کپی کن و بعد restore را اجرا کن. هر تغییر کوچک کد، cache پکیج‌ها را خراب می‌کند.

مثال زنده

یک ترتیب را انتخاب کن. بعد یک تغییر بده و ببین کدام لایه‌ها از cache می‌آیند:

لایه‌ها و cache در ساخت Image
یک ترتیب و بعد یک تغییر را انتخاب کن.

    ساخت Image کوچک با Multi-stage build

    برای ساختن برنامه به SDK دات‌نت نیاز داریم: کامپایلر، ابزارها و پکیج‌ها. ولی برای اجرای برنامه فقط runtime لازم است. اما Image مخصوص SDK خیلی بزرگ‌تر از Image مخصوص runtime است.

    در Multi-stage build، یک Dockerfile چند مرحله دارد:

    1. مرحله ساخت. روی Image مربوط به SDK، کد را restore و publish می‌کنیم.
    2. مرحله نهایی. روی Image کوچک aspnet شروع می‌کنیم. فقط خروجی publish را از مرحله قبل کپی می‌کنیم.
    3. نتیجه. کامپایلر، کد منبع و فایل‌های موقت هیچ وقت وارد Image نهایی نمی‌شوند.
    مرحله ساختFROM sdk:10.0 AS buildکامپایلر و ابزارهاپکیج‌های دانلودشدهکد منبعفایل‌های موقت ساختخروجی نهایی ساختبسته نهایی برای اجراFROM aspnet:10.0فقط محیط اجرافایل‌های برنامهCOPY --from=buildکامپایلر و کد منبع وارد بسته نهایی نمی‌شوند
    فقط خروجی publish از مرحله ساخت به Image نهایی می‌رود.

    چرا Image کوچک مهم است؟

    1. سرعت. هر سرور جدید باید Image را دانلود کند. Image کوچک یعنی بالا آمدن سریع‌تر pod در زمان شلوغی.
    2. امنیت. هر ابزار اضافه یک راه نفوذ ممکن است. چیزی که در Image نیست، آسیب‌پذیری هم ندارد.
    3. هزینه. فضای ذخیره و ترافیک شبکه کمتر می‌شود.

    کد

    فایل Dockerfile برای سرویس سفارش

    # Stage 1: build with the full SDK
    FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
    WORKDIR /src
    
    # Copy only the project file first, so restore is cached
    COPY Shop.Orders.Api/Shop.Orders.Api.csproj Shop.Orders.Api/
    RUN dotnet restore Shop.Orders.Api/Shop.Orders.Api.csproj
    
    # Now copy the rest of the code and publish
    COPY . .
    RUN dotnet publish Shop.Orders.Api/Shop.Orders.Api.csproj \
        -c Release -o /app/publish --no-restore
    
    # Stage 2: small runtime image
    FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
    WORKDIR /app
    COPY --from=build /app/publish .
    
    # Do not run as root. The .NET images define this user.
    USER $APP_UID
    EXPOSE 8080
    ENTRYPOINT ["dotnet", "Shop.Orders.Api.dll"]

    چند نکته درباره این فایل:

    • از .NET 8 به بعد، Image های رسمی ASP.NET Core به طور پیش‌فرض روی پورت 8080 گوش می‌دهند، نه پورت 80. دلیلش این است که برنامه بدون root بتواند اجرا شود.
    • متغیر APP_UID در Image های رسمی تعریف شده است و به یک کاربر معمولی (غیر root) اشاره می‌کند.
    • در .NET 10، Image های پیش‌فرض بر پایه Ubuntu هستند. نسخه‌های کوچک‌تری به اسم chiseled هم وجود دارد که shell و package manager ندارند.

    فایل dockerignore

    بدون این فایل، دستور «COPY . .» پوشه‌های bin و obj لپ‌تاپ را هم داخل Image می‌برد. این کار cache را بی‌دلیل خراب می‌کند و ممکن است فایل‌های اشتباه را وارد Image کند.

    **/bin/
    **/obj/
    .git/
    .vs/
    **/*.user
    **/appsettings.Development.json

    راه دیگر: بدون Dockerfile

    ابزار dotnet خودش هم می‌تواند Image بسازد. این راه برای پروژه‌های ساده خوب است:

    dotnet publish Shop.Orders.Api -c Release /t:PublishContainer

    قانون‌های مهم

    1. تنظیمات از بیرون بیاید. رشته اتصال دیتابیس را داخل Image نگذار. با متغیر محیطی بده، مثلاً ConnectionStrings__Orders. پس یک Image برای همه محیط‌ها (تست و Production) کار می‌کند.
    2. رمز داخل Image نگذار. هر لایه برای همیشه در Image می‌ماند. اگر در یک لایه فایل رمز را کپی کنی و در لایه بعد پاکش کنی، هنوز در لایه قبلی هست و هر کسی Image را داشته باشد آن را می‌بیند.
    3. کانتینر موقت است. هر وقت کانتینر از نو ساخته شود، فایل‌های داخلش از بین می‌روند. فایل آپلودی مشتری را در یک فضای ذخیره بیرونی بگذار.
    4. لاگ را در خروجی استاندارد بنویس. فایل لاگ داخل کانتینر را کسی نمی‌بیند. ابزارهای کانتینر خروجی استاندارد را جمع می‌کنند.
    5. یک برنامه در هر کانتینر. سرویس سفارش و دیتابیس را در یک کانتینر نگذار. هر کدام جدا بالا می‌آید، جدا scale می‌شود و جدا خراب می‌شود.
    6. نسخه مشخص بگذار. برچسب latest را برای deploy استفاده نکن. هر Image را با یک نسخه یا شناسه commit برچسب بزن تا بدانی دقیقاً چه چیزی اجرا می‌شود و بتوانی برگردی.
    حافظه در کانتینر: سیستم GC دات‌نت سقف حافظه کانتینر را می‌بیند و خودش را با آن تنظیم می‌کند. پس اگر برای کانتینر سقف حافظه می‌گذاری، آن را بر اساس مصرف واقعی برنامه انتخاب کن.

    اشتباه‌های رایج

    اشتباه نتیجه راه درست
    یک مرحله با Image مربوط به SDK Image خیلی بزرگ است و کامپایلر در Production هست. Multi-stage build.
    کپی همه کد قبل از restore هر تغییر کوچک، همه پکیج‌ها را دوباره دانلود می‌کند. اول فایل پروژه، بعد restore، بعد بقیه کد.
    نداشتن فایل dockerignore پوشه‌های bin و obj و حتی فایل‌های محرمانه وارد Image می‌شوند. این پوشه‌ها را در dockerignore بگذار.
    رمز یا رشته اتصال در Image هر کسی Image را داشته باشد، رمز را دارد. متغیر محیطی یا سرویس مدیریت رمز.
    اجرا با کاربر root اگر برنامه هک شود، نفوذگر دسترسی بیشتری دارد. دستور USER با کاربر غیر root.
    برچسب latest در deploy معلوم نیست کدام نسخه اجرا شده و برگشت سخت است. برچسب با نسخه یا شناسه commit.

    چه وقت Docker؟

    مناسب

    • سرویس وب یا Worker که روی چند سرور یا در Kubernetes اجرا می‌شود.
    • تیم می‌خواهد محیط توسعه، تست و Production یکسان باشد.
    • اجرای وابستگی‌ها مثل دیتابیس و Redis برای تست روی لپ‌تاپ یا در CI.

    کم‌فایده

    • یک ابزار کوچک که فقط روی یک دسکتاپ ویندوز اجرا می‌شود.
    • تیم هیچ زیرساختی برای ساخت و نگهداری Image ندارد و یک سرور ساده کافی است.

    خلاصه در شش خط

    1. یک Image برنامه را با همه وابستگی‌هایش بسته‌بندی می‌کند و همه جا یکسان اجرا می‌شود.
    2. کانتینر از ماشین مجازی سبک‌تر است، چون هسته سیستم‌عامل را شریک می‌شود.
    3. هر دستور یک لایه است. اولین لایه تغییرکرده و همه لایه‌های بعدش دوباره ساخته می‌شوند.
    4. اول فایل پروژه و restore، بعد بقیه کد. این‌طور cache پکیج‌ها حفظ می‌شود.
    5. با Multi-stage build فقط خروجی publish روی Image کوچک aspnet می‌رود.
    6. رمز در Image نگذار، با کاربر غیر root اجرا کن و Image را با نسخه مشخص برچسب بزن.