Dockerfileای که روی لپتاپ build میشود، لزوماً برای production مناسب نیست. ممکن است secret را در لایهها باقی بگذارد، وابستگی را از tag شناور بگیرد، برنامه را با root اجرا کند یا فایلهای build را در image نهایی نگه دارد. معیار یک Dockerfile خوب فقط حجم نیست: باید بازسازیپذیر، قابلآزمون، قابلبهروزرسانی و متناسب با رفتار واقعی برنامه باشد.
پاسخ سریع: base image معتبر و نسخهدار انتخاب کنید؛ dependencyها را با lockfile و build چندمرحلهای مدیریت کنید؛ فقط artifact لازم را به runtime ببرید؛ secret را وارد image نکنید؛ کاربر غیرroot، فرمان exec-form و مسیر shutdown روشن داشته باشید. image نهایی را در CI بسازید، با digest منتشر کنید و همان digest را در staging و production اجرا کنید.
قبل از نوشتن دستور اول، قرارداد اجرای برنامه را روشن کنید
Dockerfile نمیتواند ابهام معماری را برطرف کند. فرمان start، پورت، مسیر داده قابلنوشتن، dependencyهای شبکه، config، signal توقف و شرط آمادهبودن را فهرست کنید. اگر برنامه برای بالا آمدن نیازمند ویرایش دستی داخل container است، نخست آن کار را در فرایند build یا migration مشخص و تکرارپذیر کنید. راهنمای Dockerize کردن اپلیکیشن این مرحله پیشنیاز را باز میکند.
Base image: نسخه مشخص، منبع معتبر، چرخه patch
از image مناسب runtime و معماری هدف استفاده کنید. tagهایی مانند latest در زمانهای مختلف به artifact متفاوت اشاره میکنند. pin کردن digest نتیجه build را قابلپیگیریتر میکند، اما اگر هرگز آن را بهروزرسانی نکنید، patchهای پایه را از دست میدهید. بنابراین در کنار pin، زمانبندی rebuild، scan و promotion داشته باشید. تصویر کوچکتر خودبهخود امنتر نیست؛ بستههای لازم، CA certificate، timezone و کتابخانه native را با آزمون واقعی تعیین کنید.
Build context را کنترل کنید
COPY . . بدون .dockerignore میتواند repository، cache، backup، فایل محیط و داده شخصی را وارد context کند. حتی اگر در مرحله آخر کپی نشوند، ممکن است به builder رسیده باشند. فهرست ignore را متناسب با پروژه بنویسید و build را در CI با context تمیز تکرار کنید. پوشه .git، log، خروجی تست و فایلهای محرمانه معمولاً نباید ورودی image باشند؛ استثنا را آگاهانه ثبت کنید.
ترتیب لایهها و cache
دستورهای کمتغییر مانند دریافت dependency قفلشده را پیش از کپی کل کد قرار دهید. در غیر این صورت هر تغییر کوچک، نصب همه وابستگیها را دوباره اجرا میکند. برای Node، Python یا Go جزئیات متفاوت است، ولی اصل ثابت است: manifest و lockfile را زودتر از source کپی کنید، سپس نصب یا build را انجام دهید. cache کارایی build است، نه تضمین صحت؛ CI باید build سرد و خروجی تست را هم بسنجد.
Multi-stage: ابزار ساخت را از runtime جدا کنید
compiler، package manager، test runner و headerهای توسعه معمولاً در image اجرای برنامه لازم نیستند. مرحله builder artifact را میسازد و مرحله runtime فقط خروجی و dependency زمان اجرا را میگیرد. دستور COPY --from=build مرز انتقال را شفاف میکند. راهنمای Multi-stage Build تفاوت stage، target و خطر کپیِ وابستگیهای اشتباه را توضیح میدهد.
یک الگوی کوچک، نه نسخه آماده برای هر زبان
FROM golang:1.26 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -o /out/app ./cmd/server
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
این نمونه فقط برای برنامه Go با مسیر ./cmd/server و خروجی سازگار با CGO_ENABLED=0 است؛ نباید کورکورانه برای برنامه دارای dependency بومی کپی شود. نسخه imageهای پایه باید در پروژه pin و بهروز شود. برنامهای که به فایل config، template یا certificate خاص نیاز دارد باید آنها را صریح و با مجوز مناسب منتقل کند. مرحله runtime را با آزمون واقعی اجرا کنید؛ موفقیت build به معنی سالم بودن اجرا نیست.
Secret در Build چه میشود؟
رمز registry خصوصی، token package و کلید SSH را در ARG، ENV یا COPY قرار ندهید. پاککردن فایل در لایه بعدی لزوماً رد آن را از history حذف نمیکند. اگر build به credential نیاز دارد، از BuildKit secret یا SSH mount متناسب استفاده کنید و مطمئن شوید command آن را در log یا artifact خروجی کپی نمیکند. secret زمان اجرای برنامه باید از مسیر امن runtime تزریق شود، نه داخل Dockerfile.
USER غیرroot و مجوز فایلها
اگر برنامه نیازی به امتیاز ویژه ندارد، با USER اجرا شود. مالکیت فایل کپیشده، مسیر cache و volume را از پیش تعیین کنید؛ اجرای non-root بدون مجوز نوشتن لازم فقط خطا را به زمان اجرا منتقل میکند. UID/GID ثابت میتواند روی volume مشترک مفید باشد، ولی باید با سیاست host و orchestrator هماهنگ شود. اجرای privileged یا mount کردن Docker socket برای رفع مشکل permission راهحل عمومی نیست.
ENTRYPOINT، CMD و دریافت signal
فرم آرایهای مانند ENTRYPOINT ["/app"] اجرای مستقیم process را ساده میکند. shell-form ممکن است signal را به process اصلی نرساند، مگر entrypoint آن را درست forward کند. هنگام stop، برنامه باید SIGTERM را دریافت و با زمان محدود connection و job را جمع کند. اگر script آغازگر لازم است، خروجی آن باید با exec به process اصلی تحویل شود و خطا را پنهان نکند.
HEALTHCHECK مفید، نه تزئینی
healthcheck باید ارزان، سریع و نماینده سلامت موردنظر باشد. درخواست به endpoint محلی برنامه بهتر از صرفاً دیدن PID است، اما checkای که به تمام سرویسهای بیرونی وابسته است میتواند با اختلال موقت یک dependency باعث رفتار آبشاری شود. برنامه و محیط اجرا را جدا در نظر بگیرید: لزوماً لازم نیست تمام healthcheckها داخل Dockerfile باشند؛ برخی در Compose یا orchestrator تعریف میشوند. وجود healthcheck بهتنهایی جای monitoring بیرونی یا readiness هنگام rollout را نمیگیرد.
EXPOSE امنیت یا انتشار پورت نیست
EXPOSE قرارداد مستند پورت image است و بهتنهایی port را روی اینترنت باز نمیکند. publish کردن پورت در تنظیمات اجرا انجام میشود. برای دیتابیس و cache اغلب تنها network داخلی لازم است. اگر برنامه با user غیرroot روی پورت پایین اجرا نمیشود، بهجای افزودن privilege بیدلیل، پورت داخلی بالاتر و mapping proxy را بررسی کنید.
فایلسیستم و داده ماندگار
image باید قابلجایگزینی باشد. upload، database و state مهم را در storage تعریفشده نگه دارید و Backup و Restore را جداگانه طراحی کنید. VOLUME در Dockerfile ممکن است برای image عمومی مفید باشد، ولی محل دقیق و lifecycle داده در deployment باید روشن باشد. cache و فایل موقت نیز فضای واقعی مصرف میکنند؛ اگر filesystem را read-only میکنید، مسیر writable حداقلی را مشخص کنید.
آزمون image نهایی، نه فقط مرحله build
- در CI dependency و test را اجرا کنید.
- image نهایی را از context تمیز بسازید.
- image را از نظر secret و آسیبپذیری شناختهشده scan کنید.
- با همان user runtime راهاندازی و health را آزمون کنید.
- توقف با signal و restart را بررسی کنید.
- با config و volume شبیه staging تست کنید.
- digest را ثبت و بدون rebuild مستقل به production منتقل کنید.
scan نتیجه قطعی «امن» نمیدهد؛ advisoryها تغییر میکنند و کد برنامه نیز باید بررسی شود. تست خوشمسیر کافی نیست: نبود dependency، permission اشتباه و دیر آمادهشدن برنامه را نیز بررسی کنید.
اشتباههایی که زیاد دیده میشود
- استفاده از tag شناور بدون ثبت digest
- قرار دادن
.envدر build context - نصب ابزار ساخت در image runtime
- اجرای root فقط برای آسانشدن نوشتن فایل
- ساخت دوباره image برای هر محیط بهجای promotion همان artifact
- فرض سالم بودن برنامه پس از
docker build - بیتوجهی به migration دیتابیس و برگشت نسخه
وقتی تیم به کمک نیاز دارد
اگر image بزرگ، build کند یا release غیرقابلتکرار است، قبل از کوچککردن افراطی باید مسیر dependency، artifact و runtime بررسی شود. سرویس داکرایز اپلیکیشن میتواند Dockerfile، CI، secret و رفتار اجرای production را کنار هم طراحی و در staging آزمون کند.
پرسشهای متداول
آیا هر Dockerfile تولیدی باید Multi-stage باشد؟
نه الزام فنی؛ اما وقتی ابزار build یا فایلهای موقت دارید، جداسازی stage معمولاً ارزشمند است. معیار، خروجی قابلآزمون و کمدسترسی است.
آیا حذف package manager از runtime کافی است؟
خیر؛ بهروزرسانی base، مجوز process، secret، network و کد برنامه نیز مهماند.
آیا باید از latest استفاده کنیم؟
برای production بهتر است artifact مشخص با digest یا نسخه ثبتشده deploy شود تا معلوم باشد چه چیزی اجرا میشود.
چرا برنامه بعد از build اجرا نمیشود؟
ممکن است فایل runtime، کتابخانه native، مجوز مسیر writable، config یا معماری CPU با مرحله build فرق داشته باشد؛ image نهایی را جدا تست کنید.