🎯 آموزش گام‌به‌گام از صفر تا حرفه‌ای

داکر (Docker) — از هیچ تا استقرار یک برنامهٔ واقعی

تا آخر این صفحه، خودت یک برنامه را داخل کانتینر می‌بری، از آن یک ایمیج سبک و امن می‌سازی، برنامه و پایگاه داده را با یک دستور بالا می‌آوری، و وقتی چیزی خراب شد خودت پیدایش می‌کنی.

⏱️ حدود ۲ تا ۳ ساعت 🎚️ سطح: از صفرِ صفر 🧩 ۱۰ مرحله 🎁 خروجی: یک استک واقعی «برنامه + پایگاه داده» با ایمیج حرفه‌ای خودت

👋 قبل از شروع

این آموزش برای کسی نوشته شده که تا امروز اصلاً داکر کار نکرده است. قرار نیست فقط بفهمی داکر چیست؛ قرار است تا آخر این صفحه یک برنامهٔ واقعی بسازی که داخل کانتینر اجرا می‌شود، یک ایمیج حرفه‌ای و امن از آن بگیری، و یک سیستم دو سرویسی (برنامه + پایگاه داده) را با یک دستور بالا بیاوری.

هر مرحله یک نتیجهٔ دیدنی دارد. اگر چیزی را ندیدی، یعنی جایی اشتباه شده و بخش رفع اشکال همان را پوشش می‌دهد.

🧰 چه چیزهایی لازم داری؟
  • یک کامپیوتر ویندوز ۱۰ یا ۱۱ نسخهٔ ۶۴ بیتی — برای دیدنش کلید Windows + I را بزن و در بخش سیستم، قسمت «درباره» را ببین.
  • حداقل ۸ گیگابایت رم (با ۴ گیگابایت هم راه می‌افتد ولی تنگ است) و حدود ۱۰ گیگابایت فضای خالی روی درایو C.
  • قابلیت مجازی‌سازی روشن باشد — در مرحلهٔ ۱ دقیقاً یادت می‌دهم چطور در ۱۰ ثانیه چکش کنی.
  • یک حساب کاربری که اجازهٔ نصب نرم‌افزار داشته باشد.
  • اینترنت — به‌خصوص در مرحله‌های ۱ و ۲ که چند صد مگابایت دانلود می‌شود.
📌 فرض‌های ما
  • تو ویندوز داری. اگر لینوکس یا مک داری، فقط بخش «نصب» فرق می‌کند؛ همهٔ دستورهای داکر در این آموزش عیناً یکسان‌اند.
  • هیچ دانش قبلی از داکر، لینوکس، شبکه یا برنامه‌نویسی لازم نیست. کدهای لازم را کامل به تو می‌دهم.
  • تو در ایران هستی؛ برای همین مرحلهٔ ۲ کاملاً به رفع محدودیت دسترسی اختصاص دارد.
⚠️ صادقانه دربارهٔ هزینه و محدودیت

نرم‌افزار Docker Desktop برای استفادهٔ شخصی، آموزشی و شرکت‌های کوچک رایگان است و برای سازمان‌های بزرگ اشتراک پولی می‌خواهد (شرایط لایسنس ممکن است تغییر کند؛ ملاک، صفحهٔ رسمی خودشان است). خودِ موتور داکر متن‌باز و رایگان است.

نکتهٔ مهم‌تر: انبار رسمی ایمیج‌ها یعنی Docker Hub از ایران معمولاً در دسترس نیست. این یک خرابی از طرف تو نیست و راه‌حل استانداردی دارد که در مرحلهٔ ۲ انجامش می‌دهیم.

۱

داکر را نصب کن و مطمئن شو زنده است

⏱️ حدود ۲۰ دقیقه🎯 نتیجه: دستور docker روی کامپیوترت جواب می‌دهد
🧠 اول تصویر ذهنی

قبل از اختراع کانتینرهای باربری، هر کشتی را دستی و جعبه‌جعبه بار می‌زدند؛ هر بندر هم روش خودش را داشت. کانتینر یک جعبهٔ استاندارد ساخت: هر چیزی داخلش باشد، همهٔ بندرها بلدند با آن کار کنند.

داکر همین کار را با نرم‌افزار می‌کند: برنامه‌ات را با هر چیزی که برای اجرا لازم دارد داخل یک جعبهٔ استاندارد می‌گذارد، و بعد آن جعبه روی لپ‌تاپ تو، لپ‌تاپ همکارت و سرور شرکت دقیقاً یک‌جور اجرا می‌شود.

