احتمالا واژه Containerization رو خیلی شنیدیم، این واژه یعنی چی؟ Containerization تکنولوژی هست که به ما این اجازه رو میده که بتونیم برنامهها یا اپلیکیشنها مون رو در کنار همهی دپندنسیهاش در یک پکیج به نام Container بسته بندی کنیم، کانتینرها فضایی ایزوله شده برای برنامههامون ایجاد میکنن که بتونیم اونها رو تو محیطهای مختلف (مثلا یه سیستم دیگه یا یک سرور تو فضای ابری) بدون نگرانی از فراهم بودن منابع مورد نیازشون اجرا کنیم.
حالا ممکنه این سوال پیش بیاد که پس Dockerize کردن یعنی چی؟
خب تا اینجا گفتیم که Containerization کردن چیه و به چه دردی میخوره، بریم سراغ Dockerize. به نظر وقتی میگیم Container همه ذهنشون میره سمت داکر! درصورتی که داکر تنها پلتفرمی نیست که امکان ایجاد کانتینر رو بهمون میده بلکه پلتفرمهای دیگه ای مثل Podman، rkt، LXC و.. هم وجود دارن که هر کدومشون تو یه زمینهای بهتر از دیگری هستن.
پس با این تعریف Dockerize کردن یعنی ما با استفاده از Docker نرمافزارمون رو Containerized میکنیم، پس مفهوم جدیدی نیس که داکر ایجاد کرده باشه.
داکر مثل اون پلتفرمهای دیگه ابزارها و تکنولوژیهای مختلفی رو برای ایجاد کانتینر در اختیارمون قرار میده که Dockerfile و Docker Image از اساسیترین ابزارها برای این کار هستن. تو این نوشته هم قراره بیشتر با Dockerfile آشنا بشیم.
همونطور که گفتیم داکر یکی از چندین پلتفرمهایی هست که امکان Containerized کردن نرمافزار رو بهمون میده، پس هدف نهایی داکر ایجاد این کانتینرهاس و هر مفهموم دیگهای (مثل Docker Image) که داکر در اختیارمون قرار میده صرفا ابزاری هستن براین ایجاد کانتینرها.
داکرایمیح هم از اساسیترین این مفاهیم هست و نقش مرکزی در ایجاد کانتینرها داره، چون یک container در نهایت از روی Image ساخته میشن و این ایمیجها مثل نقشههایی هستن که مشخص میکنن یک کانتینر باید شامل چه تنظیمات، فایلها و دستوراتی برای اجرا باشند.
خب وقتی Container از روی Docker Image ساخته میشه پس ما نیاز به ابزاری داریم که بتونیم این ایمیجها رو ایجاد کنیم و Dockerfile همون ابزاری هست که داکر برای ایجاد Image در اختیارمون قرار میده.
داکرفایل یه فایل متنی به اسم `Dockerfile` هست که توش مشخص میکنیم که میخواییم Imageمون شامل چه چیزهایی باشه.
برای آشنایی بیشتر با Dockerfile و Dockerize کردن پروژه بیاییم با یه مثال عملی پیش بریم. چند سال پیش یه پروژه NodeJS که برای شالوده پروژههام استفاده می کردم رو توی GitHub گذاشتم که میتونه مثال خوبی باشه. البته شما میتونید هر پروژه ای با هر زبونی که دوست دارید رو انتخاب کنید.
برای شروع پروژه رو از این Repository با دستور زیر Clone کنید.
git clone https://github.com/mehdizarepour/backend.git
حالا اگه پروژه رو با ادیتورتون باز کنید باید به شکل زیر باشه:
اینکه پروژه چیه و چه قابلیتهایی داره و یا قسمتهای مختلفش چیکار میکنن مهم نیس، صرفا خواستم از یه پروژه واقعی استفاده کرده باشم، پس وارد جزییاتش نشین (البته پروژهی خوبیه، پیشنهاد میکنم بعدا یه نگاه بهش بندازین ?).
خب از اونجایی که این یه پروژه NodeJS هست قبل از اجرا باید ماژولهاش رو نصب کنیم، برای این کار دستور زیر رو اجرا کنید:
npm i
و برای اجرا دستور زیر رو اجرا کنید:
npm start
بعد از اجرای این دستور احتمالا به خطای زیر میخورید:
[Exception] ENOENT: no such file or directory, open './runtime/private.key' stack: Error: ENOENT: no such file or directory, open './runtime/private.key'
این خطا برای اینه که چون پروژه از JWT برای احرازهویت استفاده میکنه و شما private key و public key رو توی فلدر runtime ندارید، برای حل این مشکل اگه دوست دارید میتونید این فایلها رو اضافه کنید اگه هم نه صرفا برید به فلدر config/app.js/ و این دو خطی که پایین تو عکس نشون دادم رو کامنت کنید:
حالا دوباره دستور npm start رو اجرا کنید تا پروژه به درستی اجرا بشه:
خب رسیدیم به جای قشنگش ?، تا اینجا دو تا مشکل برای اجرا کردن پروژه ای که من نوشتم و به شما دادم برای اینکه اجرا کنید وجود داشت، چی؟
خب همین دو مسئله باعث شد پروژهای که من نوشتم قابلحمل نباشه و شما نتونید روی سیستم خودتون (یا یه سرور) اجراش کنید. این دقیقا یکی از مشکلاتی هست که ما میخواییم با Containerized کردن حلش کنیم، یعنی شما بتونید نرمافزار یا پروژهای که من نوشتم رو بدون نگرانی از اینکه آیا نیازمندیها یا دپندنسیهاش (مثل NodeJs) رو روی سیستم خودتون دارید یا نه اجرا کنید. اگه هنوز این سوال براتون وجود داره که چرا ما نیاز داریم یه نرمافزار یا اپلیکیشن رو Containerized یا Dockerize کنیم، پیشنهاد می کنیم یه نگاه به این نوشته که راجع به چرایی استفاده از داکر توضیح دادم بندازین.
خب حالا که مشکل و راه حل رو فهمیدیم بیاییم پروژه رو Dockerize کنیم که هرکسی بتونه به راحتی روی هر سیستمی اجراش کنه.
برای شروع یه فایل به اسم Dockerfile به روت پروژه به شکل زیر اضافه میکنیم:
خب قبل از اینکه شروع کنیم بیاییم یه بار دیگه یادآوری کنیم که ما قراره همهی دپندنسیهای اجرا شدن این نرمافزار رو تو یه Image پکیج کنیم، پس با استفاده از Dockerfile میخواییم مشخص کنیم که این دپندنسیها چی هستن تا داکر از روی اون image رو ایجاد کنه. پس بیاییم راجع به دپندنسیهای این برنامه صحبت کنیم و ببینیم چه چیزهایی نیازه:
قاعدتا اولین نیازمندی یه نرمافزار یه سیستمعامل هست که بتونه روش اجرا بشه و همینطور دپندنسیهاش روش نصب یا کانفیگ بشه. برای اینکار ما از دستور FROM استفاده میکنیم تا سیستمعامل مدنظرمون رو به عنوان base image انتخاب کنم.
شاید سوال باشه که منظور از base image چیه؟ اگه بخواییم خیلی ساده به این سوال جواب بدیم، اینطور میگم که، یه سری تنظیمات و یا دپندنسیها هست که خیلی کلی هستن و تقریبا تو همهی نرمافزارها مشترکن، مثلا تنظیماتی که برای خود سیستمعامل هستن، خب وقتی ما میخواییم دپندنسیهای خاص نرمافزار خودمون رو اضافه کنیم نباید نگران این باشیم که آیا مثلا سیستمعامل درست کانفیگ شده یا نه که، درسته؟
با استفاده از base image ما یه imageی رو انتخاب میکنیم تا نیازمندیهای خودمون رو بهش اضافه کنیم تا یه ایمیج جدید بسازیم، پس دستور FROM برای انتخاب base imageمون اولین دستوری هست که تو Dockerfile مینویسیم(اولین دستوری که کامنت نیست).
انتخاب من برای base image سیستمعامل Alpine Linux هست، چون یه سیستمعامل سبکه و چیز اضافهای هم روش نصب نیس که باعث میشه حجم image من زیاد نشه. پس Dockerfile رو به شکل زیر تعیین میکنم:
تو این دستور در مقابل کلمه کلیدی FROM اسم ایمیج که alpine هست آورده شده و بعد از `:` هم تگ اون ایمیج که latest هست اینجا آورده شده، این یعنی ما آخرین ورژن ایمیج alpine رو به عنوان بیس ایمیجمون انتخاب کردیم و حالا وقته اینه که دپندنسیهای دیگه رو روش نصب کنیم.
از اونجایی که برنامهمون با NodeJs نوشته شده پس قطعا نیاز داریم که NodeJs رو روی alpine و ایمیجمون داشته باشیم، پس از دستور RUN استفاده میکنم تا NodeJs رو نصب کنم، که میشه به شکل زیر:
دستور RUN این امکان رو بهمون میده که دستوراتی که جلوش مینویسیم (که اینجا `apk add --update nodejs npm` هست) رو روی shell سیستمعامل اجرا کنیم، یا به بیان ساده تر انگار اون دستور رو داریم توی ترمینال اون سیستمعامل اجرا میکنیم.
نکته: بسته به اینکه سیستمعاملی که انتخاب میکنید چی هست، دستوری که برای نصب nodejs استفاده میکنید متفاوت خواهد بود، مثلا اگه بیسمون ubuntu بود از دستور `apt install
` استفاده می کردیم.
خب تا اینجا NodeJs رو هم نصب کردیم، ولی این کار زیاد درست و استاندارد نبود! چرا؟ چون تقریبا همهی این تکنولوژیها مثل NodeJs خودشون نسخه رسمی ایمیجی رو در اختیارمون قرار میدن که NodeJs روش نصب هست و احتمالا به بهترین شکل ممکنه کانفیگ شده.
انتخاب image base مناسب
مثلا ما اگه میخواییم سیستمعاملمون alpine باشه باید ایمیجی از nodejs استفاده کنیم که روی alpine نصب شده، اینطوری ما یه alpine داریم که روش nodejs از قبل نصب شده و دیگه نیاز نیس ما نصبش کنیم. برای پیدا کردن این ایمیجها میتونیم به Docker Hub مراجعه کنیم و ایمیجی که میخواییم رو سرچ کنیم که معمولا لینکشون تو خود سایت این تکنولوژیها هست.
برای NodeJs میتونیم به صفحه رسمیش تو داکرهاب بریم (https://hub.docker.com/_/node)، که اگه یکم بریم پایینتر میتونیم این قسمت که راجع به ورژن alpine توضیح میده رو میبینیم که توضیح داده شامل چه چیزهایی میشه و حجمش چقدره:
از اونجایی که آخرین ورژن stableه NodeJs درحال حاضر `18.17.0` هست پس اسم ایمیجی که میخواییم به عنوان base ازش استفاده کنیم میشه `node:18.17-alpine` از کجا میدونم؟ چون اینجا نوشته :)
البته اگه میخوایین دقیقتر دنبال یه image مشخص بگردین میتونین به قست tags برید و ایمیج مورد نظر خودتون رو پیدا کنید:
البته میتونید از این افزونهها هم روی ادیتورتون استفاده کنید برای پیدا کردن ایمیجی که می خوایین:
نکته: سعی کنید همیشه از ایمیجهای رسمی استفاده کنید، یا اگه نبود حتما نگاه کنید چقدر دانلود یا ستاره داره، اینطوری میتونید مطمعن باشید imageی که انتخاب میکنید مشکلی نداره:
پس با این تعریف، Dockerfileمون رو به شکل زیر تغییر میدیم:
و دیگه نیازی به `RUN apk add --update nodejs npm` هم نداریم.
تا اینجا یه سیستمعامل داریم که NodeJs هم روش نصب شده و بنظر میاد همین برای پروژهمون کافی باشه تا بتونه با موفقیت اجرا بشه، پس میریم سراغ اینکه خود پروژهمون رو تو ایمیج قرار بدیم. ولی قبل از اون بیاییم با دستور WORKDIR آشنا بشیم.
کاربرد WORKDIR
با استفاده از این دستور میتونیم مشخص کنیم دایرکتوری جاری (current directory) که دستورات بعدی قراره اجرا بشن کجاست، خیلی ساده بخوام بگم وقتی داکر به این خط میرسه مثل این میمونه که ما از دستور cd استفاده کردیم تا داریکتوریمون رو تغییر بدیم به مسیری که مورد نظرمونه، با این تفاوت که حتی اگه اون دایرکتوری وجود نداشت داکر برامون اون دایرکتوری رو میسازه. پس دستور زیر مارو منتقل می کنه به `home/app/`.
WORKDIR /home/app
یعنی اگه ما بعد از این دستور از pwd استفاده کنیم تا دایرکتوری جاری رو ببینیم، خروجی `home/app/` خواهد بود.
کپی کردن پروژه
خب حالا برگردیم به جایی که میخواستیم پروژه رو به ایمیج اضافه کنیم، برای این کار از دستور COPY استفاده میکنیم تا فایلهای پروژه رو کپی کنیم تو ایمیج، ولی قبل از اون WORKDIR رو به home/app/ تغییر میدم، چون نمیخواییم فایلهای پروژه رو تو root سیستمعامل بریزیم، میخواییم جایی کپی کنیم که مشخصا میدونیم کجاست تا بتونیم کنترول روش داشته باشه باشم و مدیریتش کنیم. پس داکر فایل رو به شکل زیر تغییر میدیم:
FROM node:18.17-alpine WORKDIR /home/app
حالا وقتشه که از دستور COPY برای کپی کردن فایلهای پروژه استفاده کنیم، ساختار این دستور به شکل زیره:
COPY <source> <destination>
پس با این تعریف من دستور کپی رو به شکل زیر به داکرفایلم اضافه میکنم:
FROM node:18.17-alpine WORKDIR /home/app COPY . .
که این دستور دارکتوری جاری روی سیستممون (با توجه به build context موقع اجرا docker build - جلوتر راجع بهش حرف میزنیم) رو کپی میکنه تو home/app/ تو کانتینر یا ایمیج.
تا اینجا خوده پروژه رو هم کپی کردیم، حالا وقت اینه که دپندنسیهای خود برنامه رو نصب کنیم، از اونجایی که پروژه NodeJs هست باید با استفاده از دستور npm i دپندنسیهایی که تو packge.json مشخص شد رو نصب کنیم. پس برای این کار از دستور RUN استفاده به شکل زیر استفاده میکنیم:
FROM node:18.17-alpine WORKDIR /home/app COPY . . RUN npm i
حالا که همه دپندنسیها فراهم شدن وقت این رسیده که با CMD دستور پیشفرضی که قراره بعد از ساختن کانتینر از روی این ایمیج اجرا بشه رو تعیین کنیم. از اونجایی که ما میخوایم پروژه رو اجرا کنیم پس دستور اجرایی میتونه npm run start باشه تا پروژه اجرا بشه، پس داکرفایل رو به شکل زیر تغییر میدیم:
FROM node:18.17-alpine WORKDIR /home/app COPY . . RUN npm i CMD npm run start
خب دیگه پروژه با موفقیت Dockerize شده و الان وقت این رسیده که از روش یه ایمیج ایجاد کنیم و تستش کنیم.
برای ایجاد ایمیج کافیه که به دایرکتوری که Dockerfile قرار داره بریم و دستور زیر رو اجرا کنیم:
docker build . -t backend
نکته: دات که بعد از build اومده مشخص کننده build context و محلی هست که داکر باید دنبال Dockerfile برای ساخت ایمیج بگرده. برای اطلاعات بیشتر ای قسمت از داکیومنت داکر رو بخونید.
بعد از اجرا دستور خروجیمون به شکل زیر باید باشه:
برای نمایش ایمیج ایجاد شده هم میتونیم از دستور زیر استفاده کنیم:
docker images
حالا که ایمیج ایجاد شده، زمان این رسیده که با ایجاد یک container پروژه رو اجرا کنیم، برای این کار از دستور زیر استفاده می کنیم:
docker run backend
نکته: backend اسم ایمیجی هست که زمان ایجاد انتخاب کردیم با استفاده از t-
بعد از اجرا این دستور پروژه باید با موفقت اجرا شده باشه و تمام :)
تبریک میگم، شما پروژه رو با موفقیت Dockerize و درنهایت اجرا کردید :)
اول اینکه، دستوراتی که تو داکرفایل استفاده میشن، خیلی بیشتر از دستوراتی هستن که من استفاده کردم برای این پروژه، برای همین توصیه میکنم حتما داکیومنت داکر رو مرجع قرار بدید و دستورات دیگه رو یاد بگیرید.
نکته بعدی اینکه سناریویی که من برای ایجاد این Dockerfile استفاده کردم بهمینه نیس و خیلی از best practice ها رو توش رعایت نکردم بخاطر پیچیده نشدن فرایند، ولی توی کامنت بگید بنظرتون کجا میتونست بهتر باشه؟ چه best practiceهایی رو رعایت نکردم؟ میتونید از ریپوزیتوری پروژه که داکرفایلهای دیگه هم توش هستن تقلب کنید ?
تو نوشته بعدی راجع به این بهینه سازی و best practice ها صحبت خواهیم کرد، پس اگه براتون سواله یا میخوایین ببینین درست حدس زدید یا نه نوشته بعدی رو هم دنبال کنید.
در پایان حتما کامنت بزارین و نظرتون رو بگید، خیلی بهم کمک میکنه، بگید مثلا این کنجکاویها رو کجاها بیشتر بریم توش. اگه نوشته رو دوست داشتید با دوستان به اشتراک بزارید که هم بهشون کمک کنه و هم انرژی بشه برای من که ادامه بدم :)
و اینکه میتونید منو با آیدی mehdi_zarepour تو توییتر پیدا کنید، نوشتههای جدید رو اونجا توییت میکنم. فعلا نوشتههام راجع به داکر خواهد بود.