بستهبندی پروژه داتنت با Docker
راهنمای عملی نوشتن Dockerfile برای داتنت — همراه با nuget.config خصوصی
در این آموزش چه یاد میگیریم؟
در این آموزش یک پروژه واقعی 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"]
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 publish | COPY . . 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 را به دیتابیس وصل میکند.
حالا برنامهی داتنت شما قابلحمل، قابلتکرار و امن برای انتشار است. 🐳