آشنایی با Docker؛ چگونه کانتینرها مشکل اجرای نرمافزار را حل میکنند؟
22 بازدید
زمان مطالعه: 15 دقیقه
فرض کنید یک سرویس Backend روی سیستم شما درست اجرا میشود، اما روی سیستم همتیمیتان نه؛ تفاوت وابستگیها، پکیجها یا تنظیمات کافی است تا چنین مشکلی پیش بیاید.
داکر (Docker) برای کمکردن همین تفاوتهاست. برنامه و نیازمندیهای اجرای آن در قالب Container (کانتینر؛ محیطی ایزوله برای اجرای برنامه) بستهبندی میشوند تا محیطی قابلتکرار از توسعه تا تست و استقرار داشته باشید.
در ادامه دقیقتر یاد میگیریم داکر چیست و کانتینر چطور این محیط را میسازد.
Docker چیست و چه مشکلی را حل میکند؟
Docker پلتفرمی برای ساخت، بستهبندی و اجرای برنامهها در قالب کانتینر است. با Docker، علاوه بر کد، وابستگیها و تنظیمات لازم برای اجرای برنامه هم تعریف میشوند.
مثلاً اگر یک API برای اجرا به نسخه مشخصی از پایتون و چند کتابخانه نیاز داشته باشد، بهجای آمادهکردن این محیط روی هر سیستم، میتوان آن را یکبار با Docker تعریف کرد و محیطی قابلتکرار داشت.
داکر در عمل چند مشکل رایج را کمتر میکند:
- اختلاف نسخه وابستگیها بین محیطها
- راهاندازی دوباره پروژه روی هر سیستم
- تداخل وابستگیهای پروژهها
- تفاوت محیط توسعه، تست و استقرار
Container چیست و چگونه کار میکند؟
اگر Docker محیط اجرای برنامه را آماده کند، کانتینر جایی است که برنامه در آن اجرا میشود. هر کانتینر فضای جداشدهای برای فرایندها، فایلها و وابستگیهای خودش دارد؛ بنابراین چند برنامه با نیازهای متفاوت میتوانند روی یک سیستم اجرا شوند، بدون اینکه تنظیماتشان با یکدیگر تداخل پیدا کند.

