کانتینر و Docker سطح: مقدماتی

Image داکر کوچک، سریع و امن: Multi-stage Build و ۱۰ عادت خوب

آموزش بهینه‌سازی Image داکر: Multi-stage Build، کاهش حجم، کاربر غیر Root، Imageهای Chiseled و Distroless و Cache در GitLab CI.

۵ دقیقه مطالعه

Imageی که ۱٫۲ گیگابایت حجم دارد فقط فضای دیسک را هدر نمی‌دهد. هر بار که HPA یک Pod جدید روی Node تازه می‌سازد، باید همین ۱٫۲ گیگابایت را Pull کند. هر اسکن امنیتی هم صدها آسیب‌پذیری در ابزارهایی پیدا می‌کند که اصلاً در Production لازم نیستند. در این مقاله روش‌هایی را می‌گویم که معمولاً حجم Image را چند برابر کم و امنیتش را بیشتر می‌کنند.

۱. Multi-stage Build

ایده ساده است: یک Stage برای Build با همه‌ی ابزارها (SDK، کامپایلر، پکیج‌منیجر) و یک Stage نهایی که فقط خروجی Build را دارد.

نمونه برای .NET:

# ---- build stage ----
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY *.sln ./
COPY src/Api/Api.csproj src/Api/
RUN dotnet restore src/Api/Api.csproj
COPY . .
RUN dotnet publish src/Api/Api.csproj -c Release -o /app --no-restore

# ---- runtime stage ----
FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled
WORKDIR /app
COPY --from=build /app .
USER $APP_UID
EXPOSE 8080
ENTRYPOINT ["dotnet", "Api.dll"]

و برای Node.js:

FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]

۲. ترتیب دستورها = سرعت Build

Docker هر دستور را یک Layer در نظر می‌گیرد و اگر ورودی‌اش تغییر نکرده باشد، از Cache استفاده می‌کند. به همین دلیل:

# ❌ any code change invalidates the dependency layer
COPY . .
RUN npm ci

# ✅ dependencies are re-installed only when package files change
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

قانون کلی: چیزهایی که کمتر عوض می‌شوند، بالاتر.

۳. فایل .dockerignore را فراموش نکنید

بدون آن، کل پوشه (از جمله .git، node_modules و فایل‌های .env) به Docker فرستاده می‌شود:

.git
node_modules
bin
obj
dist
*.md
.env*
docker-compose*.yml

۴. هرگز با Root اجرا نکنید

اگر مهاجم از طریق یک آسیب‌پذیری وارد کانتینر شود، کاربر Root کار او را برای فرار از کانتینر بسیار ساده‌تر می‌کند. Imageهای رسمی معمولاً کاربر آماده دارند (node در Node.js و $APP_UID در .NET 8 به بعد). در Kubernetes هم آن را اجباری کنید:

securityContext:
  runAsNonRoot: true
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities: { drop: [ALL] }

۵. Base Image مناسب انتخاب کنید

نوع Image حجم تقریبی Shell دارد؟ مناسب برای
Debian / Ubuntu کامل ۲۰۰ تا ۸۰۰ مگابایت بله Stage مربوط به Build
Alpine ۵ تا ۵۰ مگابایت بله بیشتر سرویس‌ها
Distroless (Google) ۲ تا ۳۰ مگابایت خیر Go، Java، Node در Production
Chiseled (Ubuntu، مخصوص .NET) حدود ۱۰۰ مگابایت با Runtime خیر سرویس‌های .NET

Imageهای بدون Shell امن‌ترند، چون مهاجم ابزاری برای کار ندارد. ولی Debug کردنشان سخت‌تر است. برای عیب‌یابی از kubectl debug با یک کانتینر موقت استفاده کنید:

kubectl debug -it pod/api-7d9f -n production --image=busybox:1.36 --target=api

۶. نسخه را ثابت کنید

FROM node:latest یعنی Build امروز و Build ماه بعد از دو Image پایه‌ی متفاوت ساخته می‌شوند. حداقل نسخه‌ی Minor را مشخص کنید (node:22.11-alpine). برای اطمینان کامل از Digest استفاده کنید:

FROM node:22-alpine@sha256:<digest>

ابزارهایی مثل Renovate این Digestها را خودکار به‌روز می‌کنند و برایش Merge Request می‌سازند.

۷. Cache بین Buildها در CI

Runnerهای CI معمولاً Cache لایه‌ها را بین Jobها نگه نمی‌دارند. با BuildKit، Cache را در خود Registry ذخیره کنید:

docker-image:
  image: docker:27
  services: [docker:27-dind]
  script:
    - docker buildx create --use
    - docker buildx build
        --cache-from type=registry,ref=$CI_REGISTRY_IMAGE:buildcache
        --cache-to type=registry,ref=$CI_REGISTRY_IMAGE:buildcache,mode=max
        -t "$IMAGE" --push .

در پروژه‌هایی با ده‌ها میکروسرویس، همین تغییر زمان Build را به‌طور محسوسی کم می‌کند.

۸ تا ۱۰. سه عادت کوچک دیگر

  • HEALTHCHECK یا بهتر از آن، Probeهای Kubernetes.
  • یک Process در هر کانتینر: Nginx و برنامه را در یک کانتینر با supervisord اجرا نکنید.
  • اسکن خودکار با Trivy در Pipeline، تا هر Image قبل از رسیدن به Production بررسی شود.

نتیجه‌ی واقعی

قبل بعد
Base Image sdk کامل aspnet Chiseled
حجم حدود ۹۰۰ مگابایت حدود ۱۲۰ مگابایت
آسیب‌پذیری‌های گزارش‌شده توسط Trivy ده‌ها مورد انگشت‌شمار
کاربر root غیر Root

اعداد بالا تقریبی‌اند و به پروژه بستگی دارند، اما الگو همیشه همین است: Image کوچک‌تر، سریع‌تر و امن‌تر است.

جمع‌بندی

بهینه‌سازی Image یکی از کم‌هزینه‌ترین بهبودهایی است که می‌توانید در زنجیره‌ی DevOps انجام دهید: چند خط تغییر در Dockerfile، و نتیجه‌اش Deploy سریع‌تر، Scale سریع‌تر و گزارش امنیتی تمیزتر است.