← All tutorials

بسته‌بندی پروژه دات‌نت با Docker

راهنمای عملی نوشتن Dockerfile برای دات‌نت — همراه با nuget.config خصوصی

بنر Docker برای دات‌نت: سه مرحله Source، Build و Runtime در یک ایمیج چندمرحله‌ای.
SDK فقط برای ساخت می‌ماند و ایمیج نهایی فقط برنامه را اجرا می‌کند.

در این آموزش چه یاد می‌گیریم؟

در این آموزش یک پروژه واقعی Web API روی .NET 8 را به یک ایمیج Docker تبدیل می‌کنیم: ایمیجی تمیز، کوچک و آماده‌ی production که با ساخت چندمرحله‌ای (multi-stage build) ساخته می‌شود. یکی از رایج‌ترین دردسرهای تیم‌ها را هم حل می‌کنیم: بازیابی (restore) پکیج‌های NuGet از فیدهای خصوصی (مثل Azure Artifacts، GitHub Packages یا Nexus/BaGet داخلی) درون کانتینر، با کمک فایل nuget.config.

در پایان یک Dockerfile، یک .dockerignore، یک nuget.config و یک docker-compose.yml خواهید داشت — به‌علاوه‌ی دستورهای ساخت و اجرای امن.

۱. چرا Docker برای دات‌نت؟

Docker برنامه را همراه با همان runtime موردنیاز بسته‌بندی می‌کند، تا روی لپ‌تاپ شما، سیستم همکار و سرور production یکسان اجرا شود. برای دات‌نت این موضوع به‌خصوص مفید است چون:

  • دیگر «روی سیستم من کار می‌کند» نداریم — نسخه‌ی SDK/runtime داخل ایمیج ثابت می‌ماند.
  • ایمیج نهایی فقط runtime را دارد، نه کل SDK — پس کوچک‌تر می‌ماند.
  • خطوط CI/CD یک‌بار می‌سازند و همان خروجی را همه‌جا مستقر (deploy) می‌کنند.
از ایمیج‌های رسمی مایکروسافت استفاده می‌کنیم: mcr.microsoft.com/dotnet/sdk برای ساخت و mcr.microsoft.com/dotnet/aspnet برای اجرا.

۲. ساختار پروژه

یک solution معمول Web API را در نظر بگیرید:

MyApp/
├─ src/
│  ├─ MyApp.Api/
│  │  ├─ MyApp.Api.csproj
│  │  └─ Program.cs
│  ├─ MyApp.Application/
│  │  └─ MyApp.Application.csproj
│  └─ MyApp.Domain/
│     └─ MyApp.Domain.csproj
├─ nuget.config
├─ .dockerignore
├─ Dockerfile
└─ MyApp.sln

فایل‌های Dockerfile، nuget.config و .dockerignore را در ریشه‌ی مخزن قرار دهید تا زمینه‌ی ساخت Docker کل solution باشد.

۳. فایل nuget.config چیست و چرا؟

فایل nuget.config به ابزارهای دات‌نت می‌گوید پکیج‌ها را از کجا دانلود کند. به‌صورت پیش‌فرض فقط فید عمومی (nuget.org) را می‌شناسد. به‌محض اینکه تیم شما از یک فید خصوصی استفاده کند، باید آن را اینجا تعریف کنید — وگرنه dotnet restore درون Docker با خطای «package not found» یا «401 Unauthorized» شکست می‌خورد.

۳.۱ فقط فید عمومی

ساده‌ترین پیکربندی — منبع عمومی را صریح مشخص کنید تا ساخت قابل تکرار باشد:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
  </packageSources>
</configuration>
تگ <clear /> هر منبعی که از پیکربندی سطح سیستم به‌ارث رسیده را حذف می‌کند، تا کانتینر فقط از منابعی که شما تعریف کرده‌اید استفاده کند. این برای ساخت‌های قابل پیش‌بینی در CI مهم است.

۳.۲ افزودن فید خصوصی

حالا فید خصوصی و اطلاعات احراز هویت آن را اضافه کنید. هرگز توکن واقعی را در فایلی که commit می‌شود هاردکد نکنید. به‌جای آن از یک جای‌نگهدار (placeholder) متغیر محیطی استفاده کنید که NuGet هنگام restore آن را جایگزین می‌کند:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
    <add key="my-private" value="https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json" />
  </packageSources>

  <packageSourceCredentials>
    <my-private>
      <add key="Username" value="myorg" />
      <add key="ClearTextPassword" value="%NUGET_TOKEN%" />
    </my-private>
  </packageSourceCredentials>