مثلاً دو سرویس را تصور کنید که یکی به پایتون ۳.۱۱ و دیگری به نسخه متفاوتی از پایتون نیاز دارد. هر سرویس میتواند محیط مورد نیاز خودش را داخل کانتینر داشته باشد، بدون اینکه برای اجرای یکی مجبور شوید تنظیمات دیگری یا کل سیستم را تغییر دهید.
اما این جداسازی چطور اتفاق میافتد؟ برخلاف ماشین مجازی، هر کانتینر سیستمعامل کاملی برای خودش ندارد. کانتینرهای لینوکس از Kernel (کرنل؛ هسته سیستمعامل) میزبان استفاده میکنند و با کمک قابلیتهایی مثل Namespace و cgroups از یکدیگر جدا میشوند.
– جداسازی فرایندها با Namespace
Namespace (نیماسپیس) مشخص میکند هر کانتینر چه بخشهایی از سیستم را ببیند. به زبان ساده، برای هر کانتینر یک محدوده دید میسازد.
برای مثال، فرایندهای داخل یک کانتینر لازم نیست فرایندهای کانتینر دیگری را ببینند. همین جداسازی میتواند برای بخشهای دیگری مثل شبکه، نام میزبان و Mountها (مانت؛ متصلکردن یک مسیر یا فضای ذخیرهسازی به محیط کانتینر) نیز اعمال شود. در نتیجه چند کانتینر روی یک سیستم مشترک اجرا میشوند، اما هر کدام محیط خودشان را میبینند.
– کنترل منابع با cgroups
جدا بودن محیطها یک طرف ماجراست؛ مصرف منابع هم باید قابلکنترل باشد. cgroups (کنترل گروپها) برای همین کار استفاده میشوند.
با cgroups میتوان میزان استفاده فرایندهای یک کانتینر از منابعی مثل CPU و حافظه را محدود یا مدیریت کرد. مثلاً میتوان جلوی این را گرفت که افزایش مصرف حافظه در یک کانتینر، منابع میزبان را بدون محدودیت درگیر کند.
اگر بخواهیم تفاوت این دو را خیلی کوتاه جمع کنیم: Namespace تعیین میکند کانتینر چه چیزی را ببیند و cgroups مشخص میکند چه مقدار از منابع سیستم در اختیارش باشد.
– ارتباط کانتینر با Kernel سیستم میزبان
کانتینرهای لینوکس، Kernel جداگانهای برای خودشان اجرا نمیکنند؛ آنها Kernel میزبان را به اشتراک میگذارند. یعنی وقتی چند کانتینر روی یک میزبان دارید، هر کدام محیط جداشده خودشان را دارند، اما در لایه پایینتر از یک Kernel مشترک استفاده میکنند.
همین ساختار باعث میشود برای اجرای یک کانتینر لازم نباشد هر بار یک سیستمعامل کامل راهاندازی شود. در نتیجه، کانتینرها معمولاً سریعتر اجرا میشوند و منابع کمتری نسبت به ماشینهای مجازی مصرف میکنند.
پس کانتینر را نباید یک ماشین مجازی با سیستمعامل مستقل تصور کرد. کانتینر در اصل مجموعهای از فرایندهای جداشده است که روی سیستم میزبان اجرا میشوند و محیط و منابع کنترلشده خودشان را دارند.
معماری Docker از چه اجزایی تشکیل شده است؟
وقتی دستور اجرای یک کانتینر را میدهید، Docker پشت صحنه چند بخش را درگیر میکند. یک بخش دستور شما را دریافت میکند، بخش دیگری کار ساخت یا اجرای کانتینر را انجام میدهد و اگر Image روی سیستم موجود نباشد، باید آن را از جایی دریافت کند. معماری داکر را میتوان با همین مسیر دنبال کرد.

Docker Client و Docker Daemon
Docker Client (داکر کلاینت) همان بخشی است که شما با آن به داکر دستور میدهید. مثلاً وقتی دستور docker run را اجرا میکنید، Client درخواست را به Docker Daemon (داکر دیمِن؛ سرویس اصلی مدیریت Docker) میفرستد.
Daemon بخش اجرایی ماجراست. ساخت Image، ایجاد و اجرای کانتینر و مدیریت Network و Volume از جمله کارهایی هستند که زیر نظر آن انجام میشوند.
این دو حتی لازم نیست روی یک سیستم باشند؛ Docker Client میتواند با Daemon روی همان سیستم یا یک میزبان دیگر ارتباط برقرار کند. بنابراین وقتی در ترمینال یک دستور داکر میزنید، خود Client مستقیماً کانتینر را اجرا نمیکند؛ درخواست اجرای آن را به Daemon میدهد.
Docker Registry و Docker Hub
ایمیجهایی که میسازید همیشه قرار نیست فقط روی لپتاپ خودتان بمانند. برای اینکه بتوان آنها را میان اعضای تیم، سرورها یا مراحل مختلف استقرار جابهجا کرد، به محلی برای نگهداری و توزیعشان نیاز دارید. اینجا Docker Registry (داکر رجیستری؛ محل ذخیره و توزیع Imageها) وارد میشود.
Docker Hub (داکر هاب) یکی از شناختهشدهترین Registryهاست. وقتی Imageای را با docker pull دریافت میکنید، Docker بهصورت پیشفرض آن را از Docker Hub میگیرد؛ مگر اینکه Registry دیگری مشخص کرده باشید. در جهت عکس نیز با Push میتوان Image ساختهشده را در Registry قرار داد.
اجزای اصلی معماری Docker در یک نگاه
| جزء | نقش |
| Docker Client | ارسال دستورها به Docker Daemon |
| Docker Daemon | ساخت و مدیریت اجزای Docker |
| Docker Registry | ذخیره و توزیع Imageها |
| Docker Hub | Registry عمومی برای دریافت و انتشار Imageها |
تفاوت Docker Image، Container و Dockerfile

