👋 قبل از شروع
این آموزش برای کسی نوشته شده که تا امروز اصلاً داکر کار نکرده است. قرار نیست فقط بفهمی داکر چیست؛ قرار است تا آخر این صفحه یک برنامهٔ واقعی بسازی که داخل کانتینر اجرا میشود، یک ایمیج حرفهای و امن از آن بگیری، و یک سیستم دو سرویسی (برنامه + پایگاه داده) را با یک دستور بالا بیاوری.
هر مرحله یک نتیجهٔ دیدنی دارد. اگر چیزی را ندیدی، یعنی جایی اشتباه شده و بخش رفع اشکال همان را پوشش میدهد.
- یک کامپیوتر ویندوز ۱۰ یا ۱۱ نسخهٔ ۶۴ بیتی — برای دیدنش کلید Windows + I را بزن و در بخش سیستم، قسمت «درباره» را ببین.
- حداقل ۸ گیگابایت رم (با ۴ گیگابایت هم راه میافتد ولی تنگ است) و حدود ۱۰ گیگابایت فضای خالی روی درایو C.
- قابلیت مجازیسازی روشن باشد — در مرحلهٔ ۱ دقیقاً یادت میدهم چطور در ۱۰ ثانیه چکش کنی.
- یک حساب کاربری که اجازهٔ نصب نرمافزار داشته باشد.
- اینترنت — بهخصوص در مرحلههای ۱ و ۲ که چند صد مگابایت دانلود میشود.
- تو ویندوز داری. اگر لینوکس یا مک داری، فقط بخش «نصب» فرق میکند؛ همهٔ دستورهای داکر در این آموزش عیناً یکساناند.
- هیچ دانش قبلی از داکر، لینوکس، شبکه یا برنامهنویسی لازم نیست. کدهای لازم را کامل به تو میدهم.
- تو در ایران هستی؛ برای همین مرحلهٔ ۲ کاملاً به رفع محدودیت دسترسی اختصاص دارد.
نرمافزار Docker Desktop برای استفادهٔ شخصی، آموزشی و شرکتهای کوچک رایگان است و برای سازمانهای بزرگ اشتراک پولی میخواهد (شرایط لایسنس ممکن است تغییر کند؛ ملاک، صفحهٔ رسمی خودشان است). خودِ موتور داکر متنباز و رایگان است.
نکتهٔ مهمتر: انبار رسمی ایمیجها یعنی Docker Hub از ایران معمولاً در دسترس نیست. این یک خرابی از طرف تو نیست و راهحل استانداردی دارد که در مرحلهٔ ۲ انجامش میدهیم.
داکر را نصب کن و مطمئن شو زنده است
قبل از اختراع کانتینرهای باربری، هر کشتی را دستی و جعبهجعبه بار میزدند؛ هر بندر هم روش خودش را داشت. کانتینر یک جعبهٔ استاندارد ساخت: هر چیزی داخلش باشد، همهٔ بندرها بلدند با آن کار کنند.
داکر همین کار را با نرمافزار میکند: برنامهات را با هر چیزی که برای اجرا لازم دارد داخل یک جعبهٔ استاندارد میگذارد، و بعد آن جعبه روی لپتاپ تو، لپتاپ همکارت و سرور شرکت دقیقاً یکجور اجرا میشود.
با این تفاوت که این جعبه برخلاف کانتینر واقعی، ساختنِ کپیاش چند ثانیه طول میکشد، جای اضافه نمیگیرد، و میتوانی هزار تای آن را همزمان داشته باشی.
-
اول چک کن مجازیسازی روشن است: Ctrl + Shift + Esc را بزن تا مدیر وظیفه باز شود، به تب عملکرد (Performance) برو و روی CPU کلیک کن.
در پنل سمت راست باید خطی به اسم Virtualization ببینی که جلویش نوشته Enabled. مجازیسازی یعنی توانایی پردازنده برای اجرای یک کامپیوتر کوچک داخل کامپیوتر تو — داکر روی ویندوز دقیقاً به همین نیاز دارد.
-
اگر جلوی آن خط نوشته بود Disabled، کامپیوتر را خاموشروشن کن و وارد تنظیمات اولیهٔ مادربرد (BIOS یا UEFI) شو و گزینهٔ مجازیسازی را روشن کن.
کلید ورود به این تنظیمات بسته به سازندهٔ دستگاه فرق میکند (روی خیلی از لپتاپها F2 و روی خیلی از دسکتاپها Del است و موقع روشن شدن، پایین صفحه مینویسد). اسم گزینه معمولاً چیزی شبیه Intel Virtualization Technology یا AMD SVM Mode است. اگر نوشته بود Enabled، این گام را رد کن و تیکش را بزن.
-
روی دکمهٔ ویندوز راستکلیک کن، Terminal (Admin) یا Windows PowerShell (Admin) را باز کن و این دستور را بزن:
wsl --install
این دستور زیرسیستم لینوکس ویندوز (WSL) را نصب میکند: یک لینوکس کوچک و سبک داخل ویندوز که داکر کانتینرهایش را آنجا اجرا میکند. بعد از تمام شدن، کامپیوتر را یک بار ریاستارت کن. اگر پیغام داد که از قبل نصب است، عالی است — برو گام بعد.
-
به سایت رسمی داکر برو و نسخهٔ ویندوزِ Docker Desktop را دانلود کن.
آدرس سایت
docker.comاست و صفحهٔ دانلود، خودش نسخهٔ مناسب ویندوز را پیشنهاد میدهد. حجم فایل حدود یک گیگابایت است. اگر سایت باز نشد، فایل نصب معمولاً روی سایتهای نرمافزار ایرانی هم آینه (کپی) میشود. -
فایل دانلودشده را اجرا کن و در پنجرهٔ نصب، گزینهٔ مربوط به استفاده از WSL 2 را تیکدار بگذار و نصب را تمام کن.
معمولاً همین گزینه از قبل تیک دارد. نصب حدود ۳ تا ۱۰ دقیقه طول میکشد و ممکن است آخرش بخواهد از ویندوز خارج و دوباره وارد شوی.
-
برنامهٔ Docker Desktop را باز کن و صبر کن تا پایین پنجره وضعیت موتور سبز شود.
بار اول ممکن است ۱ تا ۳ دقیقه طول بکشد و شرایط استفاده را نشان بدهد که باید بپذیری. تا وقتی نشانگر موتور سبز نشده، هیچ دستور داکری کار نمیکند؛ این عادی است.
-
یک پنجرهٔ PowerShell معمولی (بدون دسترسی مدیر) باز کن و بنویس:
docker version
باید یک خروجی نسبتاً بلند ببینی که دو بخش دارد: بخش Client و بخش Server.
دستور docker version دو بخش دارد: Client یعنی همان دستوری که تو تایپ میکنی، و Server یعنی موتور داکر که واقعاً کار را انجام میدهد. اگر بخش Client نسخه داد ولی جلوی Server خطا دیدی، یعنی داکر نصب هست ولی موتورش خاموش است — کافی است Docker Desktop را باز کنی و منتظر بمانی. این دو حالت کاملاً فرق دارند و اکثر آدمها ساعتها اشتباهی دنبال نصب دوباره میگردند.
🔬 برای کنجکاوها: کانتینر با ماشین مجازی چه فرقی دارد؟
ماشین مجازی یک کامپیوتر کامل شبیهسازیشده است: یک سیستمعامل کامل با کرنل جدا. برای همین چند گیگابایت جا میگیرد و بالا آمدنش دقیقهها طول میکشد.
کانتینر اینطور نیست. چون A (همهٔ کانتینرها کرنلِ یک لینوکس مشترک را قرض میگیرند) → پس B (لازم نیست هرکدام یک سیستمعامل کامل داشته باشند) → به همین دلیل C (کانتینر در چند صد میلیثانیه بالا میآید و دهها مگابایت جا میگیرد).
جدا بودنشان از هم با دو قابلیت خود لینوکس انجام میشود: namespaces که تعیین میکند هر کانتینر «چه چیزی را ببیند» و cgroups که تعیین میکند «چقدر منابع مصرف کند».
حالا معلوم میشود چرا روی ویندوز به WSL نیاز داشتیم: کانتینرِ لینوکسی به کرنل لینوکس نیاز دارد، و ویندوز کرنل لینوکس ندارد. پس داکر یک لینوکس کوچک را داخل ویندوز نگه میدارد و کانتینرها را آنجا اجرا میکند. خیلیها فکر میکنند کانتینر «یک ماشین مجازی سبک» است، ولی در واقع اصلاً ماشین مجازی نیست؛ فقط یک فرایند جداشده روی همان کرنل است.
خروجی docker version باید هر دو بخش Client و Server را با شماره نسخه نشان بدهد. اگر جلوی Server پیغامی شبیه cannot connect to the Docker daemon دیدی، برو به رفع اشکال، ردیف اول.
عبور از محدودیت دسترسی — تنظیم میرور رجیستری
هر جعبهٔ آمادهٔ داکر جایی نگهداری میشود؛ به آن انبار مرکزی میگویند رجیستری (registry)، و مشهورترینش Docker Hub است. مثل یک کتابخانهٔ مرکزی بزرگ که تقریباً هر کتابی را دارد.
مشکل این است که در ورودی این کتابخانه، از ایران راهت نمیدهند. راهحل استاندارد، «شعبهٔ محلی» است: یک سرور واسط داخل کشور که همان کتابها را از کتابخانهٔ مرکزی میگیرد و به تو میدهد. به آن میگویند میرور (mirror) یعنی آینه.
-
اول عمداً بگذار خراب شود تا خطا برایت آشنا شود. در PowerShell بزن:
docker run hello-world
اگر پیغامهایی شبیه TLS handshake timeout یا unauthorized یا connection reset دیدی، هیچ اتفاق بدی نیفتاده — این دقیقاً همان محدودیتی است که الان درستش میکنیم. (اگر برایت کار کرد، یعنی اینترنتت از مسیر دیگری میرود؛ باز هم این مرحله را انجام بده تا سرعتت بیشتر شود.)
-
آدرس یک میرور فعال ایرانی را پیدا کن.
چند شرکت میزبانی و ابری ایرانی این سرویس را رایگان میدهند و آدرسش را در صفحهٔ مستندات خودشان منتشر میکنند (برای نمونه آروانکلود سالهاست چنین آدرسی دارد). این آدرسها هر چند وقت عوض میشوند، پس آدرس را از سایت خود آن سرویس بردار، نه از یک آموزش قدیمی. آدرس چیزی شبیه
https://docker.example.irخواهد بود. -
در Docker Desktop روی چرخدندهٔ تنظیمات کلیک کن و بخش Docker Engine را باز کن.
آنجا یک ویرایشگر متنی میبینی که داخلش چند خط با آکولاد نوشته شده. این همان فایل پیکربندی موتور داکر است که اسمش
daemon.jsonاست. -
کلید
registry-mirrorsرا با آدرس میرورت به همان متن اضافه کن و روی دکمهٔ اعمال و راهاندازی دوباره (Apply & restart) بزن.{ "registry-mirrors": ["https://docker.example.ir"] }بهجای
docker.example.irآدرس واقعی خودت را بگذار. اگر از قبل کلیدهای دیگری آنجا بود، پاکشان نکن؛ فقط بین آکولادها یک کاما بگذار و خط جدید را اضافه کن. مواظب باش ساختار JSON سالم بماند وگرنه موتور بالا نمیآید. -
صبر کن موتور دوباره سبز شود و همان دستور اول را تکرار کن:
docker run hello-world
این بار باید چند خط دانلود ببینی و بعد پیغام Hello from Docker! ظاهر شود.
اگر نمیخواهی تنظیمات موتور را عوض کنی (مثلاً روی سرور کسی دیگر هستی)، میتوانی همان یک بار آدرس میرور را مستقیم اول نام ایمیج بنویسی:
docker pull docker.example.ir/library/nginx:alpine
نکتهٔ ریز و مهم: برای ایمیجهای رسمی داکر باید کلمهٔ library/ قبل از نام بیاید؛ برای ایمیجهای غیررسمی نه.
ایمیجی که از میرور میگیری از دست یک شرکت دیگر رد شده است. برای یادگیری و پروژههای معمولی کاملاً عادی است، ولی برای پروژهٔ حساس یا سازمانی، یا رجیستری خصوصی خودت را بالا بیاور یا ایمیج را با اثر انگشت دیجیتالش (digest) قفل کن — در مرحلهٔ ۹ میبینی چطور.
🔬 برای کنجکاوها: وقتی docker run میزنی دقیقاً چه اتفاقی میافتد؟
یک زنجیرهٔ ساده اجرا میشود: اول داکر میگردد ببیند آن ایمیج روی کامپیوتر تو هست یا نه. اگر نبود، به رجیستری وصل میشود و لایههای ایمیج را دانلود میکند. بعد از روی ایمیج یک کانتینر میسازد، یک فایلسیستم قابلنوشتن رویش میگذارد، و بالاخره فرمان داخل کانتینر را اجرا میکند.
چون A (فقط قدم دوم به اینترنت نیاز دارد) → پس B (اگر ایمیج یک بار دانلود شده باشد، اجرای بعدی حتی بدون اینترنت هم کار میکند) → به همین دلیل C (خطای دانلود فقط بار اول رخ میدهد، و پیغام Unable to find image ... locally اصلاً خطا نیست، فقط گزارش همین جستوجوست).
باید در خروجی، جملهٔ Hello from Docker! This message shows that your installation appears to be working correctly. را ببینی. اگر هنوز خطای شبکه میگیری، برو به رفع اشکال، ردیف دوم.
اولین کانتینر واقعی: یک وبسرور در ۱۰ ثانیه
دو کلمه را از اینجا به بعد مدام میشنوی. ایمیج (image) مثل دستور پخت است: یک نسخهٔ ثابت و فقطخواندنی. کانتینر (container) مثل غذای پختهشده از روی آن دستور است: زنده، در حال اجرا، و قابل دور ریختن.
با این تفاوت که از یک دستور پخت میتوانی همزمان ده غذای کاملاً یکسان بپزی، هرکدام را جدا خاموش کنی و دور بریزی، و دستور پخت هیچ آسیبی نبیند.
-
یک وبسرور واقعی را با یک خط بالا بیاور:
docker run -d -p 8080:80 --name web nginx:alpine
یعنی: ایمیج
nginx:alpineرا بگیر، در پسزمینه اجرا کن (-d)، اسمش را بگذارweb، و پورت ۸۰۸۰ کامپیوتر من را به پورت ۸۰ داخل کانتینر وصل کن. -
مرورگر را باز کن و برو به
http://localhost:8080باید صفحهٔ خوشآمدگویی nginx را ببینی. همین الان یک وبسرور واقعی روی کامپیوترت اجرا شد، بدون اینکه چیزی نصب کنی.
-
ببین چه چیزی در حال اجراست:
docker ps
ستون STATUS باید چیزی شبیه Up 2 minutes نشان بدهد و ستون PORTS نگاشت پورتها را. دستور
docker ps -aکانتینرهای خاموش را هم نشان میدهد. -
صفحهٔ مرورگر را چند بار تازه کن، بعد بنویس:
docker logs web
هر بار که صفحه را باز کردی یک خط اینجا ثبت شده است. لاگ یعنی همان چیزی که برنامه در حال اجرا چاپ میکند؛ این پنجرهات به دنیای داخل کانتینر است.
-
خاموش و روشنش کن و بین این دو، مرورگر را تازه کن:
docker stop web docker start web
وقتی خاموش است مرورگر خطا میدهد، وقتی روشن میشود دوباره جواب میدهد.
stopکانتینر را نمیکُشد، فقط متوقفش میکند. -
حالا کاملاً پاکش کن و ببین ایمیج سر جایش هست:
docker rm -f web docker image ls
کانتینر رفت، ولی
nginxهنوز در فهرست ایمیجهاست. یعنی دفعهٔ بعد بدون دانلود، در یک ثانیه دوباره بالا میآید.
در -p 8080:80 عدد سمت چپ پورتی است روی کامپیوتر تو که در مرورگر تایپ میکنی، و عدد سمت راست پورتی است که برنامه داخل کانتینر روی آن گوش میدهد. اگر جابهجا بنویسی، یا خطای «پورت مشغول است» میگیری یا صفحه هرگز باز نمیشود. یادآور ساده: outside:inside.
اگر فقط میخواهی چیزی را امتحان کنی، اینطور اجرا کن تا بهمحض بسته شدن، کانتینر خودش پاک شود:
docker run --rm -it alpine sh
حالا داخل یک لینوکس کوچک هستی؛ ls بزن، بعد exit بزن. کانتینر ناپدید میشود و هیچ زبالهای نمیماند. سوئیچ -it یعنی «میخواهم تعاملی با آن حرف بزنم».
🔬 برای کنجکاوها: چرا بعضی کانتینرها بلافاصله میمیرند؟
هر کانتینر دقیقاً یک «فرمان اصلی» دارد. عمر کانتینر برابر است با عمر همان فرمان — نه یک ثانیه بیشتر.
چون A (کانتینر فقط تا وقتی فرمان اصلیاش در حال اجراست زنده میماند) → پس B (اگر آن فرمان کاری بکند و تمام شود، کانتینر بلافاصله خاموش میشود) → به همین دلیل C (docker run ubuntu فوراً میمیرد، چون فرمان پیشفرضش یک پوستهٔ خالی است که چیزی برای خواندن ندارد).
خیلیها فکر میکنند کانتینر مثل یک ماشین مجازی «روشن میماند»، ولی کانتینر یک ماشین نیست؛ یک فرایند است. برای همین nginx زنده میماند (چون همیشه منتظر درخواست است) ولی ubuntu نه.
باید صفحهٔ خوشآمد nginx را در مرورگر دیده باشی، و بعد از docker rm -f web خروجی docker ps دیگر ردیف web نداشته باشد ولی docker image ls هنوز nginx را نشان بدهد.
ایمیج، تگ و لایه — نگاه به درون جعبه
ایمیج یک فایل یکتکه نیست. مثل چند برگهٔ شفاف است که روی هم گذاشته شدهاند و با هم یک تصویر کامل میسازند. به هر برگه میگویند لایه (layer).
با این تفاوت که این برگهها فقطخواندنیاند و بین ایمیجهای مختلف مشترک میشوند؛ و وقتی کانتینر میسازی، یک برگهٔ سفیدِ قابلنوشتن روی همهشان گذاشته میشود که با پاک شدن کانتینر دور ریخته میشود.
-
فهرست ایمیجهایت را با اندازهشان ببین:
docker image ls
هر ردیف سه چیز مهم دارد: نام (REPOSITORY)، تگ (TAG) و شناسه (IMAGE ID).
-
یک نسخهٔ مشخص را دانلود کن تا فرق تگها را ببینی:
docker pull nginx:1.27-alpine
تگ یعنی همان چیزی که بعد از دو نقطه میآید و نسخه را مشخص میکند. اگر تگ ننویسی، داکر خودش
latestرا فرض میگیرد. -
دوباره فهرست را ببین و اندازهها را با فضای واقعی مقایسه کن:
docker image ls docker system df
جمع ستون اندازهها بیشتر از فضای واقعی اشغالشده است، چون لایههای مشترک فقط یک بار روی دیسک ذخیره میشوند.
-
تاریخچهٔ ساخته شدن یک ایمیج را ببین:
docker image history nginx:alpine
هر ردیف یک لایه است و نشان میدهد آن لایه با چه دستوری ساخته شده و چقدر حجم اضافه کرده. اینطوری میفهمی حجم یک ایمیج دقیقاً از کجا آمده.
-
نسخهٔ اضافی را پاک کن:
docker image rm nginx:1.27-alpine
اگر پیغام داد که ایمیج در حال استفاده است، یعنی کانتینری از رویش ساخته شده؛ اول آن کانتینر را حذف کن.
خیلیها فکر میکنند latest یعنی «همیشه آخرین نسخه»، ولی latest فقط یک برچسب پیشفرض است که ممکن است ماهها بهروز نشده باشد یا فردا صبح ناگهان به یک نسخهٔ کلان جدید اشاره کند. روی سرور همیشه نسخهٔ مشخص بنویس: nginx:1.27-alpine نه nginx:latest.
اگر ایمیج نهاییات از چند صد مگابایت گذشت، معمولاً ایراد از پایهٔ آن است نه از کد تو. بیشتر ایمیجهای معروف یک نسخهٔ کوچک بر پایهٔ Alpine Linux یا نسخهٔ slim دارند که همان کار را با حجم بسیار کمتر انجام میدهد.
⚡ ۵ تا ۱۰ برابر دانلود و فضای کمتر روی هر سرور🔬 برای کنجکاوها: چرا دو ایمیج ۲۰۰ مگابایتی، ۴۰۰ مگابایت جا نمیگیرند؟
هر لایه با اثر انگشت دیجیتال محتوایش شناخته میشود، نه با نامش. اگر دو ایمیج از یک پایهٔ مشترک ساخته شده باشند، لایههای پایهشان دقیقاً یکسان است و داکر آن را فقط یک بار روی دیسک نگه میدارد و به هر دو ایمیج «قرض» میدهد.
همین موضوع دو نتیجهٔ عملی مهم دارد. اول، اگر همهٔ سرویسهایت را روی یک پایهٔ مشترک بسازی، مجموع فضای اشغالی بهشدت کم میشود. دوم، وقتی نسخهٔ جدید برنامهات را روی سرور میفرستی، فقط لایههای تغییرکرده منتقل میشوند؛ برای همین آپدیت دوم همیشه خیلی سریعتر از آپدیت اول است.
در خروجی docker image history nginx:alpine باید چند ردیف با ستون SIZE ببینی، و بعد از حذف، docker image ls دیگر تگ 1.27-alpine را نشان ندهد.
اولین ایمیج خودت را بساز (Dockerfile)
تا اینجا از جعبههای آمادهٔ دیگران استفاده کردی. حالا خودت دستور پخت مینویسی. فایلی به اسم Dockerfile دقیقاً همان دستور پخت است: از چه پایهای شروع کن، چه چیزی داخلش بریز، و آخر سر چه فرمانی را اجرا کن.
با این تفاوت که این دستور پخت را کامپیوتر اجرا میکند، پس هیچ مرحلهای را نمیشود «حدس زد» — هر چیزی که برنامهات لازم دارد باید صریح نوشته شود.
-
یک پوشهٔ کاری بساز و واردش شو. در PowerShell بزن:
mkdir C:\docker-shop cd C:\docker-shop
مسیر را بدون فاصله و بدون حروف فارسی انتخاب کن؛ بعضی دستورها با مسیرهای فاصلهدار دردسر درست میکنند.
-
داخل همین پوشه فایلی به نام
server.jsبساز و این کد را داخلش بگذار.میتوانی با Notepad بسازی، ولی موقع ذخیره حتماً نوع فایل را روی «همهٔ فایلها» بگذار تا پسوند
.txtبه آخرش اضافه نشود. این برنامه یک وبسرور کوچک است و هیچ کتابخانهٔ بیرونی لازم ندارد.const http = require("http"); const net = require("net"); const os = require("os"); const PORT = process.env.PORT || 3000; const APP_NAME = process.env.APP_NAME || "Shop Web"; const DB_HOST = process.env.DB_HOST || "db"; const DB_PORT = Number(process.env.DB_PORT || 5432); function checkDb() { return new Promise((resolve) => { const socket = net.connect({ host: DB_HOST, port: DB_PORT }); const finish = (ok) => { socket.destroy(); resolve(ok); }; socket.setTimeout(1500); socket.on("connect", () => finish(true)); socket.on("timeout", () => finish(false)); socket.on("error", () => finish(false)); }); } const server = http.createServer(async (req, res) => { if (req.url === "/health") { res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ status: "ok" })); } const dbUp = await checkDb(); res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end( "<h1>" + APP_NAME + "</h1>" + "<p>container: " + os.hostname() + "</p>" + "<p>database: " + (dbUp ? "connected" : "unreachable") + "</p>" ); }); server.listen(PORT, "0.0.0.0", () => { console.log("listening on port " + PORT); }); -
در همان پوشه فایل
package.jsonرا بساز:{ "name": "docker-shop-web", "version": "1.0.0", "private": true, "main": "server.js", "scripts": { "start": "node server.js" } }این فایل شناسنامهٔ پروژهٔ Node.js است و میگوید برنامه با چه دستوری اجرا میشود.
-
حالا دستور پخت را بنویس: فایلی به نام
Dockerfile— بدون هیچ پسوندی.FROM node:22-alpine WORKDIR /app COPY package.json ./ RUN npm install --omit=dev COPY . . EXPOSE 3000 CMD ["node", "server.js"]
خط به خط: از پایهٔ Node.js نسخهٔ ۲۲ روی Alpine شروع کن؛ پوشهٔ کارت داخل کانتینر
/appباشد؛ اول فقط شناسنامه را کپی کن و کتابخانهها را نصب کن؛ بعد بقیهٔ کد را کپی کن؛ اعلام کن روی پورت ۳۰۰۰ گوش میدهی؛ و در آخر برنامه را اجرا کن. -
ایمیج را بساز (به نقطهٔ آخر دستور دست نزن):
docker build -t shop-web:1.0.0 .
نقطهٔ آخر یعنی «مصالح ساخت را از همین پوشه بردار».
-tهم نام و تگ ایمیج را تعیین میکند. بار اول چند دقیقه طول میکشد چون پایهٔ Node.js دانلود میشود. -
ایمیج خودت را اجرا کن:
docker run -d -p 8080:3000 --name shop -e APP_NAME="Rayon Shop" shop-web:1.0.0
حالا برو به
http://localhost:8080. باید نام برنامه، نام کانتینر، و عبارت database: unreachable را ببینی — پایگاه داده هنوز ساخته نشده و این کاملاً درست است. سوئیچ-eیعنی «یک متغیر محیطی به کانتینر بده».
دلیل اینکه اول فقط package.json را کپی کردیم و بعد بقیهٔ کد را، این است: داکر نتیجهٔ هر خط را کش میکند و تا اولین خطی که ورودیاش عوض شده جلو میرود. اگر COPY . . را قبل از نصب کتابخانهها بگذاری، با هر تغییر کوچک در کد، همهٔ کتابخانهها از نو نصب میشوند.
در کد ما عمداً نوشتیم server.listen(PORT, "0.0.0.0"). اگر برنامهای فقط روی localhost داخل کانتینر گوش بدهد، یعنی «فقط از داخل همین جعبه با من حرف بزنید» و هرچقدر هم پورت را درست نگاشت کنی، مرورگرت هیچوقت جواب نمیگیرد. این یکی از گیجکنندهترین خطاهای شروع کار است، چون کانتینر سالم و Up است.
🔬 برای کنجکاوها: چرا CMD را به شکل آرایه مینویسیم؟
دو شکل نوشتن وجود دارد: CMD node server.js و CMD ["node", "server.js"]. شکل اول برنامه را داخل یک پوستهٔ واسط اجرا میکند، شکل دوم مستقیم.
چون A (سیگنال خاموش شدن به فرایند اصلی کانتینر میرسد) → پس B (اگر یک پوسته فرایند اصلی باشد، سیگنال به برنامهٔ تو منتقل نمیشود) → به همین دلیل C (کانتینر موقع docker stop بهجای بستن تمیز، ۱۰ ثانیه منتظر میماند و بعد به زور کشته میشود و کارهای نیمهتمام از بین میروند).
یک تفاوت دیگر هم هست: EXPOSE هیچ پورتی را باز نمیکند و فقط مستندسازی است. چیزی که واقعاً پورت را باز میکند، -p هنگام اجراست.
دستور docker image ls shop-web باید ردیفی با تگ 1.0.0 نشان بدهد و در مرورگر روی پورت ۸۰۸۰ باید نام برنامه و خط database: unreachable را ببینی. اگر بیلد خطا داد، متن خطا معمولاً شمارهٔ خط Dockerfile را میگوید؛ همان خط را با متن بالا مقایسه کن.
دادهها کجا میروند؟ ولوم و اتصال پوشه
داخل کانتینر مثل یک تختهوایتبرد است: هرچه رویش بنویسی، لحظهای که کانتینر پاک شود کاملاً پاک میشود. ولوم (volume) مثل یک دفترچه است که بیرون تخته میماند و هر کانتینر جدیدی میتواند همان دفترچه را باز کند.
با این تفاوت که این دفترچه را داکر خودش مدیریت میکند و میشود همزمان به چند کانتینر دادش.
-
اول عمداً یک داده را از دست بده. یک کانتینر ساده بساز و داخلش فایل بنویس:
docker run -d --name loss alpine sleep 3600 docker exec loss sh -c "echo salam > /note.txt" docker exec loss cat /note.txt
باید کلمهٔ salam چاپ شود. دستور
execیعنی «این فرمان را داخل کانتینرِ در حال اجرا انجام بده». -
حالا پاکش کن، دوباره بسازش و همان فایل را بخواه:
docker rm -f loss docker run -d --name loss alpine sleep 3600 docker exec loss cat /note.txt
این بار خطای No such file or directory میگیری. فایل نابود شد. این مهمترین چیزی است که باید دربارهٔ کانتینرها بدانی.
-
یک ولوم نامدار بساز:
docker volume create shop-data
-
کانتینری بساز که آن ولوم را روی پوشهٔ
/dataسوار کرده باشد و داخلش بنویس:docker rm -f loss docker run -d --name keeper -v shop-data:/data alpine sleep 3600 docker exec keeper sh -c "echo dade-mohem > /data/note.txt"
در
-v shop-data:/dataسمت چپ نام ولوم است و سمت راست مسیری داخل کانتینر که ولوم آنجا سوار میشود. -
حالا همان آزمایش قبلی را تکرار کن: کانتینر را نابود کن، دوباره بساز و بخوان:
docker rm -f keeper docker run -d --name keeper -v shop-data:/data alpine sleep 3600 docker exec keeper cat /data/note.txt
این بار داده سر جایش است. کانتینر رفت، دفترچه ماند.
-
فهرست ولومهایت را ببین و بعد کانتینر آزمایشی را پاک کن:
docker volume ls docker rm -f keeper
دقت کن که با پاک کردن کانتینر، ولوم
shop-dataهمچنان در فهرست میماند.
یک کانتینر موقت بساز که هم ولوم را ببیند و هم پوشهٔ فعلی تو را، و کل محتوا را در یک فایل فشرده بریزد:
docker run --rm -v shop-data:/data -v ${PWD}:/backup alpine tar czf /backup/shop-data.tar.gz -C /data .
فایل shop-data.tar.gz در پوشهٔ فعلیات ساخته میشود و همهچیز داخلش است. (این نوشتار برای PowerShell است؛ در CMD بهجای ${PWD} بنویس %cd% و در لینوکس و مک $(pwd).) برای بازگرداندن، همین دستور را با tar xzf برعکس اجرا میکنی.
اگر بنویسی -v /data بدون نام، داکر یک ولوم بینام با یک کد طولانی میسازد. بعد از چند هفته دهها ولوم ناشناس داری که نمیدانی کدام مال کدام پروژه است و جرئت نمیکنی هیچکدام را پاک کنی. برای دیدنشان بزن:
docker volume ls -f dangling=true⚡ جلوگیری از اشغال چند ده گیگابایت دادهای که کسی نمیداند مال چیست
🔬 برای کنجکاوها: ولوم بهتر است یا وصل کردن مستقیم یک پوشه؟
حالت دوم را اتصال پوشه (bind mount) میگویند: یک پوشهٔ واقعی از کامپیوتر خودت را داخل کانتینر سوار میکنی، مثلاً -v ${PWD}:/app. این برای توسعه عالی است چون کدت را ویرایش میکنی و بلافاصله داخل کانتینر عوض میشود.
ولی برای دادهٔ واقعی روی سرور، ولوم بهتر است: داکر مدیریتش میکند، مجوزهای فایل درست تنظیم میشوند، پشتیبانگیریاش استاندارد است، و به ساختار پوشههای میزبان وابسته نیستی.
یک نکتهٔ عملی مهم روی ویندوز: وقتی پوشهای از درایو C ویندوز را داخل کانتینر سوار میکنی، فایلها باید از مرز بین ویندوز و لینوکس رد شوند و همین کار را کند میکند. اگر پروژهات بزرگ شد و بیلد و اجرا کند بود، بردن پوشهٔ پروژه به داخل خود WSL تفاوت محسوسی میسازد.
بعد از نابود کردن و ساختن دوبارهٔ کانتینر keeper، دستور docker exec keeper cat /data/note.txt باید عبارت dade-mohem را چاپ کند، و docker volume ls باید shop-data را نشان بدهد.
چند سرویس با هم: Docker Compose
تا اینجا هر کانتینر را دستی و با یک دستور طولانی روشن میکردی؛ مثل اینکه برای هر صحنهٔ نمایش، بازیگرها را یکییکی صدا بزنی و به هرکدام جداگانه بگویی کجا بایستد.
Compose فیلمنامه است: در یک فایل مینویسی چه سرویسهایی لازم داری و هرکدام چه تنظیماتی دارند، و بعد با یک دستور همه با هم و به ترتیب درست بالا میآیند. با این تفاوت که این فیلمنامه را میشود به همکارت داد و او دقیقاً همان صحنه را بالا بیاورد.
-
در همان پوشهٔ
C:\docker-shopفایلی به نامcompose.yamlبساز:services: web: build: . ports: - "8080:3000" environment: APP_NAME: "Rayon Shop" DB_HOST: db DB_PORT: 5432 depends_on: db: condition: service_healthy restart: unless-stopped db: image: postgres:16-alpine environment: POSTGRES_USER: shop POSTGRES_PASSWORD: change-me-123 POSTGRES_DB: shopdb volumes: - dbdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U shop -d shopdb"] interval: 5s timeout: 3s retries: 10 restart: unless-stopped volumes: dbdata:در فایلهای YAML فاصلههای ابتدای خط معنا دارند و باید فقط با فاصله باشند، نه کلید Tab. اگر خطای عجیب گرفتی، اول همین را چک کن.
-
کانتینر قدیمی را کنار بگذار تا پورت آزاد شود:
docker rm -f shop
-
کل سیستم را با یک دستور بالا بیاور:
docker compose up -d
دقت کن
docker composeبا فاصله نوشته میشود؛ شکل قدیمیdocker-composeبا خط تیره مربوط به نسخههای قدیمی است. بار اول، دانلود پایگاه دادهٔ PostgreSQL چند دقیقه طول میکشد. -
وضعیت سرویسها را ببین:
docker compose ps
باید دو ردیف ببینی و جلوی
dbعبارتی شبیه healthy بیاید. اگر هنوز starting است، چند ثانیه صبر کن و دوباره بزن. -
برو به
http://localhost:8080حالا باید بنویسد database: connected. یعنی کانتینر برنامه توانست کانتینر پایگاه داده را با نام
dbپیدا کند و به آن وصل شود. -
همه را با هم بخوابان و ببین ولوم باقی میماند:
docker compose down docker volume ls
کانتینرها و شبکه پاک شدند، ولی ولومی که اسمش با پیشوند پروژه ساخته شده هنوز هست. برای ادامهٔ کار دوباره
docker compose up -dبزن.
این دو خط در پنج ثانیه به تو میگویند کدام سرویس مشکل دارد و چرا:
docker compose ps docker compose logs -f --tail 50 web
اولی وضعیت هر سرویس را میدهد و دومی آخرین ۵۰ خط لاگ همان سرویس را زنده نشان میدهد (با Ctrl + C بیرون بیا). اگر نام سرویس را ننویسی، لاگ همه را با رنگهای جدا میبینی.
⚡ تشخیص علت خرابی در ۵ ثانیه، بهجای نیمساعت حدس زدندر فایل بالا نوشتیم DB_HOST: db چون Compose برای سرویسها یک شبکهٔ خصوصی میسازد و نام هر سرویس داخل آن شبکه تبدیل به یک آدرس معتبر میشود. اگر آنجا localhost بنویسی، برنامه سراغ خودِ کانتینر خودش میرود و خطای connection refused میگیری. این شایعترین خطای اتصال بین دو کانتینر است.
دستور docker compose down -v علاوه بر کانتینرها، ولومها را هم پاک میکند — یعنی کل پایگاه دادهٔ آن پروژه. تا وقتی مطمئن نیستی، هرگز -v را اضافه نکن.
🔬 برای کنجکاوها: چرا depends_on بهتنهایی کافی نیست؟
depends_on بهتنهایی فقط ترتیب شروع را تضمین میکند، نه آمادگی را. یعنی داکر کانتینر پایگاه داده را زودتر روشن میکند، ولی منتظر نمیماند تا پایگاه داده واقعاً آمادهٔ پذیرش اتصال شود.
چون A (فرایند پایگاه داده چند ثانیه بعد از روشن شدن هنوز در حال آمادهسازی است) → پس B (برنامهای که فوراً وصل میشود خطا میگیرد و خاموش میشود) → به همین دلیل C (باید یک بررسی سلامت واقعی تعریف کنیم و شرط را روی service_healthy بگذاریم).
کاری که pg_isready در فایل ما میکند دقیقاً همین است: هر ۵ ثانیه از خود پایگاه داده میپرسد «آمادهای؟» و تا وقتی جواب مثبت ندهد، سرویس web اصلاً شروع نمیشود. با این حال، در سیستمهای واقعی باز هم برنامه باید بلد باشد اتصال قطعشده را دوباره تلاش کند؛ بررسی سلامت جای کد مقاوم را نمیگیرد.
خروجی docker compose ps باید دو سرویس web و db را در حال اجرا نشان بدهد و صفحهٔ مرورگر روی پورت ۸۰۸۰ باید بنویسد database: connected. اگر نوشت unreachable، لاگ سرویس db را ببین.
ایمیج حرفهای: چندمرحلهای، سبک و بدون کاربر ریشه
وقتی کیک میپزی، آشپزخانه پر از ظرف و ابزار و پوست تخممرغ میشود؛ ولی چیزی که به مهمان میدهی فقط کیک است، نه کل آشپزخانه.
بیلد چندمرحلهای (multi-stage build) دقیقاً همین است: در مرحلهٔ اول با همهٔ ابزارها میسازی، و در مرحلهٔ دوم فقط محصول نهایی را به یک ایمیج تمیز منتقل میکنی. با این تفاوت که اینجا آشپزخانه بعد از کار کاملاً ناپدید میشود و هیچ ردی در ایمیج نهایی نمیگذارد.
-
در پوشهٔ پروژه فایلی به نام
.dockerignoreبساز (با یک نقطه شروع میشود):node_modules npm-debug.log .git .env Dockerfile compose.yaml *.md
ویندوز اجازه نمیدهد اسم فایل را در File Explorer با نقطه شروع کنی؛ یا از یک ویرایشگر کد استفاده کن یا در PowerShell بزن
New-Item .dockerignoreو بعد ویرایشش کن. -
محتوای
Dockerfileرا کامل با این نسخه جایگزین کن:# ---------- build stage ---------- FROM node:22-alpine AS build WORKDIR /app COPY package.json ./ RUN npm install --omit=dev COPY . . # ---------- runtime stage ---------- FROM node:22-alpine ENV NODE_ENV=production WORKDIR /app COPY --from=build --chown=node:node /app /app USER node EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \ CMD wget -qO- http://127.0.0.1:3000/health || exit 1 CMD ["node", "server.js"]
-
نسخهٔ جدید را بساز:
docker build -t shop-web:1.1.0 .
-
دو نسخه را کنار هم ببین:
docker image ls shop-web
در این برنامهٔ کوچک اختلاف اندازه کم است، چون ما ابزار ساخت سنگینی نداشتیم. صادقانه بگویم: ارزش واقعی این الگو در پروژههای واقعی است — مثلاً یک پروژهٔ React یا TypeScript که ایمیجش با این روش از حدود یک گیگابایت به حدود صد مگابایت میرسد.
-
اجرا کن و ببین با چه کاربری اجرا میشود:
docker run -d -p 8081:3000 --name shop2 shop-web:1.1.0 docker exec shop2 whoami
باید بنویسد node و نه root. پورت ۸۰۸۱ را انتخاب کردیم تا با سرویس Compose که روی ۸۰۸۰ است تداخل نکند.
-
بعد از حدود نیم دقیقه، وضعیت سلامتش را بپرس:
docker ps docker inspect --format "{{.State.Health.Status}}" shop2در خروجی
docker psکنار وضعیت، عبارتی شبیه (healthy) میبینی. دستور دوم فقط همان یک کلمه را چاپ میکند.
کانتینرها بهطور پیشفرض با کاربر root اجرا میشوند و بیشتر آموزشها هم همینطور رهایش میکنند. یک خط USER node کافی است تا اگر روزی مهاجم از دل برنامهات نفوذ کرد، داخل کانتینر هم دستش بسته باشد. در ایمیجهای رسمی Node.js کاربری به نام node از قبل ساخته شده است.
خیلیها برای فهمیدن اینکه سرویس واقعاً سالم است ابزار جداگانه نصب میکنند، در حالی که خود داکر این را دارد. با یک خط HEALTHCHECK، داکر مرتب یک آدرس را صدا میزند و نتیجه را در docker ps نشان میدهد. مهمتر اینکه Compose میتواند شروع سرویسهای دیگر را به همین سلامت گره بزند — دقیقاً کاری که در مرحلهٔ ۷ کردیم.
🔬 برای کنجکاوها: چرا ایمیج کوچک فقط «سبک» نیست، بلکه امنتر هم هست؟
هر ابزاری که در ایمیج نهایی باقی بماند — کامپایلر، ابزار بستهبندی، کتابخانههای توسعه — دو هزینه دارد: حجم، و سطح حمله.
چون A (هر بستهای که داخل ایمیج است ممکن است روزی آسیبپذیری اعلامشده بگیرد) → پس B (هرچه بستههای بیشتری داشته باشی، احتمال اینکه ایمیجت آسیبپذیر شمرده شود بیشتر است) → به همین دلیل C (حذف کامل ابزارهای ساخت از ایمیج نهایی، همزمان هم حجم و هم ریسک امنیتی را کم میکند).
یک اشتباه رایج هم اینجاست: بعضیها فایل رمز و کلید را در مرحلهٔ ساخت کپی میکنند و بعد در خط بعد پاکش میکنند و خیال میکنند رفته است. ولی لایهها تاریخچه دارند؛ آن فایل هنوز در لایهٔ قبلی هست و قابل استخراج است. رمزها باید موقع اجرا بهصورت متغیر محیطی داده شوند، نه داخل ایمیج پخته شوند.
دستور docker exec shop2 whoami باید node چاپ کند و docker inspect --format "{{.State.Health.Status}}" shop2 بعد از حدود نیم دقیقه باید healthy بدهد.
نسخهگذاری و بردن ایمیج روی سرور
تگ مثل برچسبی است که روی جعبه میچسبانی. جعبه همان جعبه است؛ برچسب فقط اسمی است که تو با آن صدایش میزنی. برای همین یک جعبه میتواند همزمان چند برچسب داشته باشد.
با این تفاوت که هر جعبه یک شمارهٔ سریال تغییرناپذیر هم دارد (digest) که هیچوقت به جعبهٔ دیگری نمیچسبد — و روی سرورهای حساس، مرجع نهایی همان است.
-
یک برچسب دوم به همان ایمیج بزن و ببین شناسهشان یکی است:
docker tag shop-web:1.1.0 shop-web:latest docker image ls shop-web
هر دو ردیف IMAGE ID یکسان دارند، چون یک ایمیجاند با دو نام.
-
ایمیج را به یک فایل قابل حمل تبدیل کن:
docker save -o shop-web-1.1.0.tar shop-web:1.1.0
حالا در پوشهات یک فایل داری که کل ایمیج داخلش است. این فایل را میتوانی با هر روشی به سرور برسانی.
-
همان فایل را دوباره بارگذاری کن تا مطمئن شوی سالم است:
docker image rm shop-web:1.1.0 docker load -i shop-web-1.1.0.tar docker image ls shop-web
روی سرور مقصد هم دقیقاً همین دستور
docker loadاجرا میشود. (اگر گفت ایمیج در حال استفاده است، اولdocker rm -f shop2بزن.) -
اگر به یک رجیستری دسترسی داری، مسیر استاندارد این است:
docker login docker tag shop-web:1.1.0 USERNAME/shop-web:1.1.0 docker push USERNAME/shop-web:1.1.0
بهجای USERNAME نام کاربریات در آن رجیستری را بگذار. توجه: ارسال به Docker Hub از داخل ایران معمولاً کار نمیکند؛ گزینههای عملی، رجیستری خصوصی روی سرور خودت یا سرویس رجیستری یک ارائهدهندهٔ داخلی است. اگر هیچکدام را نداری، این گام را رد کن و تیکش را بزن — مسیر
saveوloadکاملاً کافی است. -
اجرای «مثل سرور» را تمرین کن: با سیاست راهاندازی خودکار.
docker rm -f shop2 docker run -d -p 8081:3000 --restart unless-stopped --name shop2 shop-web:1.1.0
اگر خط اول گفت چنین کانتینری وجود ندارد، اشکالی ندارد — یعنی در گام قبل پاکش کردهای؛ فقط خط دوم را اجرا کن.
با
--restart unless-stoppedاگر کانتینر خراب شد یا سرور ریاستارت شد، داکر خودش دوباره بالایش میآورد — مگر اینکه خودت عمداً خاموشش کرده باشی.
خیلیها فکر میکنند بدون رجیستری نمیشود ایمیج را جابهجا کرد. در حالی که docker save یک فایل معمولی میسازد که با هر روشی — حتی حافظهٔ فلش — قابل انتقال است و روی مقصد با docker load برمیگردد. برای شرایط تحریم، سرور بدون اینترنت، یا شبکهٔ ایزولهٔ سازمانی، این استانداردترین راه است.
هر ایمیجی که به سرور میفرستی باید یک تگ یکتا و بازگشتپذیر داشته باشد، مثل 1.1.0 یا 1.1.0-2026-08-13. اگر فقط latest داشته باشی، وقتی نسخهٔ جدید خراب از آب درآمد هیچ راهی برای برگشتن نداری، چون نسخهٔ سالم قبلی دیگر اسمی ندارد. با تگ درست، بازگشت یعنی یک بار docker run با تگ قبلی.
🔬 برای کنجکاوها: تفاوت تگ با digest
تگ یک اشارهگر است و میتواند حرکت کند: امروز به یک ایمیج اشاره میکند و فردا صاحب رجیستری همان تگ را به ایمیج دیگری میچسباند. اما digest اثر انگشت ریاضیِ محتواست و هرگز تغییر نمیکند؛ شکلی شبیه sha256:9b2f... دارد.
برای دیدنش بزن docker image ls --digests. در محیطهای حساس، ایمیج را با digest اجرا میکنند، یعنی image@sha256:... بهجای image:tag. این کار تضمین میکند حتی اگر رجیستری یا میرور واسط عوض شود، دقیقاً همان بایتهای تأییدشده اجرا شوند.
باید فایل shop-web-1.1.0.tar در پوشهات باشد، و بعد از حذف و docker load، دستور docker image ls shop-web دوباره تگ 1.1.0 را نشان بدهد.
دیباگ، محدود کردن منابع و نظافت
تا اینجا از بیرون به جعبه نگاه میکردی. حالا یاد میگیری در جعبه را باز کنی و داخلش راه بروی، مقدار غذایی که مصرف میکند را محدود کنی، و جعبههای قدیمی را از انبار بیرون بریزی.
با این تفاوت که برخلاف انبار واقعی، اینجا بعضی دستورهای «نظافت» میتوانند دادههای واقعی را هم با خودشان ببرند — پس باید بدانی کدام امن است و کدام نه.
-
وارد کانتینر شو و داخلش بگرد:
docker exec -it shop2 sh
حالا داخل کانتینر هستی. بزن
lsتا فایلهای برنامه را ببینی، بعدenvتا متغیرهای محیطی را ببینی، و آخرشexitتا بیرون بیایی. خروج تو هیچ تأثیری روی اجرای کانتینر ندارد. -
لاگ زنده را با تعداد محدود ببین:
docker logs --tail 50 -f shop2
با Ctrl + C بیرون بیا. سوئیچ
--tailجلوی سرازیر شدن هزاران خط قدیمی را میگیرد. -
مصرف منابع همهٔ کانتینرها را ببین:
docker stats --no-stream
ستونهای مصرف پردازنده و حافظه را نشان میدهد. بدون
--no-streamاین نما زنده و مداوم بهروز میشود. -
یک کانتینر با سقف منابع اجرا کن:
docker run -d --name limited --memory 256m --cpus 0.5 --restart unless-stopped nginx:alpine
حالا این کانتینر هرچقدر هم شلوغ شود، بیش از ۲۵۶ مگابایت حافظه و نصف یک هستهٔ پردازنده نمیگیرد. روی سرور مشترک، این تفاوت بین «یک سرویس کند شد» و «کل سرور خوابید» است.
-
ببین داکر چقدر از دیسکت را گرفته:
docker system df
ستون RECLAIMABLE میگوید چقدرش قابل آزاد شدن است.
-
نظافت امن انجام بده و کانتینرهای تمرینی را جمع کن:
docker rm -f limited docker image prune
دستور
image pruneفقط ایمیجهای بینام و بیاستفاده (به آنها میگویند dangling) را پاک میکند و امن است. تأیید میخواهد؛yبزن.
اگر docker exec -it NAME bash زدی و خطای executable file not found گرفتی، اشتباه از تو نیست: ایمیجهای سبک برای کم کردن حجم فقط پوستهٔ ساده sh را دارند. همیشه اول sh را امتحان کن؛ روی ایمیجهای بزرگتر bash هم جواب میدهد.
وقتی در docker ps دیدی وضعیت مدام Restarting است، بهجای هر کار دیگری اول این را بزن:
docker logs --tail 30 NAME
هشت مورد از ده مورد، علت دقیقاً همانجا نوشته شده: یک متغیر محیطی جا افتاده، یک فایل پیدا نشده، یا پورت اشتباه. حدس زدن و بازسازی ایمیج، وقتِ تلفشده است.
⚡ رسیدن به علت واقعی در ۳۰ ثانیه بهجای نیمساعت آزمونوخطادستور docker system prune -a --volumes همهچیز را پاک میکند: همهٔ ایمیجهای بیاستفاده و همهٔ ولومهای بیاستفاده. اگر پایگاه دادهٔ پروژهای فعلاً بالا نباشد، ولومش «بیاستفاده» شمرده میشود و میرود. تا وقتی دقیقاً نمیدانی چه پاک میشود، به docker image prune بسنده کن.
🔬 برای کنجکاوها: سیاستهای راهاندازی دوباره چه فرقی دارند؟
چهار حالت وجود دارد. no یعنی هرگز خودکار بالا نیا. on-failure یعنی فقط اگر با خطا خاموش شدی. always یعنی همیشه، حتی اگر خودت دستی خاموشش کرده باشی (بعد از ریاستارت دیمن باز هم میآید). unless-stopped یعنی همیشه، مگر اینکه خودت عمداً متوقفش کرده باشی.
برای سرویسهای واقعی روی سرور معمولاً unless-stopped انتخاب درست است: هم در برابر خرابی و ریاستارت سرور مقاوم است، هم وقتی تو عمداً یک سرویس را خواباندی، شب نصفهشب خودش برنمیگردد.
یک هشدار: راهاندازی خودکار، مشکل را پنهان هم میکند. اگر کانتینری هر دقیقه ریاستارت میشود و کسی لاگ را نمیبیند، سرویس عملاً خراب است ولی در نگاه اول «بالا» به نظر میرسد. برای همین بررسی سلامت مرحلهٔ ۸ مکمل ضروری این تنظیم است.
باید توانسته باشی با docker exec -it shop2 sh وارد کانتینر شوی و با exit بیرون بیایی، و docker stats --no-stream حداقل یک ردیف با درصد پردازنده و مصرف حافظه نشان بدهد.
🎩 جعبهابزار ترفندها
همهٔ ترفندهای این آموزش، در یک نگاه. این بخش را چاپ کن یا نگه دار.
دو بخشی بودن خروجی docker version: خطا در بخش سرور یعنی موتور خاموش است، نه اینکه داکر نصب نیست.
آدرس میرور را جلوی نام ایمیج بگذار؛ برای ایمیجهای رسمی library/ را فراموش نکن.
در -p 8080:3000 سمت چپ همیشه کامپیوتر توست.
docker run --rm -it alpine sh برای امتحان کردن، بدون بهجا ماندن کانتینر مرده.
latest تضمین آخرین نسخه نیست؛ روی سرور همیشه تگ دقیق بنویس.
اول package.json و نصب، بعد COPY . . — بیلدها چند برابر سریعتر میشوند.
برنامهای که داخل کانتینر روی 127.0.0.1 گوش میدهد، از بیرون هرگز جواب نمیدهد.
یک کانتینر موقت alpine با tar، کل ولوم را در یک فایل فشرده میریزد.
در Compose نام سرویس همان نام میزبان است؛ db بنویس نه localhost.
یک خط USER node یک مسیر کامل نفوذ را میبندد.
docker save و docker load برای سرور بدون اینترنت یا شرایط تحریم.
کانتینری که مدام ریاستارت میشود، علتش در آخرین ۳۰ خط لاگ است.
🧠 آزمونک
ببین چقدر یاد گرفتی. اشتباه کردن هم بخشی از یادگیری است؛ بعد از هر جواب، توضیحش را میخوانی.
📇 برگهٔ تقلب دستورها
هر چیزی که در عمل روزانه لازم میشود، اینجاست.
docker run -d --name N IMAGE · docker ps -a · docker stop N · docker start N · docker rm -f N
docker image ls · docker pull NAME:TAG · docker image history NAME · docker image rm NAME:TAG
docker build -t name:1.0.0 . — نقطهٔ آخر یعنی «از همین پوشه»
-p بیرون:داخل · -e KEY=value · --restart unless-stopped
docker volume create NAME · -v NAME:/path · docker volume ls
docker compose up -d · docker compose ps · docker compose logs -f · docker compose down (با -v نزن!)
docker logs --tail 50 -f N · docker exec -it N sh · docker stats --no-stream · docker inspect N
docker tag A B · docker save -o file.tar IMAGE · docker load -i file.tar · docker push
docker system df · docker image prune (امن) · docker system prune -a --volumes (خطرناک)
🔧 رفع اشکال
هفت خرابی رایج که تقریباً هرکسی در ماه اول با آنها روبهرو میشود.
| چه میبینی؟ | چرا؟ | چه کار کنی؟ |
|---|---|---|
| پیغامی شبیه cannot connect to the Docker daemon یا The system cannot find the file specified | داکر نصب است ولی موتورش بالا نیامده یا هنوز در حال بالا آمدن است. | برنامهٔ Docker Desktop را باز کن و صبر کن نشانگر موتور سبز شود، بعد docker version را دوباره بزن. |
| هنگام دانلود ایمیج: TLS handshake timeout یا unauthorized یا connection reset | دسترسی به انبار مرکزی ایمیجها از ایران محدود است. | مرحلهٔ ۲ را انجام بده؛ اگر میرورت هم جواب نداد، آدرس دیگری از یک ارائهدهندهٔ داخلی بگیر — این آدرسها تغییر میکنند. |
| port is already allocated یا address already in use | پورتی که سمت چپ -p نوشتی، الان دست برنامهٔ دیگری است (شاید کانتینر قبلی خودت). |
یا پورت بیرونی را عوض کن (-p 8081:3000) یا با docker ps کانتینر اشغالکننده را پیدا و حذف کن. |
| کانتینر Up است ولی مرورگر میگوید سایت در دسترس نیست | یا نگاشت پورت برعکس نوشته شده، یا برنامه داخل کانتینر روی 127.0.0.1 گوش میدهد. |
ترتیب outside:inside را چک کن و مطمئن شو برنامه روی 0.0.0.0 گوش میدهد. با docker logs ببین اصلاً شروع شده یا نه. |
| کانتینر بلافاصله بعد از اجرا Exited میشود | فرمان اصلی کانتینر یا کارش تمام شده یا با خطا خوابیده. | docker logs --tail 30 NAME را بزن؛ علت تقریباً همیشه در همان چند خط آخر نوشته شده است. |
| name is already in use by container | قبلاً کانتینری با همین نام ساختهای، حتی اگر الان خاموش باشد. | یا نام دیگری بده، یا قبلی را با docker rm -f NAME پاک کن. |
| no space left on device یا بیلد ناگهان شکست میخورد | ایمیجها و لایههای قدیمی دیسک را پر کردهاند. | اول docker system df برای دیدن وضعیت، بعد docker image prune. سراغ -a --volumes نرو مگر اینکه دقیقاً بدانی چه پاک میشود. |
🎉 تمام شد!
حالا تو میتوانی یک برنامه را داخل کانتینر ببری، از آن ایمیجی حرفهای و امن بسازی، چند سرویس را با یک فایل کنار هم بالا بیاوری، دادهها را حفظ کنی، و وقتی چیزی خراب شد خودت پیدایش کنی. این دقیقاً همان کاری است که یک مهندس در روز کاریاش با داکر انجام میدهد.
داکر معمولاً تنها نیست. پاییندستش، سیستمهای خودکارسازی (مثل GitHub Actions یا GitLab CI) بعد از هر تغییر کد، همین docker build را میزنند و ایمیج را به رجیستری میفرستند. بالادستش، وقتی تعداد سرورها زیاد شد، ابزارهای ارکستراسیون مثل Kubernetes تصمیم میگیرند کدام کانتینر روی کدام سرور و با چند نسخه اجرا شود.
نکتهٔ مهم: ورودی همهٔ آن دنیا، همین ایمیجی است که تو الان بلدی بسازی. کسی که Dockerfile تمیز و ایمیج سبک و امن بلد نیست، در آن لایهها هم موفق نمیشود.
- پروژهای را از یک همکار یا از GitHub تحویل میگیری و بهجای دو روز نصب وابستگیها، با یک
docker compose up -dدر پنج دقیقه بالا میآوری. - روی لپتاپت کار میکند ولی روی سرور نه؛ با یک ایمیج مشترک، این جملهٔ معروف کلاً حذف میشود.
- میخواهی یک پایگاه داده یا ابزار (مثل PostgreSQL، Redis، n8n) را برای یک آزمایش بالا بیاوری بدون اینکه سیستمعاملت شلوغ شود — و بعد بدون هیچ ردی پاکش کنی.
- پلهٔ اول (تکرار): همین پروژه را از صفر در پوشهٔ جدیدی بساز، ولی این بار بدون نگاه کردن به آموزش، فقط با برگهٔ تقلب.
- پلهٔ دوم (یک چیز را عوض کن): در
compose.yamlیک سرویس سوم اضافه کن، مثلاًredis:7-alpine، و در برنامهات پورت ۶۳۷۹ آن را چک کن. ببین چه چیزی میشکند و چرا. - پلهٔ سوم (کار واقعی خودت): یکی از پروژههای واقعیات را کانتینری کن؛ همان الگو: پایهٔ سبک، ترتیب درست کپی، کاربر غیر ریشه، بررسی سلامت، و داده روی ولوم.