</configuration>
نام عنصر زیر <packageSourceCredentials> باید دقیقاً با key منبع یکی باشد (اینجا my-private). هر فاصله در key به _x0020_ تبدیل می‌شود — پس key ها را ساده و بدون فاصله نگه دارید.

مقدار %NUGET_TOKEN% از یک متغیر محیطی خوانده می‌شود. در بخش‌های بعد این مقدار را به‌صورت یک secret ساخت به Docker می‌دهیم — تا توکن هرگز در تاریخچه‌ی ایمیج باقی نماند.

۴. فایل .dockerignore

قبل از نوشتن Dockerfile، فایل .dockerignore را اضافه کنید. این فایل زمینه‌ی ساخت را کوچک و سریع نگه می‌دارد و — مهم‌تر — از کپی شدن خروجی ساخت محلی و فایل‌های حساس به داخل ایمیج جلوگیری می‌کند.

# build output
**/bin/
**/obj/
# IDE / VCS
.vs/
.git/
.gitignore
# docker
Dockerfile
.dockerignore
# misc
**/*.user
**/appsettings.Development.json
نادیده گرفتن bin/ و obj/ بزرگ‌ترین مزیت است — وگرنه buildهای قدیمی محلی کپی می‌شوند و می‌توانند کش لایه‌ها را خراب کنند یا مسیرهای مخصوص سیستم را لو دهند.

۵. نوشتن Dockerfile

۵.۱ ساخت چندمرحله‌ای (Multi-stage)

رمز کوچک‌بودن ایمیج، دو مرحله است: یک مرحله‌ی build که SDK کامل دارد و restore/publish را انجام می‌دهد، و یک مرحله‌ی runtime که فقط خروجی publish‌شده را روی ایمیج سبک ASP.NET کپی می‌کند.

# ---------- Build stage ----------
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# ابتدا csproj و nuget.config را کپی کنید تا کش لایه‌ها بهینه شود
COPY nuget.config ./
COPY src/MyApp.Api/MyApp.Api.csproj            src/MyApp.Api/
COPY src/MyApp.Application/MyApp.Application.csproj  src/MyApp.Application/
COPY src/MyApp.Domain/MyApp.Domain.csproj      src/MyApp.Domain/

# restore با استفاده از nuget.config در ریشه
RUN dotnet restore src/MyApp.Api/MyApp.Api.csproj --configfile nuget.config

# حالا بقیه‌ی سورس را کپی و publish کنید
COPY . .
RUN dotnet publish src/MyApp.Api/MyApp.Api.csproj \
    -c Release -o /app/publish --no-restore

# ---------- Runtime stage ----------
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app/publish .

EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "MyApp.Api.dll"]
چرا ابتدا فایل‌های csproj را کپی می‌کنیم؟ Docker هر لایه را cache می‌کند. اگر سورس تغییر کند ولی وابستگی‌ها ثابت بمانند، Docker لایه‌ی restore کش‌شده را دوباره استفاده می‌کند و از دانلود مجدد پکیج‌ها صرف‌نظر می‌کند — زمان ساخت به‌شدت کم می‌شود.

۵.۲ عبور امن توکن فید

فید خصوصی به NUGET_TOKEN نیاز دارد. راه اشتباه: استفاده از ARG NUGET_TOKEN + ENV، چون آرگومان‌های ساخت در تاریخچه‌ی ایمیج باقی می‌مانند و بعداً قابل مشاهده هستند. راه درست: استفاده از BuildKit secrets که مقدار را فقط برای یک RUN mount می‌کند و ذخیره نمی‌کند:

# syntax=docker/dockerfile:1.7
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

COPY nuget.config ./
COPY src/MyApp.Api/MyApp.Api.csproj src/MyApp.Api/
# ... کپی بقیه‌ی فایل‌های csproj ...

# توکن را فقط برای همین RUN به‌صورت secret mount کنید
RUN --mount=type=secret,id=nuget_token \
    NUGET_TOKEN="$(cat /run/secrets/nuget_token)" \
    dotnet restore src/MyApp.Api/MyApp.Api.csproj --configfile nuget.config

COPY . .
RUN dotnet publish src/MyApp.Api/MyApp.Api.csproj -c Release -o /app/publish --no-restore

خط اول # syntax=docker/dockerfile:1.7 برای فعال‌شدن قابلیت secret mount لازم است.

هرگز echo $NUGET_TOKEN اجرا نکنید و توکن را در یک RUN که cache می‌شود رها نکنید. با --mount=type=secret مقدار فقط در حافظه و فقط برای همان یک دستور زنده است.