Image، Container و Dockerfile سه مفهوم نزدیک به هم هستند، اما نقش یکسانی ندارند. Dockerfile دستور ساخت را مشخص میکند، Image نتیجه آن ساخت است و Container نسخهای در حال اجرا از Image.
در مثال سرویس پایتونی، ابتدا در Dockerfile نیازمندیهای اجرای برنامه را مشخص میکنیم. Docker از روی این دستورها یک Image میسازد و هنگام اجرا، Container از روی همان Image ایجاد میشود.
Image و ساختار لایهای آن
Docker Image (داکر ایمیج) بستهای شامل فایلهای برنامه، وابستگیها و تنظیمات لازم برای اجرای آن است. Image بعد از ساختهشدن بهصورت Immutable (تغییرناپذیر) در نظر گرفته میشود؛ یعنی بهجای دستکاری مستقیم آن، برای اعمال تغییرات معمولاً Image جدیدی میسازیم.
Imageها ساختاری لایهای دارند. برای مثال، ممکن است یک لایه مربوط به Image پایه پایتون باشد، لایه بعدی وابستگیها را اضافه کند و لایه دیگری کد برنامه را در خود داشته باشد. Docker میتواند لایههای بدون تغییر را دوباره استفاده کند و لازم نیست در هر Build همهچیز را از ابتدا بسازد.
Container بهعنوان نمونه اجرایی Image
اگر Image را نسخه آماده اجرای برنامه بدانیم، کانتینر نمونهای است که از روی آن ساخته و اجرا میشود. از یک Image واحد هم میتوان چند کانتینر مستقل ایجاد کرد.
مثلاً یک Image از API خود دارید و سه کانتینر از روی آن اجرا میکنید. هر سه از یک Image شروع شدهاند، اما فرایند و وضعیت اجرایی خودشان را دارند.
تغییراتی که هنگام اجرا داخل کانتینر ایجاد میشوند، خود Image اصلی را تغییر نمیدهند. به همین دلیل دادههایی که باید بعد از حذف کانتینر باقی بمانند، معمولاً خارج از چرخه عمر آن و با ابزارهایی مثل Volume نگهداری میشوند.
Dockerfile و فرایند Build
Dockerfile فایلی متنی است که مرحلههای ساخت Image را مشخص میکند. برای یک سرویس ساده پایتون میتواند چیزی شبیه این باشد:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD [“python”, “app.py”]
هر خط کار مشخصی دارد: FROM Image پایه را انتخاب میکند، WORKDIR مسیر کاری را تعیین میکند، COPY فایلها را وارد Image میکند و RUN دستور لازم برای نصب وابستگیها را هنگام Build اجرا میکند. در پایان، CMD دستور پیشفرض اجرای Container را مشخص میکند.
ترتیب این دستورها هم بیدلیل نیست. مثلاً با کپیکردن requirements.txt و نصب وابستگیها قبل از کپی کل کد، اگر فقط کد برنامه تغییر کرده باشد، Docker در بسیاری از Buildها میتواند از Cache لایه وابستگیها استفاده کند و لازم نباشد پکیجها دوباره نصب شوند.
چرخه کار با Docker از Build تا Run
تا اینجا Dockerfile را نوشتهایم و میدانیم Image و Container چه تفاوتی دارند. حالا میتوانیم مسیر یک برنامه را از ساختهشدن Image تا اجرا روی سیستم مقصد دنبال کنیم.
چرخه کار با Docker را میتوان در چند عملیات اصلی خلاصه کرد:

- Build (بیلد): داکر براساس دستورهای Dockerfile، Image برنامه را میسازد.
- Tag (تگ): برای Image نام و نسخه مشخص میکنیم؛ مثلاً my-api:1.0.
- Run (ران): از روی Image یک Container ایجاد و اجرا میشود.
- Stop (استاپ): اجرای Container متوقف میشود، بدون اینکه لزوماً خود Container حذف شود.
- Push (پوش): Image در Registry قرار میگیرد تا سیستمها یا اعضای دیگر تیم هم به آن دسترسی داشته باشند.
- Pull (پول): Image از Registry روی سیستم مقصد دریافت میشود تا بتوان از روی آن Container اجرا کرد.
فرض کنید نسخه ۱.۰ یک API آماده انتشار است. ابتدا Image را Build و با نسخه مناسب Tag میکنید. میتوانید همان Image را روی سیستم خودتان Run و بررسی کنید و بعد آن را با Push در Registry قرار دهید. سرور مقصد نیز بهجای دریافت کد و نصب دوباره وابستگیها، Image را Pull میکند و Container را از روی همان نسخه اجرا میکند.
البته این مراحل یک زنجیره اجباری نیستند؛ مثلاً Push و Pull ارتباطی با Stop ندارند. این فلو بیشتر نشان میدهد یک Image چطور ساخته، نسخهگذاری، اجرا و میان محیطهای مختلف منتقل میشود.
Docker فقط یکی از بخشهای فرایند توسعه و استقرار است. اگر میخواهید ببینید این ابزار در کنار CI/CD، Cloud، مانیتورینگ و سایر مهارتهای این حوزه کجا قرار میگیرد، نقشه راه DevOps مسیر کاملتری را نشان میدهد.
Volume و Network در Docker چه کاربردی دارند؟

کانتینرها قابلحذف و جایگزینیاند، اما دادههای برنامه همیشه نباید همراه آنها از بین بروند. از طرف دیگر، سرویسهایی که در کانتینرهای جدا اجرا میشوند باید بتوانند با هم ارتباط داشته باشند. Volume و Network این دو نیاز را پوشش میدهند.
- نگهداری داده خارج از چرخه عمر Container
فایلهایی که هنگام اجرا در لایه قابلنوشتن کانتینر ایجاد میشوند، به همان کانتینر وابستهاند. برای دادههایی که باید ماندگار باشند، از Volume (والیوم؛ فضای ذخیرهسازی مستقل از چرخه عمر کانتینر) استفاده میشود.
Volume توسط Docker مدیریت و به کانتینر مانت میشود. مثلاً میتوان اطلاعات دیتابیس را روی یک Volume نگه داشت تا با حذف و ساخت دوباره کانتینر، دادهها همچنان در دسترس باشند.
- ارتباط کانتینرها و انتشار Port
اگر بکند و دیتابیس در دو کانتینر جدا اجرا شوند، Docker Network مسیر ارتباط آنها را فراهم میکند. در شبکههای تعریفشده توسط کاربر، کانتینرها میتوانند یکدیگر را با نام پیدا کنند و لازم نیست به آیپیهای متغیر وابسته باشند.
برای دسترسی از بیرون Docker نیز میتوان از Port Publishing (انتشار پورت) استفاده کرد. مثلاً پورت ۸۰۰۰ یک وبسرور داخل کانتینر را به پورتی روی سیستم میزبان متصل کرد تا سرویس از بیرون قابلدسترسی باشد.
در یک جمله: Volume داده را ماندگار میکند و Network ارتباط میان کانتینرها را میسازد.
Docker Compose چیست و چه زمانی استفاده میشود؟
یک پروژه Backend ممکن است علاوه بر خود برنامه به سرویسهایی مثل PostgreSQL و Redis نیاز داشته باشد. مدیریت جداگانه کانتینرهای این سرویسها، بهخصوص در کار تیمی، خیلی زود دردسرساز میشود.
Docker Compose اجازه میدهد سرویسها و تنظیماتی مثل Image، پورت، Volume و Network را در یک فایل YAML تعریف و کل محیط را یکجا اجرا یا متوقف کنید. مثلاً:

به همین دلیل Compose برای محیط توسعه، تست و پروژههای چند سرویسی روی یک میزبان کاربردی است. اما برای مدیریت کانتینرها روی چند نود و نیازهایی مثل مقیاسپذیری و بازیابی خودکار طراحی نشده است. این مرز را در مقاله تفاوت Kubernetes و Docker دقیقتر بررسی کردهایم.
تفاوت Container و ماشین مجازی
در نگاه اول، Container و Virtual Machine یا VM (ماشین مجازی) شبیه هم به نظر میرسند؛ هر دو کمک میکنند برنامه را در محیطی جدا از بخشهای دیگر سیستم اجرا کنیم. تفاوت اصلی را باید یک لایه پایینتر، یعنی در سیستمعامل، پیدا کرد.
در ماشین مجازی، هر VM سیستمعامل مهمان و Kernel خودش را دارد. برای مثال، اگر سه ماشین مجازی روی یک سرور اجرا کنید، هر کدام سیستمعامل مستقلی دارند که روی منابع مجازیشده اجرا میشود.
کانتینر چنین ساختاری ندارد. کانتینرهای لینوکس روی یک میزبان، Kernel سیستم میزبان را به اشتراک میگذارند و برنامهها به کمک سازوکارهایی مثل Namespace از یکدیگر جدا میشوند. به همین دلیل برای اجرای هر کانتینر لازم نیست یک سیستمعامل کامل جداگانه راهاندازی شود.
مقایسه کانتینر و ماشین مجازی
| معیار | Container | Virtual Machine |
| سیستم عامل | از Kernel میزبان استفاده میکند. | سیستمعامل مهمان و Kernel مستقل دارد. |
| نحوه جداسازی | جداسازی در سطح فرایندهای سیستمعامل | جداسازی در سطح ماشین مجازی |
| راهاندازی | معمولاً سریعتر | معمولاً زمان بیشتری نیاز دارد. |
| مصرف منابع | سبکتر و با سربار کمتر | به منابع بیشتری برای سیستمعامل مهمان نیاز دارد. |
| کاربرد رایج | بستهبندی و اجرای قابلتکرار برنامهها | اجرای محیطهای سیستمعاملی مستقل |
پس نمیتوان گفت کانتینر صرفاً «نسخه سبکتر ماشین مجازی» است. معماری این دو از پایه متفاوت است. Docker نیز کانتینر را یک فرایند ایزوله توصیف میکند که فایلهای مورد نیاز اجرای برنامه را همراه دارد، درحالیکه VM یک سیستمعامل کامل با Kernel خودش را اجرا میکند.
البته قرار نیست همیشه بین این دو یکی را انتخاب کنیم. در بسیاری از زیرساختها، VM و کانتینر کنار هم استفاده میشوند؛ مثلاً یک ماشین مجازی در Cloud ایجاد میشود و چند کانتینر روی همان VM اجرا میشوند.
کاربردهای Docker در توسعه، تست و استقرار نرمافزار

