چرا محیط ساخت را به سرور نبریم؟

یک image که هم کامپایلر، هم ابزار تست و هم برنامه نهایی را در خودش دارد، معمولاً بیش از نیاز محیط اجرا حمل می‌کند. Multi-stage Build اجازه می‌دهد مراحل ساخت و اجرای نهایی جدا باشند و فقط خروجی لازم به مرحله آخر کپی شود. این روش برای برنامه کامپایل‌شده، مانند Go، روشن است: کد در مرحله اول به فایل اجرایی تبدیل می‌شود و مرحله دوم آن فایل را اجرا می‌کند. کم شدن حجم به انتقال و بررسی image کمک می‌کند، اما به‌تنهایی تضمین امنیت یا سرعت برنامه نیست.

نمونه این مقاله یک سرویس کوچک HTTP را از داخل Dockerfile می‌سازد. برنامه هیچ وابستگی خارجی Go ندارد و برای فهم سازوکار از go build روی یک فایل استفاده می‌کنیم. ساخت بدون CGO امکان اجرای باینری در scratch را فراهم می‌کند. scratch سیستم‌عامل عمومی با shell و ابزار عیب‌یابی نیست؛ این تفاوت روی بررسی خطا و نیاز برنامه به فایل‌های محیطی اثر دارد. تصویر مقاله از وبلاگ رسمی Docker درباره imageهای برنامه وب است؛ مثال کد خود مقاله مستقل و مبتنی بر Go نوشته شده است.

اندازه کم تنها معیار نیست

در پروژه واقعی، فایل وابستگی‌ها را قبل از بقیه کد کپی کنید تا تغییر یک فایل منبع تمام نصب وابستگی‌ها را دوباره اجرا نکند. .dockerignore باید پوشه‌های محلی، خروجی ساخت، فایل‌های موقت و رازها را کنار بگذارد. راز را با ARG یا COPY وارد image نکنید؛ روش secret mount ابزار ساخت برای این کار مناسب‌تر است. برای انتشار قابل تکرار، نسخه پایه و در صورت لزوم digest را ثبت کنید. tag شناور در مثال آموزشی ساده است، ولی کنترل دقیق انتشار به ثبت نسخه‌های واقعاً ساخته‌شده نیاز دارد.

کاربر غیرریشه و دسترسی محدود نوشتن را از ابتدا آزمایش کنید. در نمونه، برنامه روی پورت ۸۰۸۰ گوش می‌دهد تا به پورت ممتاز نیاز نداشته باشد. اگر برنامه به HTTPS بیرونی، منطقه زمانی یا فایل پیکربندی نیاز دارد، scratch به‌خودی‌خود همه فایل‌های لازم را فراهم نمی‌کند. این نیازها را با آزمون واقعی مشخص کنید، نه با افزودن تصادفی پکیج‌ها. همچنین خروج graceful، timeout سرور، health check و ثبت خطا باید قبل از تولید تکمیل شوند؛ نمونه عمداً یک سرویس حداقلی برای نمایش مراحل ساخت است.

پیشنهاد تحریریه این است که اندازه image، زمان build سرد، build پس از تغییر کد و زمان بالا آمدن سرویس را جدا اندازه بگیرید. کوچک‌ترین image الزاماً بهترین انتخاب برای تیم شما نیست؛ دسترسی به ابزارهای تشخیص خطا و سازگاری کتابخانه‌ها نیز اهمیت دارد. اسکن وابستگی‌ها باید image نهایی و مسیر ساخت را پوشش دهد. اگر خروجی برای چند معماری منتشر می‌شود، هر معماری را آزمایش کنید. موفق بودن build روی رایانه توسعه‌دهنده، رفتار یکسان روی سرور ARM یا محیط محدودشده را ثابت نمی‌کند.

نمونه کد و روش بررسی

این مثال آموزشی برای فهم مسیر پیاده‌سازی نوشته شده است. نسخه‌ها و پیش‌نیازهای ذکرشده را در محیط آزمایش بررسی کنید؛ نکات زیر مشخص می‌کنند برای استفاده عملی چه چیزهایی باید تکمیل شوند.

Dockerfile مستقل؛ سپس build و درخواست HTTP
# syntax=docker/dockerfile:1
FROM golang:1.26 AS build
WORKDIR /src
COPY <<EOF main.go
package main
import ("fmt"; "log"; "net/http")
func main() {
  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "ready")
  })
  log.Fatal(http.ListenAndServe(":8080", nil))
}
EOF
RUN CGO_ENABLED=0 go build -trimpath -o /out/server main.go

FROM scratch
COPY --from=build /out/server /server
USER 65532:65532
EXPOSE 8080
ENTRYPOINT ["/server"]

فایل را با نام Dockerfile ذخیره کنید. docker build -t liyan-go-demo . و سپس docker run --rm -p 127.0.0.1:8080:8080 liyan-go-demo را اجرا کنید. درخواست به http://127.0.0.1:8080 باید ready برگرداند. برنامه آزمایشی timeout و graceful shutdown ندارد؛ پیش از تولید اضافه کنید. نسخه 1.26 پایه صرفاً نسخه مشخص نمونه است؛ patch و digest تأییدشده پروژه را برای انتشار انتخاب کنید.

از ساخت موفق تا سرویس قابل بهره‌برداری

قبل از انتشار، سرویس را با همان UID، محدودیت حافظه و شبکه محیط هدف اجرا کنید و یک درخواست واقعی بفرستید. نسخه قبلی قابل بازگشت، digest خروجی جدید و نتیجه بررسی را در تحویل ثبت کنید. افزایش نسخه image باید قابل ردیابی و تکرارپذیر باشد. پس از این مرحله می‌توانید سراغ cache اشتراکی، تولید SBOM و امضای artifact بروید. ارزش Multi-stage وقتی کامل می‌شود که تیم بداند چه چیزی در مرحله ساخت مصرف شده، چه چیزی وارد خروجی شده و خروجی در محیط اجرا چه نیازهایی دارد.

چک‌لیست اجرای عملی

  • نمونه به Docker BuildKit و دسترسی برای دریافت image پایه نیاز دارد.
  • فایل‌های راز و محیط محلی را در .dockerignore کنار بگذارید.
  • با کاربر غیرریشه، محدودیت منابع و تنظیم شبکه هدف آزمایش کنید.
  • اندازه، digest و مسیر بازگشت را همراه نتیجه درخواست HTTP ثبت کنید.

توضیح‌ها و پیشنهادهای اجرایی این مطلب، تحلیل تحریریه دانشنامه لیان هستند.منابع: Docker — Multi-stage builds · Docker — Building best practices · Docker — Build secrets

این مطلب بازنویسی تحلیلی دانشنامه لیان بر پایه منبع اصلی است.مشاهده منبع اصلی