Docker و Image کوچک برای .NET
برنامه را با همه چیزهایی که لازم دارد در یک Image بستهبندی کن تا همه جا یکسان اجرا شود. با Multi-stage build و ترتیب درست لایهها، Image کوچک، امن و سریع ساخته میشود.
نویسنده: bezzad
مشکل: «روی سیستم من کار میکند»
فروشگاه اینترنتی ما یک سرویس سفارش دارد. اسم پروژه Shop.Orders.Api است. برنامهنویس آن را روی لپتاپ خودش اجرا میکند و همه چیز درست است. ولی روی سرور خطا میدهد.
چرا؟ چون دو محیط با هم فرق دارند:
- نسخه runtime داتنت روی سرور قدیمیتر است.
- یک کتابخانه سیستمی روی سرور نصب نیست.
- تنظیم منطقه زمانی یا زبان سیستم فرق دارد.
هر بار که سرور جدید اضافه میکنیم، باید همه اینها را دستی درست کنیم. این کار کند است و خطا دارد.
ایده: برنامه و محیطش را با هم بستهبندی کن
با Docker، برنامه را همراه همه چیزهایی که لازم دارد در یک بسته میگذاریم. به این بسته Image میگوییم. وقتی Image اجرا میشود، اسمش Container است.
- یک Image مثل یک قالب است. ثابت است و عوض نمیشود.
- هر Container یک نسخه در حال اجرا از آن قالب است. از یک Image میشود ده Container ساخت.
پس همان Image که روی لپتاپ تست شد، روی سرور هم اجرا میشود. محیط دیگر فرق ندارد.
فرق کانتینر با ماشین مجازی
- هر ماشین مجازی یک سیستمعامل کامل دارد. پس حافظه زیادی میگیرد و دیر روشن میشود.
- کانتینرها هسته سیستمعامل میزبان را شریک میشوند. هر کانتینر فقط برنامه و کتابخانههایش را دارد.
- نتیجه. کانتینر سبکتر است و سریعتر بالا میآید. برای همین Kubernetes و سرویسهای ابری با کانتینر کار میکنند.
لایهها: چرا ترتیب دستورها مهم است
فایل Dockerfile دستور ساخت Image است. هر دستور در آن یک لایه (Layer) میسازد. لایهها روی هم قرار میگیرند.
نکته مهم این است که Docker لایهها را cache میکند:
- ابزار Docker دستورها را از بالا به پایین بررسی میکند.
- اگر دستور و فایلهای ورودی آن عوض نشده باشد، Docker لایه را از cache برمیدارد.
- اولین لایهای که عوض شده باشد، دوباره ساخته میشود.
- همه لایههای بعد از آن هم دوباره ساخته میشوند، حتی اگر خودشان تغییری نداشته باشند.
حالا ببین چرا ترتیب مهم است. دانلود پکیجهای NuGet (دستور restore) کند است. فایلهای کد را چند بار در روز عوض میکنیم، ولی فایل پروژه (csproj) کمتر عوض میشود. پس:
- ترتیب خوب. اول فقط فایل پروژه را کپی کن و restore را اجرا کن. بعد بقیه کد را کپی کن.
- ترتیب بد. اول همه کد را کپی کن و بعد restore را اجرا کن. هر تغییر کوچک کد، cache پکیجها را خراب میکند.
مثال زنده
یک ترتیب را انتخاب کن. بعد یک تغییر بده و ببین کدام لایهها از cache میآیند:
ساخت Image کوچک با Multi-stage build
برای ساختن برنامه به SDK داتنت نیاز داریم: کامپایلر، ابزارها و پکیجها. ولی برای اجرای برنامه فقط runtime لازم است. اما Image مخصوص SDK خیلی بزرگتر از Image مخصوص runtime است.
در Multi-stage build، یک Dockerfile چند مرحله دارد:
- مرحله ساخت. روی Image مربوط به SDK، کد را restore و publish میکنیم.
- مرحله نهایی. روی Image کوچک aspnet شروع میکنیم. فقط خروجی publish را از مرحله قبل کپی میکنیم.
- نتیجه. کامپایلر، کد منبع و فایلهای موقت هیچ وقت وارد Image نهایی نمیشوند.
چرا Image کوچک مهم است؟
- سرعت. هر سرور جدید باید Image را دانلود کند. Image کوچک یعنی بالا آمدن سریعتر pod در زمان شلوغی.
- امنیت. هر ابزار اضافه یک راه نفوذ ممکن است. چیزی که در Image نیست، آسیبپذیری هم ندارد.
- هزینه. فضای ذخیره و ترافیک شبکه کمتر میشود.
کد
فایل 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
قانونهای مهم
- تنظیمات از بیرون بیاید. رشته اتصال دیتابیس را داخل Image نگذار. با متغیر محیطی بده، مثلاً ConnectionStrings__Orders. پس یک Image برای همه محیطها (تست و Production) کار میکند.
- رمز داخل Image نگذار. هر لایه برای همیشه در Image میماند. اگر در یک لایه فایل رمز را کپی کنی و در لایه بعد پاکش کنی، هنوز در لایه قبلی هست و هر کسی Image را داشته باشد آن را میبیند.
- کانتینر موقت است. هر وقت کانتینر از نو ساخته شود، فایلهای داخلش از بین میروند. فایل آپلودی مشتری را در یک فضای ذخیره بیرونی بگذار.
- لاگ را در خروجی استاندارد بنویس. فایل لاگ داخل کانتینر را کسی نمیبیند. ابزارهای کانتینر خروجی استاندارد را جمع میکنند.
- یک برنامه در هر کانتینر. سرویس سفارش و دیتابیس را در یک کانتینر نگذار. هر کدام جدا بالا میآید، جدا scale میشود و جدا خراب میشود.
- نسخه مشخص بگذار. برچسب latest را برای deploy استفاده نکن. هر Image را با یک نسخه یا شناسه commit برچسب بزن تا بدانی دقیقاً چه چیزی اجرا میشود و بتوانی برگردی.
اشتباههای رایج
| اشتباه | نتیجه | راه درست |
|---|---|---|
| یک مرحله با 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 ندارد و یک سرور ساده کافی است.
خلاصه در شش خط
- یک Image برنامه را با همه وابستگیهایش بستهبندی میکند و همه جا یکسان اجرا میشود.
- کانتینر از ماشین مجازی سبکتر است، چون هسته سیستمعامل را شریک میشود.
- هر دستور یک لایه است. اولین لایه تغییرکرده و همه لایههای بعدش دوباره ساخته میشوند.
- اول فایل پروژه و restore، بعد بقیه کد. اینطور cache پکیجها حفظ میشود.
- با Multi-stage build فقط خروجی publish روی Image کوچک aspnet میرود.
- رمز در Image نگذار، با کاربر غیر root اجرا کن و Image را با نسخه مشخص برچسب بزن.