مثال استفاده از docker compose - نمونه فایل .yml یه پروژه واقعی

docker compose یا داکر کامپوز

داکر کامپوز برای تعریف و اجرای اپلیکیشن های داکریه که از چندین کانتینر تشکیل شده اند. یعنی اینکه مثلا تصور کنید برای اجرای یه سایت به این سرویس ها نیاز داریم: وب سرور، دیتابیس، بک اند، سیستم ارسال ایمیل و … با استفاده از docker compose می تونیم همه ی این سرویس ها رو دقیقا با ورژن و تنظیماتی که لازم داریم تعریف کنیم و بدیم به داکر که همه رو با ورژن های دقیق، برای ما راه اندازی کنه.

خیلی وقتا اتفاق میوفته که کد و پروژه ای که می سازیم روی لپ تاپ یا سیستم لوکالمون درست کار میکنه ولی وقتی آپلودش می کنیم روی سرور خطا میده. اکثر مواقع دلیلش اینه که مثلا نسخه دیتابیسی که رو سرور وجود داره با نسخه لوکالمون فرق داره. یکی از مزایای docker compose حل این مشکله یعنی دقیقا همون ورژنی از نرم افزار که انتظارشو داریم ایجاد میشه و باگ های غیرمنتظره کمتر پیش میاد. یه مزیت دیگش هم راحت تر کردن و تسریع پروسه راه اندازیه.

قبل از ادامه اول آخرین ورژن داکر رو نصب کنید. (کامپوز خودش با داکر نصب میشه)

نمونه فایل docker-compose.yml

داکر و رسانیکا

سایت رسانیکا هم با استفاده از داکر کامپوز راه اندازی شده و فایل docker-compose.yml اون تقریبا به این شکله:

version: '3.8'

networks:
  rt-network:
    driver: bridge

secrets:
  postgres_db:
    file: ./.secrets/postgres_db
  postgres_usr:
    file: ./.secrets/postgres_usr
  postgres_pass:
    file: ./.secrets/postgres_pass
  jwt_secret:
    file: ./.secrets/jwt_secret
  rfs_secret:
    file: ./.secrets/rfs_secret

x-service-defaults:
  &service-defaults
  ulimits:
    core:
      soft: 0
      hard: 0
  restart: unless-stopped
  tty: true
  networks:
    - rt-network

x-pg-secrets:
  &pg-secrets
  - postgres_db
  - postgres_usr
  - postgres_pass

x-pg-secrets-env:
  &pg-secrets-env
  POSTGRES_DB_FILE: /run/secrets/postgres_db
  POSTGRES_USER_FILE: /run/secrets/postgres_usr
  POSTGRES_PASSWORD_FILE: /run/secrets/postgres_pass