با این تفاوت که این جعبه برخلاف کانتینر واقعی، ساختنِ کپی‌اش چند ثانیه طول می‌کشد، جای اضافه نمی‌گیرد، و می‌توانی هزار تای آن را هم‌زمان داشته باشی.

  1. اول چک کن مجازی‌سازی روشن است: Ctrl + Shift + Esc را بزن تا مدیر وظیفه باز شود، به تب عملکرد (Performance) برو و روی CPU کلیک کن.

    در پنل سمت راست باید خطی به اسم Virtualization ببینی که جلویش نوشته Enabled. مجازی‌سازی یعنی توانایی پردازنده برای اجرای یک کامپیوتر کوچک داخل کامپیوتر تو — داکر روی ویندوز دقیقاً به همین نیاز دارد.

  2. اگر جلوی آن خط نوشته بود Disabled، کامپیوتر را خاموش‌روشن کن و وارد تنظیمات اولیهٔ مادربرد (BIOS یا UEFI) شو و گزینهٔ مجازی‌سازی را روشن کن.

    کلید ورود به این تنظیمات بسته به سازندهٔ دستگاه فرق می‌کند (روی خیلی از لپ‌تاپ‌ها F2 و روی خیلی از دسکتاپ‌ها Del است و موقع روشن شدن، پایین صفحه می‌نویسد). اسم گزینه معمولاً چیزی شبیه Intel Virtualization Technology یا AMD SVM Mode است. اگر نوشته بود Enabled، این گام را رد کن و تیکش را بزن.

  3. روی دکمهٔ ویندوز راست‌کلیک کن، Terminal (Admin) یا Windows PowerShell (Admin) را باز کن و این دستور را بزن:

    wsl --install

    این دستور زیرسیستم لینوکس ویندوز (WSL) را نصب می‌کند: یک لینوکس کوچک و سبک داخل ویندوز که داکر کانتینرهایش را آن‌جا اجرا می‌کند. بعد از تمام شدن، کامپیوتر را یک بار ری‌استارت کن. اگر پیغام داد که از قبل نصب است، عالی است — برو گام بعد.

  4. به سایت رسمی داکر برو و نسخهٔ ویندوزِ Docker Desktop را دانلود کن.

    آدرس سایت docker.com است و صفحهٔ دانلود، خودش نسخهٔ مناسب ویندوز را پیشنهاد می‌دهد. حجم فایل حدود یک گیگابایت است. اگر سایت باز نشد، فایل نصب معمولاً روی سایت‌های نرم‌افزار ایرانی هم آینه (کپی) می‌شود.

  5. فایل دانلودشده را اجرا کن و در پنجرهٔ نصب، گزینهٔ مربوط به استفاده از WSL 2 را تیک‌دار بگذار و نصب را تمام کن.

    معمولاً همین گزینه از قبل تیک دارد. نصب حدود ۳ تا ۱۰ دقیقه طول می‌کشد و ممکن است آخرش بخواهد از ویندوز خارج و دوباره وارد شوی.

  6. برنامهٔ Docker Desktop را باز کن و صبر کن تا پایین پنجره وضعیت موتور سبز شود.

    بار اول ممکن است ۱ تا ۳ دقیقه طول بکشد و شرایط استفاده را نشان بدهد که باید بپذیری. تا وقتی نشانگر موتور سبز نشده، هیچ دستور داکری کار نمی‌کند؛ این عادی است.

  7. یک پنجرهٔ 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) یعنی آینه.

  1. اول عمداً بگذار خراب شود تا خطا برایت آشنا شود. در PowerShell بزن:

    docker run hello-world

    اگر پیغام‌هایی شبیه TLS handshake timeout یا unauthorized یا connection reset دیدی، هیچ اتفاق بدی نیفتاده — این دقیقاً همان محدودیتی است که الان درستش می‌کنیم. (اگر برایت کار کرد، یعنی اینترنتت از مسیر دیگری می‌رود؛ باز هم این مرحله را انجام بده تا سرعتت بیشتر شود.)

  2. آدرس یک میرور فعال ایرانی را پیدا کن.

    چند شرکت میزبانی و ابری ایرانی این سرویس را رایگان می‌دهند و آدرسش را در صفحهٔ مستندات خودشان منتشر می‌کنند (برای نمونه آروان‌کلود سال‌هاست چنین آدرسی دارد). این آدرس‌ها هر چند وقت عوض می‌شوند، پس آدرس را از سایت خود آن سرویس بردار، نه از یک آموزش قدیمی. آدرس چیزی شبیه https://docker.example.ir خواهد بود.

  3. در Docker Desktop روی چرخ‌دندهٔ تنظیمات کلیک کن و بخش Docker Engine را باز کن.

    آن‌جا یک ویرایشگر متنی می‌بینی که داخلش چند خط با آکولاد نوشته شده. این همان فایل پیکربندی موتور داکر است که اسمش daemon.json است.

  4. کلید registry-mirrors را با آدرس میرورت به همان متن اضافه کن و روی دکمهٔ اعمال و راه‌اندازی دوباره (Apply & restart) بزن.

    {
      "registry-mirrors": ["https://docker.example.ir"]
    }

    به‌جای docker.example.ir آدرس واقعی خودت را بگذار. اگر از قبل کلیدهای دیگری آن‌جا بود، پاکشان نکن؛ فقط بین آکولادها یک کاما بگذار و خط جدید را اضافه کن. مواظب باش ساختار JSON سالم بماند وگرنه موتور بالا نمی‌آید.

  5. صبر کن موتور دوباره سبز شود و همان دستور اول را تکرار کن:

    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) مثل غذای پخته‌شده از روی آن دستور است: زنده، در حال اجرا، و قابل دور ریختن.