۵.۳ منطق کش لایه‌ها در Docker

هر خط در Dockerfile (FROM، COPY، RUN و …) یک لایه (layer) می‌سازد. هنگام docker build، Docker از بالا به پایین فایل را می‌خواند و برای هر لایه می‌پرسد: «آیا نتیجه‌ی دقیقاً همین مرحله را از ساخت قبلی دارم؟» اگر بله، لایه‌ی کش‌شده را دوباره استفاده می‌کند و آن مرحله را اجرا نمی‌کند.

قانون اصلی

Docker یک لایه را فقط وقتی cache می‌کند که هر سه شرط برقرار باشد:

  • متن دستور دقیقاً همان باشد (مثلاً همان RUN dotnet restore …).
  • همه‌ی لایه‌های بالای آن هم از cache آمده باشند (زنجیره باید سالم بماند).
  • برای COPY/ADD: محتوای فایل‌های کپی‌شده همان checksum قبلی را داشته باشد.
اگر یک لایه دوباره ساخته شود، همه‌ی لایه‌های پایین‌تر هم باطل می‌شوند — حتی اگر ورودی خودشان تغییر نکرده باشد. ترتیب خطوط مهم است.

در Dockerfile ما چه اتفاقی می‌افتد؟

دستورچه زمانی cache می‌شودچه زمانی cache می‌شکند
FROM … AS buildهمان تگ ایمیج پایه (مثلاً sdk:8.0) قبلاً pull شدهتگ را عوض کنید یا digest جدید برای همان تگ بیاید
WORKDIR /srcتقریباً همیشه (مگر لایه‌ی بالایی rebuild شده باشد)لایه‌ی بالایی rebuild شده باشد
COPY nuget.configمحتوای nuget.config تغییر نکردهفید اضافه/حذف کنید یا placeholder احراز هویت را عوض کنید
COPY … .csprojهیچ‌کدام از فایل‌های .csproj کپی‌شده تغییر نکردهPackageReference جدید یا TargetFramework عوض شود
RUN dotnet restoreهمه‌ی لایه‌های بالا cache شده و دستور restore همان باشدهر تغییر در .csproj یا nuget.config — پکیج‌ها دوباره دانلود می‌شوند
COPY . .هیچ فایلی در زمینه‌ی ساخت از ساخت قبلی عوض نشدههر فایلی عوض شود (حتی یک کامنت در .cs) — با ویرایش سورس طبیعی است
RUN dotnet publishCOPY . . cache شده (هیچ تغییری در سورس نبوده)هر تغییر سورس — compile کامل دوباره اجرا می‌شود
FROM … AS finalایمیج runtime پایه تغییر نکردهتگ/digest جدید aspnet
COPY --from=build …مرحله‌ی ساخت همان خروجی publish را تولید کردهلایه‌ی publish بالایی تغییر کرده

دو سناریوی عملی

سناریو A — فقط Program.cs را عوض می‌کنید:

  • FROM → WORKDIR → COPY nuget.config → COPY csproj → RUN restore → همه cache ✅
  • COPY . . → cache miss (سورس عوض شده) → دوباره اجرا می‌شود
  • RUN publish → دوباره اجرا می‌شود (لایه‌ی بالایی rebuild شده)

نتیجه: پکیج‌های NuGet دوباره دانلود نمی‌شوند. فقط compile/publish اجرا می‌شود — معمولاً خیلی سریع‌تر.

سناریو B — یک پکیج به MyApp.Api.csproj اضافه می‌کنید:

  • COPY … .csproj → cache miss (محتوای فایل عوض شده)
  • RUN restore → دوباره اجرا، پکیج جدید دانلود می‌شود
  • COPY . . و RUN publish → هر دو دوباره اجرا می‌شوند

نتیجه: منطقی است — وابستگی‌ها عوض شده، پس restore باید دوباره اجرا شود.

چرا COPY . . باید بعد از restore بیاید

اگر COPY . . را قبل از dotnet restore بگذارید، هر ویرایش سورس (حتی اصلاح یک کامنت) restore را باطل می‌کند و NuGet مجبور می‌شود همه‌چیز را دوباره دانلود کند. جدا کردن «اول فایل‌های وابستگی، بعد کل سورس» مهم‌ترین بهینه‌سازی Dockerfile برای دات‌نت است.