services:
  postgres:
    <<: *service-defaults
    image: postgres:13.2-alpine
    volumes:
      - ./storage/postgres:/var/lib/postgresql/data
    expose:
      - '5432'
    environment:
      <<: *pg-secrets-env
      TZ: UTC
      LANG: en_US.utf8
    secrets: *pg-secrets

  redis:
    <<: *service-defaults
    image: redis:6.0.5-alpine
    expose:
      - '6379'
    environment:
      TZ: UTC
      NODE_ENV: ${APP_ENV}

  elastic:
    <<: *service-defaults
    image: elasticsearch:8.6.2
    volumes:
      - ./storage/elastic:/usr/share/elasticsearch/data
    expose:
      - '9200'
    environment:
      TZ: UTC
      xpack.security.enabled: 'false'
      ingest.geoip.downloader.enabled: 'false'
      bootstrap.memory_lock: 'true'
      discovery.type: single-node
      ES_JAVA_OPTS: '-Xms512m -Xmx512m'

  api:
    &api
    <<: *service-defaults
    image: node:18-alpine
    user: node:node
    command: ['npm', 'run', '${API_CMD}']
    working_dir: /main
    volumes:
      - ./storage/api:/main/storage
      - ./shared-types:/shared-types
      - ./api:/main
    environment:
      TZ: UTC
      APP_TZ: ${APP_TZ}
      APP_CALENDAR: ${APP_CALENDAR}
      EXPOSED_PORT: 4500
      APP_DOMAIN: ${APP_DOMAIN}
      APP_PROTOCOL: ${APP_PROTOCOL}
      APP_TITLE: ${APP_TITLE}
      NODE_ENV: ${APP_ENV}
      SMS_SECRET: ${SMS_SECRET}
      SMS_TEMPLATE_VERIFY: ${SMS_TEMPLATE_VERIFY}
      SMS_TEMPLATE_NOTIFS: ${SMS_TEMPLATE_NOTIFS}
      SMS_FROM: ${SMS_FROM}
      SMS_USE_TEXT: ${SMS_USE_TEXT}
      ALLOW_EMAIL_AUTH: ${ALLOW_EMAIL_AUTH}
      GOOGLE_OAUTH_CLIENT_ID: ${GOOGLE_OAUTH_CLIENT_ID}
    expose:
      - '4500'
    depends_on:
      - postgres
      - elastic
    secrets:
      - postgres_db
      - postgres_usr
      - postgres_pass
      - jwt_secret
      - rfs_secret

  api_cron:
    <<: *api
    command: ['npm', 'run', 'cron']
    expose: []

  rfs:
    &rfs
    <<: *service-defaults
    image: node:18-alpine
    user: node:node
    command: ['npm', 'run', '${RFS_CMD}']
    working_dir: /main
    volumes:
      - ./storage/rfs:/main/storage
      - ./shared-types:/shared-types
      - ./rfs:/main
    environment:
      TZ: UTC
      APP_TZ: ${APP_TZ}
      EXPOSED_PORT: 3500
      APP_DOMAIN: ${APP_DOMAIN}
      APP_PROTOCOL: ${APP_PROTOCOL}
      APP_TITLE: ${APP_TITLE}
      NODE_ENV: ${APP_ENV}
    expose:
      - '3500'
    depends_on:
      - postgres
    secrets:
      - rfs_secret

  rfs_cron:
    <<: *rfs
    command: ['npm', 'run', 'cron']
    expose: []

  next:
    <<: *service-defaults
    image: node:18-alpine
    user: node:node
    command: ['npm', 'run', '${NEXT_CMD}']
    working_dir: /main
    volumes:
      - ./next:/main
      - ./shared-types:/shared-types
    environment:
      TZ: UTC
      EXPOSED_PORT: 3000
      NEXT_PUBLIC_APP_TZ: ${APP_TZ}
      NEXT_PUBLIC_APP_TITLE: ${APP_TITLE}
      NEXT_PUBLIC_APP_DOMAIN: ${APP_DOMAIN}
      NEXT_PUBLIC_APP_PROTOCOL: ${APP_PROTOCOL}
      NEXT_PUBLIC_ENV: ${APP_ENV}
      NODE_ENV: ${APP_ENV}
      NEXT_PUBLIC_TRACKING_ID: ${APP_TRACKING_ID}
      NEXT_PUBLIC_ALLOW_EMAIL_AUTH: ${ALLOW_EMAIL_AUTH}
      NEXT_PUBLIC_GOOGLE_OAUTH_CLIENT_ID: ${GOOGLE_OAUTH_CLIENT_ID}
    expose:
      - '3000'
    secrets: *pg-secrets

  nginx:
    <<: *service-defaults
    image: nginx:1.18-alpine
    volumes:
      - ./storage/errors:/etc/nginx/service-errors
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./storage/rfs/public:/rfs/storage
    environment:
      TZ: UTC
    ports:
      - '127.0.0.1:${APP_PORT}:80'
    depends_on:
      - api
      - rfs
      - next

توضیح قسمت های مختلف فایل yml

فایل های yml معمولا برای تنظیمات و کانفیگ نرم افزار استفاده میشن و یه فرمت خاصی دارن به اسم YAML که داکر کامپوز هم از این فرمت استفاده میکنه. توی کانفیگ فایل موردنظر ما، هفت بخش اصطلاحا روت داریم:

version: '3.8'

networks:

secrets:

x-service-defaults:

x-pg-secrets:

x-pg-secrets-env:

services:
  • version همیشه باید اولین بخش فایل docker-compose.yml باشه و نسخه ی اون رو مشخص کنه چون توی نسخه های مختلف ویژگی های مختلفی وجود داره.

  • networks یه سری شبکه تعریف میکنه که سرویس هامون از طریق این شبکه ها باهم در ارتباط هستند. مثلا دیتابیس و بک اند رو میذاریم توی یه شبکه مشترک که بتونن به هم دیگه دسترسی داشته باشن. اینجا من یه شبکه با اسم rt-network و نوع bridge تعریف کردم. اطلاعات بیشتر

  • secrets کلید ها و رمز های استفاده شده توسط سرویس ها رو تعریف میکنه. در واقع بجای اینکه رمز ها رو مستقیما توی سورس کد بنویسیم، از طریق داکر سکرت تعریفشون میکنیم و موقع اجرا به سرویس ها میدیم. با این روش رمز ها از سورس کد جدا میشن که امنیت بیشتری داره.
    اینجا این کد یعنی یه سکرت با اسم postgres_pass تعریف کن و مقدارش رو از فایل ./.secrets/postgres_pass بردار (فایل رو خودمون ایجاد میکنم تو سرور یا سیستم لوکال):

postgres_pass:
  file: ./.secrets/postgres_pass
  • بخش هایی که با x- شروع میشن بهشون میگن extension fields و برای جلوگیری از تکرار هستند. مثلا این تنظیمات بین همه ی سرویس ها مشترکه که با اسم service-defaults تعریف کردم:

  ulimits:
    core:
      soft: 0
      hard: 0
  restart: unless-stopped
  tty: true
  networks:
    - rt-network
  • services مهم ترین بخش این فایله و همه سرویس های این اپلیکیشن رو تعریف میکنه. اینجا ما 9 تا سرویس داریم که به ترتیب توضیح میدم:

    • postgres دیتایبسه (توی عنوان سرویس یعنی بجای postgres هر اسمی میشه استفاده کرد). <<: *service-defaults یعنی کانفیگ هایی که با x-service-defaults تعریف کردیم رو روی این سرویس اعمال کن. image: postgres:13.2-alpine این خط مشخص میکنه دیتابیس postgres ورژن 13 رو راه اندازی کن. به صورت پیش فرض داکر این ایمیج ها رو از داکر هاب برمیداره، میتونین برین اونجا و ایمیج مورد نظرتون رو پیدا کنید.
      نکته مهم: قسمت volumes اینجا تعریف میکنه که دقیقا فولدر /var/lib/postgresql/data دیتابیس رو روی فولدر ./storage/postgres سیستم قرار بده. ⚠ اگه اینکارو نکنیم با هر بار ریستارت این سرویس کل اطلاعات دیتابیس پاک میشه. هر فولدری توی هر سرویسی خواستیم اطلاعاتش ریست و پاک نشه باید براش volume تعریف کنیم.
      توی expose پورت هایی که برای این سرویس باید باز بشه رو تعریف میکنم. این نکته رو هم بگم که هر اسمی واسه سرویس بدین به صورت پیشفرض هاست نیم سرویس میشه. یعنی اینجا postgres:5432 آدرس اتصال به دیتابیسه. توی environment متغیر های محیطی این سرویس رو تعیین میکنیم و توی secrets تعریف میکنیم به کدوم یک از سکرت ها دسترسی داشته باشه

    • redis یه دیتابیس از نوع in-memory با سرعت بالاست که برای کش استفاده میکنیم. این سرویس چون صرفا برای کش استفاده میشه هیچ ولومی نداره و موقع ریستارت اطلاعاتش پاک میشه.

    • elastic یه سیستم جستجو هست. اگه الان یچیزی داخل رسانیکا سرچ کنید، این سرویس نتایج رو براتون پیدا میکنه.

    • api در واقع بک اند سایته که بر پایه ایمیج nodejs ساخته شده. اینجا چندتا آپشن جدید داریم. user همون یوزر و گروهیه که تو لینوکس تعریف میشه. command دستور پیشفرضی هست که موقع بالا اومدن سرویس اجرا میشه. و working_dir دایرکتوری یا فولدر پیشفرض موقع اجرای دستوراته. توی depends_on میتونیم لیست سرویس های پیش نیاز رو مشخص کنیم تا داکر همیشه قبل از بالا آوردن این سرویس، اونا رو بالا بیاره. مثلا دیتابیس برای شروع به کار بک اند لازمه و قبل از اون باید بالا بیاد.

    • api_cron سرویس کار های پس زمینه و زمان بندی شده هست. دقیقا مشخصات api رو داره <<: *api فقط دستور پیشفرضش فرق میکنه و هیچ پورتی لازم نداره واسه همین expose خالی دادم.

    • rfs بک اند پردازش فایل هاست. تصاویر و ویدیو هایی که توی سایت آپلود میشه توسط این سرویس بهینه و اعتبار سنجی میشه.

    • rfs_cron هم کار های پس زمینه و زمان بندی شده ی مربوط به فایل هاست. مثلا هر روز فایل های استفاده نشده رو پاک میکنه.

    • next سرویس UI و فرانت رسانیکاه.

    • nginx وب سرور اصلیه و درخواست های HTTP رو به سرویس های مربوطه ارجاع میده. اینجا آپشن ports پورت مپینگ هست. شبکه rt-network رو که تعریف کردیم و گفتیم این سرویس ها ازش استفاده کنن یه شبکه ایزوله و امن هست و داکر اجازه نمیده هیچ درخواستی از خارج از این شبکه به دیتابیس و … وارد بشه. با استفاده از پورت مپینگ به داکر میگیم برای این سرویس و این پورت اجازه دسترسی بده. یعنی مثلا وقتی یه درخواست به rasanika.com/api/ ارسال میشه اول میره به سرویس nginx که پورت مپینگ داره و دامنه روی اون تنظیمه، بعدش nginx تصمیم میگیره این درخواست رو هدایت میکنه به سرویس api یا هر سرویس دیگه ای.