با این تفاوت که از یک دستور پخت می‌توانی هم‌زمان ده غذای کاملاً یکسان بپزی، هرکدام را جدا خاموش کنی و دور بریزی، و دستور پخت هیچ آسیبی نبیند.

  1. یک وب‌سرور واقعی را با یک خط بالا بیاور:

    docker run -d -p 8080:80 --name web nginx:alpine

    یعنی: ایمیج nginx:alpine را بگیر، در پس‌زمینه اجرا کن (-d)، اسمش را بگذار web، و پورت ۸۰۸۰ کامپیوتر من را به پورت ۸۰ داخل کانتینر وصل کن.

  2. مرورگر را باز کن و برو به http://localhost:8080

    باید صفحهٔ خوش‌آمدگویی nginx را ببینی. همین الان یک وب‌سرور واقعی روی کامپیوترت اجرا شد، بدون اینکه چیزی نصب کنی.

  3. ببین چه چیزی در حال اجراست:

    docker ps

    ستون STATUS باید چیزی شبیه Up 2 minutes نشان بدهد و ستون PORTS نگاشت پورت‌ها را. دستور docker ps -a کانتینرهای خاموش را هم نشان می‌دهد.

  4. صفحهٔ مرورگر را چند بار تازه کن، بعد بنویس:

    docker logs web

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

  5. خاموش و روشنش کن و بین این دو، مرورگر را تازه کن:

    docker stop web
    docker start web

    وقتی خاموش است مرورگر خطا می‌دهد، وقتی روشن می‌شود دوباره جواب می‌دهد. stop کانتینر را نمی‌کُشد، فقط متوقفش می‌کند.

  6. حالا کاملاً پاکش کن و ببین ایمیج سر جایش هست:

    docker rm -f web
    docker image ls

    کانتینر رفت، ولی nginx هنوز در فهرست ایمیج‌هاست. یعنی دفعهٔ بعد بدون دانلود، در یک ثانیه دوباره بالا می‌آید.

پورت را برعکس ننویس — سمت چپ همیشه «بیرون» است

در -p 8080:80 عدد سمت چپ پورتی است روی کامپیوتر تو که در مرورگر تایپ می‌کنی، و عدد سمت راست پورتی است که برنامه داخل کانتینر روی آن گوش می‌دهد. اگر جابه‌جا بنویسی، یا خطای «پورت مشغول است» می‌گیری یا صفحه هرگز باز نمی‌شود. یادآور ساده: outside:inside.

⚡ حذف رایج‌ترین علت «کانتینر بالاست ولی صفحه باز نمی‌شود»
برای تست‌های یک‌بارمصرف، --rm بگذار

اگر فقط می‌خواهی چیزی را امتحان کنی، این‌طور اجرا کن تا به‌محض بسته شدن، کانتینر خودش پاک شود:

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).

با این تفاوت که این برگه‌ها فقط‌خواندنی‌اند و بین ایمیج‌های مختلف مشترک می‌شوند؛ و وقتی کانتینر می‌سازی، یک برگهٔ سفیدِ قابل‌نوشتن روی همه‌شان گذاشته می‌شود که با پاک شدن کانتینر دور ریخته می‌شود.

  1. فهرست ایمیج‌هایت را با اندازه‌شان ببین:

    docker image ls

    هر ردیف سه چیز مهم دارد: نام (REPOSITORY)، تگ (TAG) و شناسه (IMAGE ID).

  2. یک نسخهٔ مشخص را دانلود کن تا فرق تگ‌ها را ببینی:

    docker pull nginx:1.27-alpine

    تگ یعنی همان چیزی که بعد از دو نقطه می‌آید و نسخه را مشخص می‌کند. اگر تگ ننویسی، داکر خودش latest را فرض می‌گیرد.

  3. دوباره فهرست را ببین و اندازه‌ها را با فضای واقعی مقایسه کن:

    docker image ls
    docker system df

    جمع ستون اندازه‌ها بیشتر از فضای واقعی اشغال‌شده است، چون لایه‌های مشترک فقط یک بار روی دیسک ذخیره می‌شوند.

  4. تاریخچهٔ ساخته شدن یک ایمیج را ببین:

    docker image history nginx:alpine

    هر ردیف یک لایه است و نشان می‌دهد آن لایه با چه دستوری ساخته شده و چقدر حجم اضافه کرده. این‌طوری می‌فهمی حجم یک ایمیج دقیقاً از کجا آمده.

  5. نسخهٔ اضافی را پاک کن:

    docker image rm nginx:1.27-alpine

    اگر پیغام داد که ایمیج در حال استفاده است، یعنی کانتینری از رویش ساخته شده؛ اول آن کانتینر را حذف کن.

به latest اعتماد نکن — نسخه را پین کن

خیلی‌ها فکر می‌کنند latest یعنی «همیشه آخرین نسخه»، ولی latest فقط یک برچسب پیش‌فرض است که ممکن است ماه‌ها به‌روز نشده باشد یا فردا صبح ناگهان به یک نسخهٔ کلان جدید اشاره کند. روی سرور همیشه نسخهٔ مشخص بنویس: nginx:1.27-alpine نه nginx:latest.

⚡ جلوگیری از خرابی ناگهانی سرور بعد از یک بازسازی معمولی
قاعدهٔ سرانگشتی حجم: اول دنبال alpine یا slim بگرد

اگر ایمیج نهایی‌ات از چند صد مگابایت گذشت، معمولاً ایراد از پایهٔ آن است نه از کد تو. بیشتر ایمیج‌های معروف یک نسخهٔ کوچک بر پایهٔ Alpine Linux یا نسخهٔ slim دارند که همان کار را با حجم بسیار کمتر انجام می‌دهد.

⚡ ۵ تا ۱۰ برابر دانلود و فضای کمتر روی هر سرور
🔬 برای کنجکاوها: چرا دو ایمیج ۲۰۰ مگابایتی، ۴۰۰ مگابایت جا نمی‌گیرند؟

هر لایه با اثر انگشت دیجیتال محتوایش شناخته می‌شود، نه با نامش. اگر دو ایمیج از یک پایهٔ مشترک ساخته شده باشند، لایه‌های پایه‌شان دقیقاً یکسان است و داکر آن را فقط یک بار روی دیسک نگه می‌دارد و به هر دو ایمیج «قرض» می‌دهد.

همین موضوع دو نتیجهٔ عملی مهم دارد. اول، اگر همهٔ سرویس‌هایت را روی یک پایهٔ مشترک بسازی، مجموع فضای اشغالی به‌شدت کم می‌شود. دوم، وقتی نسخهٔ جدید برنامه‌ات را روی سرور می‌فرستی، فقط لایه‌های تغییرکرده منتقل می‌شوند؛ برای همین آپدیت دوم همیشه خیلی سریع‌تر از آپدیت اول است.

✅ ایست بازرسی

در خروجی docker image history nginx:alpine باید چند ردیف با ستون SIZE ببینی، و بعد از حذف، docker image ls دیگر تگ 1.27-alpine را نشان ندهد.

۵

اولین ایمیج خودت را بساز (Dockerfile)

⏱️ حدود ۲۵ دقیقه🎯 نتیجه: برنامهٔ خودت داخل کانتینر اجرا می‌شود
🧠 اول تصویر ذهنی

تا اینجا از جعبه‌های آمادهٔ دیگران استفاده کردی. حالا خودت دستور پخت می‌نویسی. فایلی به اسم Dockerfile دقیقاً همان دستور پخت است: از چه پایه‌ای شروع کن، چه چیزی داخلش بریز، و آخر سر چه فرمانی را اجرا کن.