چیزهایی که مثل تصور معمول cache نمی‌شوند

  • مقدار ARG — تغییر آرگومان ساخت همه‌ی لایه‌های بعد از خط ARG که از آن استفاده می‌کند را باطل می‌کند.
  • Timestamp داخل RUN — دستوراتی مثل RUN date که هر بار خروجی متفاوت دارند، cache پایدار نمی‌گیرند.
  • فایل‌های حذف‌شده با .dockerignore — اصلاً به Docker فرستاده نمی‌شوند، پس نمی‌توانند COPY . . را بشکنند؛ یک دلیل دیگر اهمیت .dockerignore.
  • docker build --no-cache — همه‌ی لایه‌ها را از نو می‌سازد و cache را نادیده می‌گیرد.
چک سریع هنگام ساخت: وقتی docker build می‌زنید، خطوطی که با CACHED تمام می‌شوند از cache آمده‌اند؛ بقیه تازه اجرا شده‌اند. از همین خروجی می‌فهمید cache دقیقاً کجا شکسته.

۶. docker-compose

برای توسعه‌ی محلی همراه با دیتابیس، یک فایل compose همه‌چیز را به هم وصل می‌کند:

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ConnectionStrings__Default=Server=db;Database=myapp;User=sa;Password=Your_password123;TrustServerCertificate=True
    depends_on:
      - db

  db:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=Your_password123
    ports:
      - "1433:1433"
دات‌نت ConnectionStrings__Default (با دو زیرخط) را به کلید تودرتوی ConnectionStrings:Default نگاشت می‌کند — این‌طور می‌توانید appsettings را با متغیر محیطی override کنید.

۷. ساخت و اجرا

ساخت با توکن فید خصوصی (BuildKit secret)

# مطمئن شوید BuildKit فعال است (در Docker جدید پیش‌فرض روشن است)
export DOCKER_BUILDKIT=1

# توکن را از متغیر محیطی به secret بدهید
echo "$NUGET_TOKEN" | docker build \
  --secret id=nuget_token,src=/dev/stdin \
  -t myapp:latest .

یا از روی یک فایل

docker build --secret id=nuget_token,src=./token.txt -t myapp:latest .

اجرای کانتینر

docker run --rm -p 8080:8080 myapp:latest

یا با compose

docker compose up --build

سپس آدرس http://localhost:8080/swagger را برای تست API باز کنید.

دستورکارش چیست
docker build -t myapp .ساخت ایمیج
docker run -p 8080:8080 myappاجرای یک کانتینر
docker imagesفهرست ایمیج‌های محلی
docker compose up --buildساخت و اجرای کل استک
docker system pruneپاک‌سازی داده/کش بلااستفاده

۸. بهترین روش‌ها

  • ساخت چندمرحله‌ای ایمیج نهایی را فقط در حد runtime نگه می‌دارد — اغلب ۲۰۰ تا ۲۲۰ مگابایت به‌جای بیش از ۸۰۰ مگابایت.
  • اول csproj را کپی کنید، سپس restore، بعد بقیه را کپی کنید تا بیشترین استفاده از کش لایه‌ها را داشته باشید.
  • هرگز secret را داخل ARG/ENV قرار ندهید؛ از BuildKit --mount=type=secret استفاده کنید.
  • تگ ایمیج پایه را ثابت نگه دارید (مثلاً 8.0 یا یک digest) تا ساخت قابل تکرار باشد.
  • در production با کاربر non-root اجرا کنید — ایمیج‌های رسمی از USER $APP_UID در دات‌نت ۸ به بالا پشتیبانی می‌کنند.
  • همیشه .dockerignore اضافه کنید تا زمینه‌ی ساخت سبک بماند و bin/obj لو نرود.
  • روی publish از --no-restore استفاده کنید چون قبلاً restore انجام شده — هم سریع‌تر است هم از خطاهای غیرمنتظره جلوگیری می‌کند.

جمع‌بندی نهایی

  • nuget.config منابع پکیج را تعریف می‌کند؛ برای فید خصوصی از <clear/> + جای‌نگهدار متغیر محیطی استفاده کنید.
  • .dockerignore پوشه‌های bin/obj و secretها را از زمینه‌ی ساخت بیرون نگه می‌دارد.
  • Dockerfile = چندمرحله‌ای: ایمیج SDK می‌سازد و publish می‌کند، ایمیج runtime ASP.NET اجرا می‌کند.
  • BuildKit secrets توکن NuGet را بدون لو رفتن در تاریخچه‌ی ایمیج عبور می‌دهد.
  • docker-compose برای توسعه‌ی محلی، API را به دیتابیس وصل می‌کند.

حالا برنامه‌ی دات‌نت شما قابل‌حمل، قابل‌تکرار و امن برای انتشار است. 🐳