برای دیدن همه ی ویژگی ها میتونید به رفرنس فایل docker-compose.yml مراجعه کنید.

استفاده از متغیر های .env

حتما توی این فایل یه سری عبارت های اینطوری ${…} مثل ${APP_DOMAIN} دیدین، اینا در واقع اسم متغیر های محیطی یا environment variable هستن که داکر مقدارشون رو از داخل فایل .env کنار فایل docker-compose.yml میخونه. بخش از فایل .env رسانیکا در سرور:

APP_DOMAIN="rasanika.com"
API_CMD="start"
NEXT_CMD="start"
RFS_CMD="start"

و همچنین روی سیستم لوکال (محیط کدنویسی):

APP_DOMAIN="localhost"
API_CMD="dev"
NEXT_CMD="dev"
RFS_CMD="dev"

همونطور که میبینید این روش به ما اجازه میده خیلی راحت کانفیگ داکر رو بر اساس محیط پروداکشن سرور یا محیط کدنویسی تغییر بدیم. البته میشه از docker-compose.prop.yaml هم استفاده کرد ولی من env رو ترجیح میدم.

بررسی وضعیت سرویس ها

بعد اینکه با دستور docker compose up -d پروژه رو بالا آوردیم میتونیم وضعیت سرویس ها رو با دستور docker compose ps بررسی کنیم. نمونش به این صورته:

NAME                     IMAGE                  COMMAND                  SERVICE             CREATED             STATUS              PORTS
rasanika_elastic_1     elasticsearch:8.6.2    "/bin/tini -- /usr/l…"   elastic             2 days ago          Up 2 days           9200/tcp, 9300/tcp
rasanika_nginx_1       nginx:1.18-alpine      "/docker-entrypoint.…"   nginx               20 hours ago        Up 20 hours         127.0.0.1:10903->80/tcp
rasanika_postgres_1    postgres:13.2-alpine   "docker-entrypoint.s…"   postgres            2 days ago          Up 2 days           5432/tcp
rasanika_api_1         node:18-alpine         "docker-entrypoint.s…"   api                 20 hours ago        Up 20 hours         4500/tcp
rasanika_api_cron_1    node:18-alpine         "docker-entrypoint.s…"   api_cron            20 hours ago        Up 1 second
rasanika_redis_1       redis:6.0.5-alpine     "docker-entrypoint.s…"   redis               2 days ago          Up 2 days           6379/tcp
rasanika_next_1        node:18-alpine         "docker-entrypoint.s…"   next                20 hours ago        Up 20 hours         3000/tcp
rasanika_rfs_1         node:18-alpine         "docker-entrypoint.s…"   rfs                 2 days ago          Up 20 hours         3500/tcp
rasanika_rfs_cron_1    node:18-alpine         "docker-entrypoint.s…"   rfs_cron            2 days ago          Up 20 hours

راستی اگه علاقه داشتین مراحل راه اندازی (کدنویسی، سرور، دامنه و …) یه سایت کامل رو توی این ویدیو توضیح دادم:

در نهایت امیدوارم این پست برای شما مفید واقع شده باشه و خیلی ممنونم. موفق و پیروز باشید. 🌹💪


کامنت ها