Docker در بخشهای مختلف چرخه توسعه یک مزیت مشترک دارد: محیط اجرای برنامه را قابلتکرار میکند.
در توسعه، اعضای تیم میتوانند پروژه را با نسخههای مشخصی از زبان، کتابخانهها و سرویسهای مورد نیاز اجرا کنند. در تست نیز میتوان محیط موردنیاز را برای هر بار اجرای تست ایجاد کرد و بعد از پایان کار کنار گذاشت؛ قابلیتی که در فرایندهای CI/CD هم کاربرد دارد.
در استقرار، همان Imageای که ساخته و تست شده میتواند از Registry دریافت و روی سرور اجرا شود؛ بنابراین لازم نیست وابستگیهای برنامه در هر محیط دوباره نصب و تنظیم شوند.
کار با Docker در کنار لینوکس، شبکه و CI/CD از مهارتهایی است که در مسیر استخدام DevOps هم با آنها روبهرو خواهید شد.
مزایا و محدودیتهای Docker
Docker بسیاری از دردسرهای جابهجایی و اجرای نرمافزار را کمتر میکند، اما برای هر پروژه و هر مسئلهای راهحل کامل نیست.
| مزایا | محدودیتها |
| ایجاد محیط اجرای قابلتکرار | نیاز به یادگیری مفاهیم و تنظیمات Docker |
| انتقال سادهتر برنامه بین محیطها | اشتراک Kernel میزبان در کانتینرهای لینوکس |
| راهاندازی سریعتر نسبت به VM | نیاز به مدیریت جداگانه دادههای ماندگار |
| جداسازی وابستگیهای پروژهها | افزایش پیچیدگی با زیادشدن سرویسها و کانتینرها |
در نتیجه، ارزش Docker بیشتر زمانی دیده میشود که قابلتکرار بودن محیط، جابهجایی برنامه و جداسازی وابستگیها برای پروژه اهمیت داشته باشد. در مقابل، استفاده از آن مسئولیتهایی مثل مدیریت Imageها، دادهها، شبکه و امنیت کانتینرها را هم به پروژه اضافه میکند.
اصول امنیتی ضروری هنگام استفاده از Docker
کانتینرها محیط برنامهها را از یکدیگر جدا میکنند، اما این جداسازی بهتنهایی به معنی امنبودن آنها نیست. چند اصل پایه میتواند ریسکهای رایج را کمتر کند:
- Imageها را از منابع معتبر دریافت و نسخههای آنها را بهروز نگه دارید.
- کانتینر را بدون نیاز با دسترسی root اجرا نکنید.
- رمز عبور، API Key و سایر اطلاعات حساس را داخل Dockerfile یا Image قرار ندهید.
- فقط پورتهایی را منتشر کنید که واقعاً باید از خارج کانتینر در دسترس باشند.
- برای کانتینرها محدودیت منابع در نظر بگیرید تا یک سرویس نتواند بیرویه CPU یا حافظه میزبان را مصرف کند.
امنیت Docker یک تنظیم یکباره نیست؛ از انتخاب Image تا نحوه اجرای کانتینر و دسترسی آن به میزبان، هر مرحله بخشی از امنیت محیط را میسازد.
برای شروع یادگیری Docker چه مفاهیمی را تمرین کنیم؟
Docker با خواندن دستورها یاد گرفته نمیشود؛ باید یک برنامه را بردارید و واقعاً داخل کانتینر اجرا کنید. برای شروع، یک API ساده انتخاب کنید و قدمبهقدم محیط اجرای آن را بسازید.
مسیر تمرین میتواند اینطور پیش برود:

در پایان باید بتوانید برای برنامه خودتان Image بسازید، آن را در کانتینر اجرا کنید، دادههای ماندگار را مدیریت کنید و چند سرویس مرتبط را کنار هم بالا بیاورید.
بعد از این پایهها، Docker را در کنار لینوکس، شبکه و CI/CD ادامه دهید. برای یادگیری ساختاریافتهتر این مهارتها هم میتوانید سراغ دورههای DevOps بروید.
جمعبندی
Docker قرار نیست همه مسائل توسعه و استقرار را حل کند؛ اما یک دردسر مهم را از سر راه برمیدارد: اینکه برنامه برای اجرا، هر بار به یک محیط تازه و تنظیمات دوباره نیاز داشته باشد.
با داکر، برنامه و محیط اجرای آن در قالب Image تعریف میشوند و کانتینر همان محیط را به اجرا درمیآورد. از اینجا به بعد، مفاهیمی مثل Volume، Network و Docker Compose کمک میکنند این ساختار را برای پروژههای واقعیتر گسترش دهید.
شاید بهترین تعریف داکر همین باشد: راهی برای اینکه «روی سیستم من کار میکند» به «میتوانیم همین را جای دیگری هم اجرا کنیم» نزدیکتر شود.
