مقدمه
کاهش حجم image داکر یکی از چیزهاییه که همیشه مدنظر بوده و هست. به عنوان مثال وجود فایلهایی که بهشون نیازی نداریم، زیادتر شدن تعداد لایهها و ... همه از مواردیه هستند که میتونند عامل این موضوع باشند. اینکه چه کارهایی بکنیم تا image با حجم بهینهتر داشته باشیم، چیزیه که در ادامه باهم به اون میپردازیم.
اصولا الان براتون سواله که اصلا multi stage building یعنی چی؟ به طور خیلی خلاصه یعنی اینکه یک Dockerfile داشته باشیم که چندتا image پایه(from statement) داشته باشه! سوالی بعدی که براتون پیش اومده اینکه خب کاربرد این چندتا From داشتن چیه؟ گاها ما موقع ساخت یک image یک سری فایل داریم که تو یک مرحلهای مورد استفاده قرار میگیرند و بعدا به کارمون نمیان! به عنوان مثال تو برنامههایی که با js نوشته شدند(مثلا angular) پس از گرفتن build prod نیازی به وابستگیها نداریم و میتونیم اونها رو حذف کنیم. اینجاست که multi stage معنی پیدا میکنه و ازش استفاده میکنیم. تو From اول (image پایه اول) وابستگیها رو نصب میکنیم و تستها رو اجرا میکنیم و build prod رو انجام میدیم و بعد از گرفتن خروجی، میایم و این فایلهای تولید شده رو تو From بعدی(image پایه دوم) استفاده میکنیم. اگر براتون سواله که آورده این ماجرا چیه؟ کاهش حجم، وابستگیهای مدنظر که حجم خوبی هم دارند، تو image نهایی خودمون نداریم و در نتیجه ما گلهای شاد و خندانیم(به مناسبت ماه مهر D:)!!
احتمالا هنوز براتون سوالاتی وجود داره، بزارین با یک مثال عملی موضوع رو یکم شفافتر کنیم.
FROM node:latest as my_builder_stage COPY package.json /app/ COPY my_node_proj /app/ WORKDIR /app RUN npm install RUN npm run build --prod --outpath=./prod_build # این دستور الکیه و همینجوری نوشتم! # Second stage FROM nginx:latest COPY --from=my_builder_stage /app/prod_build /var/www/my_front COPY my_custom.conf /etc/nginx/conf.d/default.conf
زیباست؟
یک توضیح خیلی مختصر داشته باشیم برای کسایی که شاید براشون سوال شده بالا چیکار کردیم. ما اومدیم اول فایلهای برنامه خودمون رو(با جاوااسکریپت نوشته شده) کپی کردیم و وابستگیهای اون رو نصب کردیم(دستور npm install) و بعد از نصب وابستگیها اومدیم و build پروداکشن اون رو گرفتیم. تو برنامههای انگولاری موقعی که build پروداکشن میگیریم فقظ چیزهایی میخواد رو برمیداره و اونارو میزاره توی یک فایل(البته اون دستور گرفتن build بالا الکلیه و دستورات انگولار با ng هستند). خب حالا دیگه به وابستگیها نیازی نداریم، چون هر چیزی ازشون میخواستیم رو گرفتیم و میتونیم پاکشون کنیم. ولی نکته اینه حتی اگر پاکشون کنیم هم بازم توی لایههای قبلی image ها هستند و حجمشون تو image ما تاثیر میزارند. پس میایم از FROM دوم استفاده میکنیم و میگیم که برو از فلان stage این فایلها رو بردار و بیار تو این image جدید که میخوایم بسازیم و این باعث میشه دیگه اون وابستگیها نیان و حجم کاهش پیدا کنه و بریم آهنگ ملیپوشان پیروز باشید رو پلی کنیم(وقتی دارم این مطلب رو مینویسم کشتی تبش داغه. امان از آدم جو گیر!).
یک اشتباهی خیلی رایجی که موقع ساخت image وجود داره، عدم انتخاب image پایه مناسب هستش. به عنوان مثال اگر توجه کرده باشید توی مثال بخش قبل برای image دوم ما از nginx استفاده کردیم و سراغ node نرفتیم! یا سراغ ubuntu نرفتم و یا سراغ alpine(alpine واقعا سم بدیه!) چرا این انتخاب image پایه مهمه؟ ببینید ما میتونیم یک image پایه خیلی خام مثل alpine رو انتخاب کنیم و بیایم خودمون تک تک وابستگیها را نصب کنیم. ایده هم اینه که فقط چیزهایی که نیاز داریم رو نصب میکنیم و این باعث میشه حجم imageمون کم بشه. در حالی که این جوری نیست و علاوه بر اینکه این وابستگیها بعضا چیزهایی نصب میکنند که زیاد بهشون نیازی نداریم، گاها به دلیل خیلی خام بودن این imageها، یک سری پکیج رو تو image نداریم که این باعث میشه برنامهمون تو پروداکشن بره تو دیوار(در حالی که یک موقع توسعه حالش خیلی هم خوب بوده)، نگم براتون از دیباگ کردن این موضوع که، واقعا اذیت کننده میشه. محصول به فنا رفته و تو پروداکشنی و در سریعترین زمان ممکن باید پیدا کنی چرا و به چه علت! مثلا وقتی داری با پایتون ۳٫۹ کار میکنی خب برو از python3.9 استفاده کن دیگه، این وسط alpine چی میگه!! افراد مختلف هم اومدن imageهای مختلف برای موارد مختلف ارائه دادن که بد نیست اونها رو هم یک نگاهی بندازین. در کل یکم وقت گذاشتن روی انتخاب image پایه میتونه تو آینده و سردردهای احتمالی ارورها کم کنه.
ایده بدی نیست که هر چند وقت یکبار بیایم و فایلهای داخل image نهایی ساخته شده رو بررسی کنیم. به عنوان مثال تجربه خودم بخوام بگم، وقتی داشتم یکی از imageهای خودمون رو بررسی میکردم دیدم یک کتابخونهای داریم که حجم خوبی رو به خودش اختصاص داده و اگر ما بیایم و یک نسخه دیگه از اون کتابخونه رو استفاده کنیم، از لحاظ حجمی خیلی بهینهتر میشه و مشکلی هم موقع اجرای برنامه نخواهیم داشت. من برای این کار از ابزار dive استفاده میکنم که کار راهاندازه و سنگین هم نیست.
خوبی dive اینه میتونیم توی CI هم ازش استفاده کنیم و ببینیم که image نهایی ساخته شده چقدر بهینهست.
موقع ساخت یک image میتونیم یک فلگ به نام squash رو به داکر پاس بدیم تا عمل squash رو برای ما انجام بده. البته این فلگ هنوز به صورت experimental هستش و برای اینکه بتونیم ازش استفاده کنیم باید مقدار experimental را برای سرویس داکر فعال کرده باشیم. برای این کار میتونیم به این شکل عمل کنیم:
sudo vim /etc/docker/daemon.json
حالا مقدار عبارت زیر را در داخل فایل قرار میدیم:
{ "experimental": true }
و سرویس داکر رو restart میکنیم:
sudo service restart docker
اما این فلگ چیکار میکنه؟ این فلگ میاد هیستوری لایهها رو از بین میبره و تمام لایهها رو بررسی میکنه و هیستوریهای مرتبط با یک فایل رو از بین میبره. یعنی چی؟ اگر مطلب مرتبط با overlay رو خونده باشید، یک فایل تو لایههای مختلف میتونه تغییر کنه و این تغییرات رو هم تو هر لایه خواهیم داشت، این فلگ میاد همه این تغییرات رو میریزه دور و فقط آخرین وضعیت اون فایل رو نگه میداره(موارد مرتبط با upperdir و merged). اما یک نکته هم داره اونم اینکه دیگه نمیاد فایلهایی که بی استفاده و پاک شده رو هم پیدا کنه و بریزه دور! فقط فایلهایی که تغییر داشتن رو بررسی میکنه، برای همین ممکنه یک سری فایل داشته باشیم که تو لایههای پایین ساخته شده باشند و استفادهای نداشته باشند ولی این فلگ کاری باهاشون نداره. برای مطالعه بیشتر مستندات خود داکر رو مطالعه کنید.
نمونه استفاده از این فلگ:
docker build --squash -t my_image:1.0.0 .
این دستور مختص حامل(container) هستش و میاد از وضعیت فعلی حامل خروجی میگیره. این خروجی میاد تمام فایلها رو بررسی میکنه و فایلهایی رو مورد نیازش نیست رو پاک میکنه. نمونه این دستور:
docker export my_container > my_tar_image.tar.gz
حالا باید این image به شکل tar.gz هستش و باید تو سرویس داکر بارگذاری کنیم، برای این کار:
docker import my_tar_image.tar.gz
مشکل دستور export:
دستور export یک مشکلی داره و اونم اینه که میاد و metadata مرتبط با اون image رو پاک میکنه. به عنوان مثال CMD و ENTRYPOINT پاک میشن(یادم نیست که env variable ها هم پاک میشوند یا نه). به عنوان مثال میشه از حامل خودمون export بگیریم و بهش یک تگی بدیم و توی یک dockerfile دیگه بیایم و اون image رو استفاده کنیم.
همین! لبخند بزنین لطفا :)
منابع: