در این پست تصمیم داریم درباره‌ی استفاده از docker compose برای ایجاد برنامه‌های چند کانتینری توضیحاتی بدیم.

فرض کنین برنامه‌ای دارین که به سرویس‌های مختلفی مثل سرور Node.js و بانک اطلاعاتی PostgreSQL، نیاز داره.
در این حالت می‌تونین با استفاده از docker compose، فایلی به اسم docker-compose.yml ایجاد کنین و کل پیکربندی سرویس‌های مختلف برنامه مثل وب سرور سمت کلاینت، وب سرور اپلیکیشن و بانک اطلاعاتی رو در این فایل تعریف کنین.

قبل از بررسی مراحل لازم برای تعریف و اجرای یک پروژه‌ی چند کانتینری، درباره‌ی اهمیت برنامه‌های چند کانتینری و مفاهیم کلیدی داکر و docker compose، صحبت می‌کنیم و بعد از اون می‌ریم سراغ ایجاد برنامه‌‌ی چند کانتینری.

چرا برنامه‌های چند کانتینری راه‌اندازی کنیم؟

داکر یک پروژه‌ی متن بازه و ایجاد یک محیط توسعه‌ی local رو آسون‌تر کرده.

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

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

برای ایجاد برنامه‌های چند کانتینری، داکر ابزار docker compose رو معرفی کرده:

به کمک این ابزار، توسعه‌دهنده‌ فایل yaml رو ایجاد کرده و تنظیمات مربوط به سرویس‌های برنامه رو در این فایل تعریف می‌کنه.

بعد از اون می‌تونه با یک دستور واحد همه‌ی سرویس ها رو به راحتی راه‌اندازی و اجرا کنه.

مروری بر تعاریف کلیدی 

داکر چیست؟  

گفتیم که داکر ابزاریه برای ایجاد، استقرار و اجرای راحت‌تر برنامه‌ با استفاده از کانتینرها.

کانتینر این امکان رو به برنامه‌نویس‌ها می‌ده تا یک برنامه رو با تمام ماژول‌ها و فایل‌ها (مثل کتابخانه‌ها و توابع)، یکجا بسته‌بندی کنن و این بسته‌ی قابل‌حمل رو در سیستم‌عامل‌های مختلف اجرا کنن.

با انجام این کار و به لطف کانتینرها، توسعه‌دهنده می‌تونه اطمینان داشته باشه این برنامه روی هر نوع ماشین لینوکسی به درستی اجرا می‌شه.

داکر شبیه به قطعات لگو می‌مونه که این قطعات برای تسهیل فرآیند توسعه و حذف سربار، در یک بسته‌ی قابل‌حمل در کنار هم قرار گرفتن.


تا این‌جا توضیح دادیم که داکر چیه و چه کاری انجام می‌ده.

حالا می‌ریم سراغ docker compose، و می‌گیم که چطوری می‌تونین اپلیکیشن‌های داکر چند کانتینری رو بسازین.

آشنایی با Docker Compose

docker compose به صورت رایگان در  زمان نصب داکر ارائه می‌شه و در اجرای برنامه‌های پیچیده به توسعه‌دهنده‌ها کمک می‌کنه.
docker compose،‌ این امکان رو ایجاد می‌‌کنه تا از یک فایل yaml ‌برای کار با برنامه‌های چند کانتینری استفاده کنین.

 اما قبل از این‌که وارد جزئیات فنی بشیم، اصلا ببینیم چرا توسعه‌دهنده‌ها باید از docker compose استفاده کنن. 

دلایل اهمیت Docker Compose

قابلیت حمل:

docker compose این امکان رو می‌ده تا با دستور docker-compose up، محیط توسعه‌ی کاملی رو ایجاد کنین و با دستور docker-compose down هم به راحتی تمام سرویس‌ها رو متوقف کنین.
این کار باعث نگه‌داری محیط توسعه در یک مکان اصلی می‌شه و استقرار برنامه‌ها به راحتی انجام می‌گیره.

قابلیت تست:

یکی از ویژگی‌های جالب docker compose پشتیبانی از اجرای تست‌های واحد و E2E، به صورت سریع و قابل تکراره و این تست‌ها در محیط‌های مخصوص به خودش انجام می‌شه.

چندین محیط ایزوله شده در یک host واحد:

docker compose، از نام‌ پروژه برای ایزوله کردن محیط‌ها از هم استفاده می‌کنه. با این‌کار:

  • از تداخل پروژه‌ها و سرویس‌های مختلف، جلوگیری می‌شه. 
  • و همچنین می‌تونین نسخه‌های زیادی از محیط ایزوله شده رو، روی یک دستگاه اجرا کنین. 

فایل docker-compose.yml چیست؟ 

باید در مسیر root برنامه‌، برای تعیین تنظیمات کانتینرها یک فایل yaml ایجاد کنین.

با استفاده از این فایل، تنظیمات مربوط به تمام سرویس‌های (کانتینرها) برنامه رو می‌نویسیم، نحوه‌ی تعامل سرویس‌ها با یکدیگر و سیستم عامل میزبان رو تعریف می‌کنیم، و بعد با یک دستور همه رو اجرا می‌کنیم.

به طور کلی اگه برنامه‌ی شما به یک database، یک cache ،queue و سرویس API و…، وابسته باشه، می‌تونین تمام این وابستگی‌ها رو به عنوان سرویس‌های برنامه در یک فایل yml، تعریف کنین.
بعد با یک دستور واحد شروع به کار کنین؛ دیگه نیازی به نصب و اجرای همه‌ی سرویس‌ها به طور مجزا نیست.

برای استفاده از docker compose، باید سه کار زیر رو انجام بدین که جزئیات انجام این کارها رو در ادامه بهتون می‌گیم:

  1. محیط اپلیکیشن رو در dockerfile با استفاده از دستوراتش تعریف کنین.
  2. سرویس‌های برنامه‌‌تون رو که می‌خواین در محیطی ایزوله با هم کار کنن، در docker-compose.yml تعریف کنین.
  3.  و در آخر دستور docker-compose up رو اجرا کنین.  

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

ساختار پروژه‌ی فعلی 

کدهای این پروژه‌ی فعلی، در گیت‌هاب قرار داده شده و می‌تونین ازش استفاده کنین. 

فرض کنین پروژه‌ی ما از دو سرویس Node.js‌ ای و یک پایگاه داده‌‌ی Postgres تشکیل شده. یعنی تو این پروژه، کانتینرها رو برای دو سرور Node.js‌ و یک پایگاه داده‌ی Postgres تنظیم می‌کنیم. 

شما می‌تونین با تنظیم درست dockerfile،  هر دو سرور رو به زبان‌ یا چارچوب‌های مختلف دیگه هم بنویسین. 

برای تست راحت این پروژه، دو سرور Node.js رو با دو endpoint میزبانی می‌کنیم: 

### Sample requests
curl http://localhost:4000/

# Expected response
#
#  {"message":"Hello from Server 1"}
#

curl http://localhost:3001/

# Expected response
#
#  {"message":"Hello from Server 2"}
#

ساختار پروژه‌ی فعلی هم اینه:

docker-example
- server1 (will run on port 4000)
- server2 (will run on port 3001)
- docker-compose.yml (will initialize all Docker containers: postgres, server1, and server2)

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

۱. اضافه کردن dockerfile

اول باید dockerfile رو به دو سرور پروژه‌ی ‌Node.js اضافه کنیم.

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

برای dockerfile ،server1 چیزی شبیه به کد زیر می‌شه:

FROM node:12.4.0
EXPOSE 4000

WORKDIR /home/server1

COPY package.json /home/server1/
COPY package-lock.json /home/server1/

RUN npm ci

COPY . /home/server1

RUN npm install

ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.2.1/wait /wait
RUN chmod +x /wait

## Launch the wait tool and then your application
CMD /wait && PORT=4000 yarn start

دستورهای dockerfile

حالا هر کدوم از دستورات بالا رو با هم بررسی می‌کنیم:

  • برای ساخت این پروژه، پورت 4000 رو EXPOSE کرده و ورژن 12.4.0 ایمیج Node رو دانلود می‌کنیم. 
  • دستورات شما در دایرکتوری کاری که مشخص می‌کنین اجرا می‌شن.
    پس یک دایرکتوری کاری ایجاد می‌کنیم و پوشه‌های مورد نیاز رو در اون مسیر کپی می‌کنیم.
  • زمانی که پروژه ساخته می‌شه، تمام سرویس‌های داکر موجود در docker-compose.yml به طور هم‌زمان، راه‌اندازی و اجرا می‌شن.
    در نمونه کد بالا دستور wait/، یعنی قبل از اجرای دستور بعدی، باید منتظر بارگذاری یک ایمیج باشیم. مثلا برای این‌که مشکل اتصال به پایگاه‌داده وجود نداشته باشه، اول پایگاه داده‌ی PostgreSQL راه‌اندازی می‌شه و بعد سرورها شروع به کار می‌کنن. 
  • در نهایت در دستور آخر، یک کانتینر داکر ایجاد کردیم که می‌تونه از طریق docker-compose.yml راه‌اندازی بشه.   

