ایمیجی که با هر Dockerfile ساخته شود اجرا میشود، ولی ایمیج تولیدی باید کوچک، سریع در build، قابلبازتولید و امن باشد. تفاوت بین این دو در تعداد کمی اصل است: ترتیب دستورها، جداکردن مرحلهی build از مرحلهی اجرا، انتخاب آگاهانهی ایمیج پایه و محدودکردن دسترسی برنامه. این راهنما این اصول را با نمونهی «بد و خوب» توضیح میدهد.
IMGDocker Image Size Calculatorحجم لایههای یک ایمیج را ببینید و Dockerfile خود را برای مشکلات رایج بررسی کنید.انتخاب ایمیج پایه
ایمیج پایه بیشترین سهم را در حجم و آسیبپذیریهای شناختهشدهی ایمیج نهایی دارد. گزینههای رایج:
| نوع | مثال | مزیت | نکته |
|---|---|---|---|
| کامل | python:3.12 | ابزارهای ساخت آماده | حجم بالا؛ مناسب مرحلهی build |
| slim | python:3.12-slim | کوچک و سازگار با glibc | پیشفرض خوب برای اجرا |
| Alpine | python:3.12-alpine | بسیار کوچک | musl؛ مراقب وابستگیهای native باشید |
| Distroless | gcr.io/distroless/static | بدون شل و مدیر بسته؛ سطح حملهی کم | عیبیابی سختتر؛ مناسب باینریهای استاتیک |
| scratch | scratch | خالی | فقط برای باینری کاملاً استاتیک (مثلاً Go) |
همیشه تگ دقیق بنویسید (python:3.12.7-slim نه python یا latest). برای بازتولیدپذیری کامل میتوانید digest را هم ثابت کنید: FROM python:3.12.7-slim@sha256:….
ترتیب دستورها و کش لایهها
Docker هر لایه را کش میکند و تا اولین لایهای که تغییر کرده، همهی لایههای بعدی را دوباره میسازد. بنابراین چیزهایی که کمتر تغییر میکنند (وابستگیها) باید قبل از چیزهایی که زیاد تغییر میکنند (کد برنامه) بیایند.
# بد: هر تغییر کوچک در کد، نصب مجدد همهی وابستگیها را باعث میشود
FROM node:20-slim
WORKDIR /app
COPY . .
RUN npm ci
CMD ["node", "server.js"]
# خوب: لایهی وابستگیها تا وقتی package-lock.json عوض نشود کش میماند
FROM node:20-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]
همین الگو برای هر زبان دیگری هم برقرار است: فایلهای تعریف وابستگی (requirements.txt، go.mod، pom.xml) را اول کپی و نصب کنید و کد را بعد از آن.
Multi-stage build
کامپایلر، ابزارهای build و وابستگیهای توسعه برای اجرای برنامه لازم نیستند. با multi-stage، در مرحلهی اول برنامه را میسازید و فقط خروجی آن را به یک ایمیج تمیز منتقل میکنید.
# syntax=docker/dockerfile:1
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/app
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot
ENTRYPOINT ["/app"]
برای یک فرانتاند، همین الگو با Node در مرحلهی build و Nginx در مرحلهی نهایی به کار میرود:
FROM node:20-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
نتیجه معمولاً ایمیجی است که چند برابر کوچکتر است و ابزارهایی مثل کامپایلر یا مدیر بسته را ندارد؛ یعنی مهاجم هم چیزی برای سوءاستفاده پیدا نمیکند.
کاهش حجم در هر لایه
هر دستور RUN یک لایهی فقطافزودنی میسازد؛ فایلی که در یک لایه ساخته شود، حتی اگر بعداً پاک شود، حجمش در ایمیج میماند. پس دانلود، نصب و پاکسازی باید در یک RUN باشد:
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
apt-get updateرا هرگز در یکRUNجدا ازinstallننویسید؛ کش لایه ممکن است فهرست بستههای قدیمی را نگه دارد.- در پایتون از
pip install --no-cache-dirو در Node ازnpm ci --omit=devاستفاده کنید. - برای کپیکردن فایلهای محلی از
COPYاستفاده کنید، نهADD؛ ADD رفتارهای اضافی (استخراج آرشیو، دانلود URL) دارد که معمولاً لازم نیست.
ابزار بررسی Dockerfile بکسرور همین الگوها (جداکردن update از install، پاکنکردن کش بستهها، استفاده از ADD و اجرا با root) را شناسایی میکند.
فایل .dockerignore
Docker هنگام build کل پوشهی context را به دیمن میفرستد. بدون .dockerignore، پوشههایی مثل node_modules، .git و فایلهای .env هم ارسال میشوند؛ build کند میشود و ممکن است رمزها به ایمیج برسند.
.git
node_modules
dist
*.log
.env
.env.*
Dockerfile
docker-compose*.yml
اجرای امن و درست برنامه
کاربر غیر root
اگر برنامه نیازی به root ندارد، با یک کاربر معمولی اجرایش کنید تا در صورت نفوذ، دسترسی مهاجم محدود بماند:
RUN groupadd --system app && useradd --system --gid app --no-create-home app
COPY --chown=app:app . .
USER app
exec form و مدیریت سیگنال
اگر CMD node server.js (shell form) بنویسید، فرایند اصلی یک شل است و سیگنال SIGTERM هنگام توقف به برنامه نمیرسد؛ Docker بعد از ۱۰ ثانیه آن را با SIGKILL میکشد و خاموشی تمیز (graceful shutdown) رخ نمیدهد. همیشه از exec form استفاده کنید:
CMD ["node", "server.js"]
اگر برنامهی شما فرایندهای فرزند میسازد، docker run --init (یا init: true در Compose) یک init سبک اضافه میکند که zombie processها را جمع میکند.
HEALTHCHECK
HEALTHCHECK --interval=15s --timeout=3s --retries=3 \
CMD curl -fsS http://localhost:8080/healthz || exit 1
در ایمیجهای بدون curl، یک زیردستور داخل خود برنامه برای بررسی سلامت بنویسید. بدون healthcheck، docker ps و depends_on: condition: service_healthy نمیتوانند آمادگی واقعی را تشخیص دهند.
ویژگیهای BuildKit: secret و کش دانلود
BuildKit (پیشفرض Docker در نسخههای جدید) دو قابلیت مهم دارد. اول، secret mount: رمز فقط در همان RUN قابلخواندن است و در هیچ لایهای ذخیره نمیشود:
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
docker build --secret id=npmrc,src=$HOME/.npmrc -t app:1.0 .
دوم، کش دانلود بین buildها که زمان نصب وابستگیها را کم میکند:
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
ARG یا ENV به build ندهید؛ هر دو در تاریخچهی ایمیج میمانند و با docker history دیده میشوند.بررسی نتیجه
docker build -t app:1.0 .
docker images app:1.0
docker history app:1.0 # حجم هر لایه
لایهای که بیشترین حجم را دارد معمولاً اولین هدف بهینهسازی است. برای ایمیجهایی که روی رجیستری هستند، میتوانید حجم فشردهی هر لایه را بدون دانلود ببینید.
چکلیست Dockerfile آمادهی تولید
- ایمیج پایه با تگ دقیق (یا digest)، و نوع مناسب (slim یا distroless برای اجرا).
- فایلهای وابستگی قبل از کد کپی شدهاند.
- multi-stage برای جداکردن ابزارهای build از ایمیج نهایی.
- دستورهای نصب بسته و پاکسازی کش در یک
RUN. .dockerignoreموجود است و.envو.gitرا شامل میشود.- کاربر غیر root و exec form برای CMD/ENTRYPOINT.
- HEALTHCHECK تعریف شده است.
- هیچ رمزی در ARG، ENV یا لایهها نیست.
جمعبندی و مطالعهی بعدی
ایمیج خوب از چند تصمیم ساده ساخته میشود: پایهی سبک و ثابت، کشپذیری، multi-stage و اجرای محدود. بعد از ساخت ایمیج، نوشتن فایل Compose سرویسها را کنار هم میگذارد و امنسازی Docker اقدامهای امنیتی لازم برای کانتینر و میزبان را فهرست میکند. اگر ایمیج پایه یا ابزارها را روی سرور ایران نصب میکنید، راهنمای نصب از مخزن داخلی را ببینید.
پرسشهای متداول
Alpine بهتر است یا Debian slim؟
-slim (مبتنی بر Debian) انتخاب امنتری است.تفاوت CMD و ENTRYPOINT چیست؟
ENTRYPOINT فرمان اصلی کانتینر است و CMD آرگومانهای پیشفرض آن. اگر هر دو باشند، CMD بهعنوان آرگومان به ENTRYPOINT داده میشود و هنگام docker run قابلجایگزینی است. هر دو را بهصورت exec form (آرایهی JSON) بنویسید تا سیگنالها به برنامه برسند.چرا حجم ایمیج بعد از حذف فایلها کم نشد؟
RUN یک لایهی فقطافزودنی میسازد. فایلی که در یک لایه ساخته و در لایهی بعد پاک شود، همچنان در لایهی قبلی هست. دانلود و پاکسازی باید در همان RUN انجام شود یا از multi-stage استفاده کنید.آیا میتوانم رمز را با ARG به build بدهم؟
docker history) باقی میماند. برای رمزهای زمان build از BuildKit secret mount استفاده کنید که در هیچ لایهای ذخیره نمیشود.