با این تفاوت که این دستور پخت را کامپیوتر اجرا می‌کند، پس هیچ مرحله‌ای را نمی‌شود «حدس زد» — هر چیزی که برنامه‌ات لازم دارد باید صریح نوشته شود.

  1. یک پوشهٔ کاری بساز و واردش شو. در PowerShell بزن:

    mkdir C:\docker-shop
    cd C:\docker-shop

    مسیر را بدون فاصله و بدون حروف فارسی انتخاب کن؛ بعضی دستورها با مسیرهای فاصله‌دار دردسر درست می‌کنند.

  2. داخل همین پوشه فایلی به نام 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);
    });
  3. در همان پوشه فایل package.json را بساز:

    {
      "name": "docker-shop-web",
      "version": "1.0.0",
      "private": true,
      "main": "server.js",
      "scripts": {
        "start": "node server.js"
      }
    }

    این فایل شناسنامهٔ پروژهٔ Node.js است و می‌گوید برنامه با چه دستوری اجرا می‌شود.

  4. حالا دستور پخت را بنویس: فایلی به نام 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 باشد؛ اول فقط شناسنامه را کپی کن و کتابخانه‌ها را نصب کن؛ بعد بقیهٔ کد را کپی کن؛ اعلام کن روی پورت ۳۰۰۰ گوش می‌دهی؛ و در آخر برنامه را اجرا کن.

  5. ایمیج را بساز (به نقطهٔ آخر دستور دست نزن):

    docker build -t shop-web:1.0.0 .

    نقطهٔ آخر یعنی «مصالح ساخت را از همین پوشه بردار». -t هم نام و تگ ایمیج را تعیین می‌کند. بار اول چند دقیقه طول می‌کشد چون پایهٔ Node.js دانلود می‌شود.

  6. ایمیج خودت را اجرا کن:

    docker run -d -p 8080:3000 --name shop -e APP_NAME="Rayon Shop" shop-web:1.0.0

    حالا برو به http://localhost:8080. باید نام برنامه، نام کانتینر، و عبارت database: unreachable را ببینی — پایگاه داده هنوز ساخته نشده و این کاملاً درست است. سوئیچ -e یعنی «یک متغیر محیطی به کانتینر بده».

ترتیب خط‌های Dockerfile = سرعت بیلد

دلیل اینکه اول فقط package.json را کپی کردیم و بعد بقیهٔ کد را، این است: داکر نتیجهٔ هر خط را کش می‌کند و تا اولین خطی که ورودی‌اش عوض شده جلو می‌رود. اگر COPY . . را قبل از نصب کتابخانه‌ها بگذاری، با هر تغییر کوچک در کد، همهٔ کتابخانه‌ها از نو نصب می‌شوند.

⚡ بیلد چنددقیقه‌ای تبدیل می‌شود به چندثانیه‌ای، در هر تغییر کد
داخل کانتینر روی 0.0.0.0 گوش بده، نه 127.0.0.1

در کد ما عمداً نوشتیم 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) مثل یک دفترچه است که بیرون تخته می‌ماند و هر کانتینر جدیدی می‌تواند همان دفترچه را باز کند.

با این تفاوت که این دفترچه را داکر خودش مدیریت می‌کند و می‌شود هم‌زمان به چند کانتینر دادش.

  1. اول عمداً یک داده را از دست بده. یک کانتینر ساده بساز و داخلش فایل بنویس:

    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 یعنی «این فرمان را داخل کانتینرِ در حال اجرا انجام بده».

  2. حالا پاکش کن، دوباره بسازش و همان فایل را بخواه:

    docker rm -f loss
    docker run -d --name loss alpine sleep 3600
    docker exec loss cat /note.txt

    این بار خطای No such file or directory می‌گیری. فایل نابود شد. این مهم‌ترین چیزی است که باید دربارهٔ کانتینرها بدانی.

  3. یک ولوم نام‌دار بساز:

    docker volume create shop-data
  4. کانتینری بساز که آن ولوم را روی پوشهٔ /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 سمت چپ نام ولوم است و سمت راست مسیری داخل کانتینر که ولوم آن‌جا سوار می‌شود.

  5. حالا همان آزمایش قبلی را تکرار کن: کانتینر را نابود کن، دوباره بساز و بخوان:

    docker rm -f keeper
    docker run -d --name keeper -v shop-data:/data alpine sleep 3600
    docker exec keeper cat /data/note.txt

    این بار داده سر جایش است. کانتینر رفت، دفترچه ماند.

  6. فهرست ولوم‌هایت را ببین و بعد کانتینر آزمایشی را پاک کن:

    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 فیلم‌نامه است: در یک فایل می‌نویسی چه سرویس‌هایی لازم داری و هرکدام چه تنظیماتی دارند، و بعد با یک دستور همه با هم و به ترتیب درست بالا می‌آیند. با این تفاوت که این فیلم‌نامه را می‌شود به همکارت داد و او دقیقاً همان صحنه را بالا بیاورد.

  1. در همان پوشهٔ 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. اگر خطای عجیب گرفتی، اول همین را چک کن.

  2. کانتینر قدیمی را کنار بگذار تا پورت آزاد شود:

    docker rm -f shop
  3. کل سیستم را با یک دستور بالا بیاور:

    docker compose up -d

    دقت کن docker compose با فاصله نوشته می‌شود؛ شکل قدیمی docker-compose با خط تیره مربوط به نسخه‌های قدیمی است. بار اول، دانلود پایگاه دادهٔ PostgreSQL چند دقیقه طول می‌کشد.

  4. وضعیت سرویس‌ها را ببین:

    docker compose ps

    باید دو ردیف ببینی و جلوی db عبارتی شبیه healthy بیاید. اگر هنوز starting است، چند ثانیه صبر کن و دوباره بزن.

  5. برو به http://localhost:8080

    حالا باید بنویسد database: connected. یعنی کانتینر برنامه توانست کانتینر پایگاه داده را با نام db پیدا کند و به آن وصل شود.

  6. همه را با هم بخوابان و ببین ولوم باقی می‌ماند:

    docker compose down
    docker volume ls

    کانتینرها و شبکه پاک شدند، ولی ولومی که اسمش با پیشوند پروژه ساخته شده هنوز هست. برای ادامهٔ کار دوباره docker compose up -d بزن.