مشابه نمونه کد بالا، یک dockerfile هم باید برای server2 تعریف کنین.

۲. تعریف سرویس‌ها در فایل docker-compose.yml

فایل docker-compose.yml، نقطه شروع برنامه‌ی ماست. یک فایل متنی که همه‌ی تنظیمات رو یک‌بار در اون وارد می‌کنیم و هر بار با همون، کانتینر رو می‌سازیم.

برای راه‌اندازی همه‌ی کانتینرها (در این مثال Expose ،(Postgres ،Server2 ،Server1 و map کردن پورت‌ها با استفاده از متغیرهای محیطی موجود، از این فایل استفاده می‌شه.

# docker-compose.yml
version: "3.3"
services:
 postgres:
   image: postgres
   hostname: postgres
   environment:
     POSTGRES_DB: postgres
     POSTGRES_USER: postgres
     POSTGRES_PASSWORD: postgres
   ports:
     - 5432:5432

app:
   build: ./server1
   hostname: app
   env_file: ./server1/.env
   depends_on:
     - postgres
   links:
     - postgres
   ports:
     - 4000:4000
   environment:
     WAIT_HOSTS: postgres:5432


client:
   build: ./server2
   hostname: client
   env_file: ./server2/.env
   ports:
     - 3001:3001
   depends_on:
     - postgres
   links:
     - postgres
   environment:
     WAIT_HOSTS: postgres:5432

دستورهای docker-compose.yml

در کد بالا نمونه‌ای از یک docker-compose.yml نشون داده شده:

  • هر کدوم از سرویس‌های برنامه رو در بخش services تعریف می‌کنیم. یعنی برای سرویس‌های مختلفی که داریم، پیکربندی جداگانه ایجاد می‌کنیم.
  • متغیرهای محیطی مورد نیاز برای هر کانتینر با تگ environment مشخص شده. بنابراین می‌تونیم متغیرهایی که کانتینر مورد نظر برای استفاده بهش نیاز داره رو در این قسمت مقداردهی کنیم.
  • پورت‌های مورد نیاز رو EXPOSE می‌کنیم و مسیر  build ‌رو اضافه می‌کنیم. 

۳. اجرای دستور docker-compose up

حالا برای اجرای برنامه از دستور docker -compose up استفاده می‌کنیم.
با این دستور می‌تونیم سه کانتینر  موجود رو راه‌اندازی کنیم. برای اجرای پروژه فعلی ترمینال رو باز کرده و دستور زیر رو وارد می‌کنیم:‌

cd to-the-parent-directory
docker-compose up

- برای متوقف کردن و حذف کردن کانتینرها هم از دستور زیر استفاده می‌کنیم: 

cd to-the-parent-directory
docker-compose down

- برای کسب اطلاعات بیش‌تر در مورد کانتینرهای در حال اجرا، از دستور docker ps استفاده می‌کنیم:

docker ps

برای این‌که از صحت عملکرد سرورها مطمئن بشین، می‌تونین درخواست‌هایی رو به کمک curl انجام بدین. 

در نهایت برای دسترسی به server1 از طریق server2، باید host URL رو برابر با  app (نه localhost) در env. سرور دوم قرار بدین. چون host name ،app مربوط به server1 است. 

به طور مشابه، هنگام اتصال به بانک اطلاعاتی، باید از postgres که نام سرویس در فایل yml است، استفاده کنین (نه از localhost).  

نتیجه‌گیری

در این پست ضمن معرفی داکر و docker compose، نحوه‌ی ایجاد برنامه‌های چند کانتینری را توضیح دادیم. 

برای این‌کار تمام وابستگی های برنامه مانند سرورهای Node.js و پایگاه داده‌ی PostgreSQL رو در فایل yml تعریف کردیم و با یک دستور همه رو اجرا کردیم.

امیدوارم این پست به شما به عنوان یک توسعه‌دهنده در درک docker compose و نحوه استفاده ازون در توسعه‌ی برنامه‌ها کمک کرده باشه.

منابع:
Creating multi-container Docker Applications
Overview of Docker Compose
Docker Compose Overview