وقتی چیزی بالا نیامد، اول این دو دستور — نه گوگل

این دو خط در پنج ثانیه به تو می‌گویند کدام سرویس مشکل دارد و چرا:

docker compose ps
docker compose logs -f --tail 50 web

اولی وضعیت هر سرویس را می‌دهد و دومی آخرین ۵۰ خط لاگ همان سرویس را زنده نشان می‌دهد (با Ctrl + C بیرون بیا). اگر نام سرویس را ننویسی، لاگ همه را با رنگ‌های جدا می‌بینی.

⚡ تشخیص علت خرابی در ۵ ثانیه، به‌جای نیم‌ساعت حدس زدن
آدرس سرویس بغلی، نام سرویس است — نه localhost

در فایل بالا نوشتیم 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) دقیقاً همین است: در مرحلهٔ اول با همهٔ ابزارها می‌سازی، و در مرحلهٔ دوم فقط محصول نهایی را به یک ایمیج تمیز منتقل می‌کنی. با این تفاوت که این‌جا آشپزخانه بعد از کار کاملاً ناپدید می‌شود و هیچ ردی در ایمیج نهایی نمی‌گذارد.

  1. در پوشهٔ پروژه فایلی به نام .dockerignore بساز (با یک نقطه شروع می‌شود):

    node_modules
    npm-debug.log
    .git
    .env
    Dockerfile
    compose.yaml
    *.md

    ویندوز اجازه نمی‌دهد اسم فایل را در File Explorer با نقطه شروع کنی؛ یا از یک ویرایشگر کد استفاده کن یا در PowerShell بزن New-Item .dockerignore و بعد ویرایشش کن.

  2. محتوای 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"]
  3. نسخهٔ جدید را بساز:

    docker build -t shop-web:1.1.0 .
  4. دو نسخه را کنار هم ببین:

    docker image ls shop-web

    در این برنامهٔ کوچک اختلاف اندازه کم است، چون ما ابزار ساخت سنگینی نداشتیم. صادقانه بگویم: ارزش واقعی این الگو در پروژه‌های واقعی است — مثلاً یک پروژهٔ React یا TypeScript که ایمیجش با این روش از حدود یک گیگابایت به حدود صد مگابایت می‌رسد.

  5. اجرا کن و ببین با چه کاربری اجرا می‌شود:

    docker run -d -p 8081:3000 --name shop2 shop-web:1.1.0
    docker exec shop2 whoami

    باید بنویسد node و نه root. پورت ۸۰۸۱ را انتخاب کردیم تا با سرویس Compose که روی ۸۰۸۰ است تداخل نکند.

  6. بعد از حدود نیم دقیقه، وضعیت سلامتش را بپرس:

    docker ps
    docker inspect --format "{{.State.Health.Status}}" shop2

    در خروجی docker ps کنار وضعیت، عبارتی شبیه (healthy) می‌بینی. دستور دوم فقط همان یک کلمه را چاپ می‌کند.

کانتینر را با کاربر ریشه اجرا نکن

کانتینرها به‌طور پیش‌فرض با کاربر root اجرا می‌شوند و بیشتر آموزش‌ها هم همین‌طور رهایش می‌کنند. یک خط USER node کافی است تا اگر روزی مهاجم از دل برنامه‌ات نفوذ کرد، داخل کانتینر هم دستش بسته باشد. در ایمیج‌های رسمی Node.js کاربری به نام node از قبل ساخته شده است.

⚡ بستن یک مسیر کامل نفوذ، با یک خط و بدون هیچ هزینه‌ای
پایش سلامت رایگان با HEALTHCHECK

خیلی‌ها برای فهمیدن اینکه سرویس واقعاً سالم است ابزار جداگانه نصب می‌کنند، در حالی که خود داکر این را دارد. با یک خط HEALTHCHECK، داکر مرتب یک آدرس را صدا می‌زند و نتیجه را در docker ps نشان می‌دهد. مهم‌تر اینکه Compose می‌تواند شروع سرویس‌های دیگر را به همین سلامت گره بزند — دقیقاً کاری که در مرحلهٔ ۷ کردیم.

⚡ پایش سلامت واقعی بدون نصب حتی یک ابزار اضافه
🔬 برای کنجکاوها: چرا ایمیج کوچک فقط «سبک» نیست، بلکه امن‌تر هم هست؟

هر ابزاری که در ایمیج نهایی باقی بماند — کامپایلر، ابزار بسته‌بندی، کتابخانه‌های توسعه — دو هزینه دارد: حجم، و سطح حمله.

چون A (هر بسته‌ای که داخل ایمیج است ممکن است روزی آسیب‌پذیری اعلام‌شده بگیرد) → پس B (هرچه بسته‌های بیشتری داشته باشی، احتمال اینکه ایمیجت آسیب‌پذیر شمرده شود بیشتر است) → به همین دلیل C (حذف کامل ابزارهای ساخت از ایمیج نهایی، هم‌زمان هم حجم و هم ریسک امنیتی را کم می‌کند).

یک اشتباه رایج هم این‌جاست: بعضی‌ها فایل رمز و کلید را در مرحلهٔ ساخت کپی می‌کنند و بعد در خط بعد پاکش می‌کنند و خیال می‌کنند رفته است. ولی لایه‌ها تاریخچه دارند؛ آن فایل هنوز در لایهٔ قبلی هست و قابل استخراج است. رمزها باید موقع اجرا به‌صورت متغیر محیطی داده شوند، نه داخل ایمیج پخته شوند.

✅ ایست بازرسی

دستور docker exec shop2 whoami باید node چاپ کند و docker inspect --format "{{.State.Health.Status}}" shop2 بعد از حدود نیم دقیقه باید healthy بدهد.

۹

نسخه‌گذاری و بردن ایمیج روی سرور

⏱️ حدود ۲۰ دقیقه🎯 نتیجه: ایمیجت قابل انتقال و قابل بازگشت است
🧠 اول تصویر ذهنی

تگ مثل برچسبی است که روی جعبه می‌چسبانی. جعبه همان جعبه است؛ برچسب فقط اسمی است که تو با آن صدایش می‌زنی. برای همین یک جعبه می‌تواند هم‌زمان چند برچسب داشته باشد.

با این تفاوت که هر جعبه یک شمارهٔ سریال تغییرناپذیر هم دارد (digest) که هیچ‌وقت به جعبهٔ دیگری نمی‌چسبد — و روی سرورهای حساس، مرجع نهایی همان است.

  1. یک برچسب دوم به همان ایمیج بزن و ببین شناسه‌شان یکی است:

    docker tag shop-web:1.1.0 shop-web:latest
    docker image ls shop-web

    هر دو ردیف IMAGE ID یکسان دارند، چون یک ایمیج‌اند با دو نام.

  2. ایمیج را به یک فایل قابل حمل تبدیل کن:

    docker save -o shop-web-1.1.0.tar shop-web:1.1.0

    حالا در پوشه‌ات یک فایل داری که کل ایمیج داخلش است. این فایل را می‌توانی با هر روشی به سرور برسانی.

  3. همان فایل را دوباره بارگذاری کن تا مطمئن شوی سالم است:

    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 بزن.)

  4. اگر به یک رجیستری دسترسی داری، مسیر استاندارد این است:

    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 کاملاً کافی است.

  5. اجرای «مثل سرور» را تمرین کن: با سیاست راه‌اندازی خودکار.

    docker rm -f shop2
    docker run -d -p 8081:3000 --restart unless-stopped --name shop2 shop-web:1.1.0

    اگر خط اول گفت چنین کانتینری وجود ندارد، اشکالی ندارد — یعنی در گام قبل پاکش کرده‌ای؛ فقط خط دوم را اجرا کن.

    با --restart unless-stopped اگر کانتینر خراب شد یا سرور ری‌استارت شد، داکر خودش دوباره بالایش می‌آورد — مگر اینکه خودت عمداً خاموشش کرده باشی.

وقتی رجیستری در دسترس نیست، save و load راه نجات است

خیلی‌ها فکر می‌کنند بدون رجیستری نمی‌شود ایمیج را جابه‌جا کرد. در حالی که docker save یک فایل معمولی می‌سازد که با هر روشی — حتی حافظهٔ فلش — قابل انتقال است و روی مقصد با docker load برمی‌گردد. برای شرایط تحریم، سرور بدون اینترنت، یا شبکهٔ ایزولهٔ سازمانی، این استانداردترین راه است.

⚡ استقرار روی سرورِ بدون اینترنت، بدون نیاز به هیچ رجیستری
قانون نام‌گذاری: نسخهٔ معنایی روی سرور، هرگز فقط latest

هر ایمیجی که به سرور می‌فرستی باید یک تگ یکتا و بازگشت‌پذیر داشته باشد، مثل 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 را نشان بدهد.

۱۰

دیباگ، محدود کردن منابع و نظافت

⏱️ حدود ۲۰ دقیقه🎯 نتیجه: می‌توانی هر مشکلی را خودت پیدا و رفع کنی
🧠 اول تصویر ذهنی

تا اینجا از بیرون به جعبه نگاه می‌کردی. حالا یاد می‌گیری در جعبه را باز کنی و داخلش راه بروی، مقدار غذایی که مصرف می‌کند را محدود کنی، و جعبه‌های قدیمی را از انبار بیرون بریزی.

با این تفاوت که برخلاف انبار واقعی، این‌جا بعضی دستورهای «نظافت» می‌توانند داده‌های واقعی را هم با خودشان ببرند — پس باید بدانی کدام امن است و کدام نه.

  1. وارد کانتینر شو و داخلش بگرد:

    docker exec -it shop2 sh

    حالا داخل کانتینر هستی. بزن ls تا فایل‌های برنامه را ببینی، بعد env تا متغیرهای محیطی را ببینی، و آخرش exit تا بیرون بیایی. خروج تو هیچ تأثیری روی اجرای کانتینر ندارد.

  2. لاگ زنده را با تعداد محدود ببین:

    docker logs --tail 50 -f shop2

    با Ctrl + C بیرون بیا. سوئیچ --tail جلوی سرازیر شدن هزاران خط قدیمی را می‌گیرد.

  3. مصرف منابع همهٔ کانتینرها را ببین:

    docker stats --no-stream

    ستون‌های مصرف پردازنده و حافظه را نشان می‌دهد. بدون --no-stream این نما زنده و مداوم به‌روز می‌شود.

  4. یک کانتینر با سقف منابع اجرا کن:

    docker run -d --name limited --memory 256m --cpus 0.5 --restart unless-stopped nginx:alpine

    حالا این کانتینر هرچقدر هم شلوغ شود، بیش از ۲۵۶ مگابایت حافظه و نصف یک هستهٔ پردازنده نمی‌گیرد. روی سرور مشترک، این تفاوت بین «یک سرویس کند شد» و «کل سرور خوابید» است.

  5. ببین داکر چقدر از دیسکت را گرفته:

    docker system df

    ستون RECLAIMABLE می‌گوید چقدرش قابل آزاد شدن است.

  6. نظافت امن انجام بده و کانتینرهای تمرینی را جمع کن:

    docker rm -f limited
    docker image prune

    دستور image prune فقط ایمیج‌های بی‌نام و بی‌استفاده (به آن‌ها می‌گویند dangling) را پاک می‌کند و امن است. تأیید می‌خواهد؛ y بزن.

در Alpine دستور bash وجود ندارد — sh بزن

اگر 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 تضمین آخرین نسخه نیست؛ روی سرور همیشه تگ دقیق بنویس.

ترتیب Dockerfile

اول package.json و نصب، بعد COPY . . — بیلدها چند برابر سریع‌تر می‌شوند.

گوش دادن روی 0.0.0.0

برنامه‌ای که داخل کانتینر روی 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

Compose

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، و در برنامه‌ات پورت ۶۳۷۹ آن را چک کن. ببین چه چیزی می‌شکند و چرا.
  • پلهٔ سوم (کار واقعی خودت): یکی از پروژه‌های واقعی‌ات را کانتینری کن؛ همان الگو: پایهٔ سبک، ترتیب درست کپی، کاربر غیر ریشه، بررسی سلامت، و داده روی ولوم.