بهترین شیوه‌ها


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

بهترین شیوه‌های رسمی Dockerfile محتوای عالی زیادی درباره‌ی نحوه‌ی بهبود Dockerfileهایتان دارند.

کارایی

باید در درجه‌ی اول برای کارایی بهینه‌سازی کنید (به‌ویژه برای test runnerها). این تضمین می‌کند ابزارهایتان تا حد امکان سریع اجرا شوند و تایم‌اوت نشوند.

اندازه‌گیری

اندازه‌گیری مکرر زمان اجرا راهی عالی برای درک کارایی ابزارهاست. عادت کنید زمان اجرا را هم بعد و هم قبل از یک تغییر اندازه‌گیری کنید. حتی وقتی احساس می‌کنید «مطمئن» هستید که تغییری کارایی را بهتر می‌کند، باز هم باید زمان اجرا را اندازه‌گیری کنید.

اسکریپت‌ها

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

مخازن جدیدتر ابزارهای track به دو اسکریپت زیر دسترسی خواهند داشت:

  1. ./bin/benchmark.sh: بنچمارک کردن کد ابزارهای track (کد منبع)
  2. ./bin/benchmark-in-docker.sh: بنچمارک کردن ایمیج Docker ابزارهای track (کد منبع)
Note

اگر روی مخزن ابزارهای track کار می‌کنید و این فایل‌ها را ندارید، با خیال راحت با استفاده از لینک‌های منبع بالا آن‌ها را در مخزن خود کپی کنید.

Caution

اسکریپت‌های بنچمارک می‌توانند به تخمین کارایی ابزارها کمک کنند. اما در نظر داشته باشید که کارایی روی سرورهای production Exercism اغلب پایین‌تر است.

با ایمیج‌های پایه‌ی مختلف آزمایش کنید

امتحان کنید با ایمیج‌های پایه‌ی مختلف (مثلاً Alpine به‌جای Ubuntu) کار کنید تا ببینید آیا یکی (به‌طور قابل‌توجهی) از دیگری بهتر عمل می‌کند. اگر کارایی تقریباً برابر بود، ایمیجی را انتخاب کنید که کوچک‌ترین است.

شبکه‌ی Internal را امتحان کنید

بررسی کنید که استفاده از شبکه‌ی internal به‌جای none کارایی را بهتر می‌کند یا نه. برای اطلاعات بیشتر مستندات شبکه را ببینید.

دستورهای زمان build را به دستورهای زمان run ترجیح دهید

ابزارهای track یک کانتینر Docker یک‌بارمصرف و کوتاه‌عمر اجرا می‌کنند که مراحل زیر را انجام می‌دهد.

  1. یک کانتینر Docker ساخته می‌شود.
  2. کانتینر Docker با آرگومان‌های درست اجرا می‌شود.
  3. کانتینر Docker از بین می‌رود.

بنابراین، کدی که در مرحله‌ی ۲ اجرا می‌شود، برای هر بار اجرای ابزار اجرا می‌شود. به همین دلیل، کاهش مقدار کدی که در مرحله‌ی ۲ اجرا می‌شود راهی عالی برای بهبود کارایی است. یکی از راه‌های انجام این کار، انتقال کد از زمان run به زمان build است. در حالی که کد زمان run در هر بار اجرای ابزار اجرا می‌شود، کد زمان build فقط یک بار اجرا می‌شود (زمانی که ایمیج Docker ساخته می‌شود).

کد زمان build یک بار به‌عنوان بخشی از workflow GitHub Actions اجرا می‌شود. بنابراین، اشکالی ندارد اگر کدی که در زمان build اجرا می‌شود (نسبتاً) کند باشد.

مثال: پیش‌کامپایل کتابخانه‌ها

وقتی تست‌ها در test runner Haskell اجرا می‌شوند، نیاز است چند کتابخانه‌ی پایه کامپایل شوند. از آنجا که هر اجرای test در یک کانتینر تازه اتفاق می‌افتد، این یعنی این کامپایل در هر بار اجرای test انجام می‌شد! برای دور زدن این مشکل، Dockerfile مربوط به test runner Haskell دو دستور زیر را دارد:

COPY pre-compiled/ .
RUN stack build --resolver lts-20.18 --no-terminal --test --no-run-tests

ابتدا، پوشه‌ی pre-compiled در ایمیج کپی می‌شود. این پوشه به‌عنوان یک تمرین تست راه‌اندازی می‌شود و به همان کتابخانه‌های پایه‌ای وابسته است که تمرین واقعی به آن وابسته است. سپس تست‌ها را روی آن پوشه اجرا می‌کنیم، که مشابه نحوه‌ی اجرای تست‌ها برای یک تمرین واقعی است. اجرای تست‌ها باعث کامپایل شدن پایه می‌شود، اما تفاوت این است که این کار در زمان build اتفاق می‌افتد. بنابراین ایمیج Docker حاصل، کتابخانه‌های پایه‌اش از قبل کامپایل شده خواهد بود. این یعنی در زمان run نیازی به کامپایل نیست و در نتیجه اجرا (بسیار) سریع‌تر می‌شود.

مثال: پیش‌کامپایل باینری‌ها

برخی زبان‌ها اجازه می‌دهند کد به‌صورت ahead-of-time یا just-in-time کامپایل شود. این یک معامله‌ی زمان build در برابر زمان run است، و باز هم، به دلایل کارایی، اجرا در زمان build را ترجیح می‌دهیم.

Dockerfile مربوط به test runner C# از این رویکرد استفاده می‌کند، که در آن test runner به‌صورت ahead-of-time (در زمان build) به یک باینری کامپایل می‌شود، نه اینکه کد به‌صورت just-in-time (در زمان run) کامپایل شود. این یعنی در زمان run کار کمتری برای انجام دادن هست، که باید به افزایش کارایی کمک کند.

اندازه

باید تلاش کنید اندازه‌ی ایمیج را کاهش دهید، که یعنی:

  • استقرار سریع‌تر می‌شود
  • هزینه‌ها را برای ما کاهش می‌دهد
  • زمان راه‌اندازی هر کانتینر را بهتر می‌کند

توزیع‌های مختلف را امتحان کنید

ایمیج‌های توزیع‌های مختلف اندازه‌های متفاوتی دارند. برای مثال، ایمیج alpine:3.20.2 ده برابر کوچک‌تر از ایمیج ubuntu:24.10 است:

REPOSITORY   TAG       SIZE
alpine       3.20.2    8.83MB
ubuntu       24.10     101MB

به‌طور کلی، ایمیج‌های مبتنی بر Alpine از کوچک‌ترین ایمیج‌ها هستند، بنابراین بسیاری از ایمیج‌های ابزارها بر پایه‌ی Alpine هستند.

ایمیج‌های کم‌حجم را امتحان کنید

برخی ایمیج‌ها نسخه‌های خاص «slim» دارند که در آن‌ها بعضی قابلیت‌ها حذف شده‌اند و در نتیجه اندازه‌ی ایمیج کوچک‌تر می‌شود. برای مثال، ایمیج node:20.16.0-slim پنج برابر کوچک‌تر از ایمیج node:20.16.0 است:

REPOSITORY   TAG            SIZE
node         20.16.0        1.09GB
node         20.16.0-slim   219MB

دلیل کوچک‌تر بودن نسخه‌های «slim» این است که قابلیت‌های کمتری دارند. ممکن است ایمیج شما به قابلیت‌های اضافی نیازی نداشته باشد، و اگر این‌طور است، استفاده از نسخه‌ی «slim» را در نظر بگیرید.

حذف بخش‌های غیرضروری

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

  • فایل‌های منبعی که پس از ساخت یک باینری از آن‌ها دیگر لازم نیستند
  • فایل‌هایی که معماری متفاوتی از ایمیج Docker را هدف می‌گیرند
  • مستندات

فایل‌های package manager را حذف کنید

بیشتر ایمیج‌های Docker نیاز به نصب بسته‌های اضافی دارند، که معمولاً از طریق یک package manager انجام می‌شود. این بسته‌ها باید در زمان build نصب شوند (چون در زمان run اتصال اینترنت در دسترس نیست). بنابراین، هر فایل کش/نگهداری سوابق package manager باید پس از نصب بسته‌های اضافی حذف شود.

apk

توزیع‌هایی که از package manager apk استفاده می‌کنند (مانند Alpine) باید هنگام استفاده از apk add برای نصب بسته‌ها، پرچم --no-cache را به کار ببرند:

RUN apk add --no-cache curl
apt-get/apt

توزیع‌هایی که از package manager apt-get/apk استفاده می‌کنند (مانند Ubuntu) باید دستورهای apt-get autoremove -y و rm -rf /var/lib/apt/lists/* را بعد از نصب بسته‌ها و در همان دستور RUN اجرا کنند:

RUN apt-get update && \
    apt-get install curl -y && \
    apt-get autoremove -y && \
    rm -rf /var/lib/apt/lists/*

از ساخت‌های چندمرحله‌ای استفاده کنید

Docker قابلیتی به نام ساخت‌های چندمرحله‌ای دارد. این‌ها به شما اجازه می‌دهند Dockerfile خود را به مرحله‌های جداگانه تقسیم کنید، طوری که فقط مرحله‌ی آخر در ایمیج Docker تولیدشده قرار می‌گیرد (بقیه فقط برای پشتیبانی از ساخت مرحله‌ی آخر آنجا هستند). می‌توانید هر مرحله را مانند یک Dockerfile کوچک در نظر بگیرید؛ مرحله‌ها می‌توانند از ایمیج‌های پایه‌ی مختلف استفاده کنند.

ساخت‌های چندمرحله‌ای به‌ویژه وقتی مفید هستند که Dockerfile شما نیاز به نصب بسته‌هایی داشته باشد که فقط در زمان build لازم هستند. در این وضعیت، ساختار کلی Dockerfile شما به این شکل است:

  1. یک مرحله‌ی جدید تعریف کنید (که آن را مرحله‌ی «build» می‌نامیم). این مرحله فقط در زمان build استفاده می‌شود.
  2. بسته‌های اضافی مورد نیاز را (در مرحله‌ی «build») نصب کنید.
  3. دستورهایی را که به بسته‌های اضافی نیاز دارند (در مرحله‌ی «build») اجرا کنید.
  4. یک مرحله‌ی جدید تعریف کنید (که آن را مرحله‌ی «runtime» می‌نامیم). این مرحله ایمیج Docker حاصل را می‌سازد و در زمان run اجرا می‌شود.
  5. نتیجه(های) دستورهای اجراشده در مرحله‌ی ۳ (در مرحله‌ی «build») را به این مرحله (مرحله‌ی «runtime») کپی کنید.

با این چیدمان، بسته‌های اضافی فقط در مرحله‌ی «build» نصب می‌شوند و نه در مرحله‌ی «runtime»، که یعنی در ایمیج Docker تولیدشده قرار نمی‌گیرند.

مثال: دانلود فایل‌ها

test runner Fortran برای دانلود برخی فایل‌ها به curl نیاز دارد. با این حال، ایمیج زمان run آن به curl نیاز ندارد، که این را به یک مورد استفاده‌ی عالی برای ساخت چندمرحله‌ای تبدیل می‌کند.

ابتدا، Dockerfile آن مرحله‌ای (به نام «build») تعریف می‌کند که package curl در آن نصب می‌شود. سپس از curl برای دانلود فایل‌ها در آن مرحله استفاده می‌کند.

FROM alpine:3.15 AS build

RUN apk add --no-cache curl

WORKDIR /opt/test-runner
COPY bust_cache .

WORKDIR /opt/test-runner/testlib
RUN curl -R -O https://raw.githubusercontent.com/exercism/fortran/main/testlib/CMakeLists.txt
RUN curl -R -O https://raw.githubusercontent.com/exercism/fortran/main/testlib/TesterMain.f90

WORKDIR /opt/test-runner
RUN curl -R -O https://raw.githubusercontent.com/exercism/fortran/main/config/CMakeLists.txt

بخش دوم Dockerfile یک مرحله‌ی جدید تعریف می‌کند و فایل‌های دانلودشده را از مرحله‌ی «build» با استفاده از دستور COPY به مرحله‌ی خودش کپی می‌کند:

FROM alpine:3.15

RUN apk add --no-cache coreutils jq gfortran libc-dev cmake make

WORKDIR /opt/test-runner
COPY --from=build /opt/test-runner/ .

COPY . .
ENTRYPOINT ["/opt/test-runner/bin/run.sh"]
مثال: نصب کتابخانه‌ها

test runner Ruby نیاز دارد بسته‌های git، openssh، build-base، gcc و wget نصب شوند تا کتابخانه‌های مورد نیازش (gemها) بتوانند نصب شوند. Dockerfile آن با مرحله‌ای شروع می‌شود (با نام build) که آن بسته‌ها را (از طریق apk add) نصب می‌کند و سپس وابستگی‌ها را (از طریق bundle install) نصب می‌کند:

FROM ruby:3.2.2-alpine3.18 AS build

RUN apk update && apk upgrade && \
    apk add --no-cache git openssh build-base gcc wget git

COPY Gemfile Gemfile.lock .

RUN gem install bundler:2.4.18 && \
    bundle config set without 'development test' && \
    bundle install

سپس مرحله‌ای را تعریف می‌کند که ایمیج Docker حاصل را تشکیل می‌دهد. این مرحله وابستگی‌هایی را که مرحله‌ی قبلی نصب کرده بود نصب نمی‌کند، در عوض از دستور COPY استفاده می‌کند تا کتابخانه‌های نصب‌شده را از مرحله‌ی build به مرحله‌ی خودش کپی کند:

FROM ruby:3.2.2-alpine3.18

RUN apk add --no-cache bash

WORKDIR /opt/test-runner

COPY --from=build /usr/local/bundle /usr/local/bundle

COPY . .

ENTRYPOINT [ "sh", "/opt/test-runner/bin/run.sh" ]
Note

Dockerfile مربوط به test runner C# کار مشابهی انجام می‌دهد، فقط در این مورد مرحله‌ی build می‌تواند از یک ایمیج Docker موجود استفاده کند که بسته‌های اضافی لازم برای نصب کتابخانه‌ها را از قبل نصب کرده است.

تست

از تست‌های یکپارچگی استفاده کنید

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

از Docker استفاده کنید

برای بهترین شبیه‌سازی محیط production، تست‌های یکپارچگی باید ابزارها را مثل محیط production اجرا کنند. این یعنی ساختن ایمیج Docker و سپس اجرای ایمیج ساخته‌شده روی یک راه‌حل برای بررسی خروجی آن.

از تست‌های golden استفاده کنید

تست‌های یکپارچگی باید به‌صورت تست‌های golden تعریف شوند، که تست‌هایی هستند که خروجی مورد انتظار در یک فایل ذخیره می‌شود. این برای تست‌های یکپارچگی ابزارهای track عالی است، چون خروجی ابزارها هم فایل است.

مثال: test runner

وقتی test runner را روی یک راه‌حل اجرا می‌کنید، خروجی آن یک فایل results.json است. سپس می‌توانیم این فایل را با یک فایل خروجی «known good» (یعنی «مورد انتظار») (به نام expected_results.json) مقایسه کنیم تا بررسی کنیم آیا test runner همان‌طور که انتظار می‌رود کار می‌کند.

ایمنی

ایمنی دلیل اصلی است که ما از کانتینرهای Docker برای اجرای ابزارهایمان استفاده می‌کنیم.

ایمیج‌های رسمی را ترجیح دهید

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

نسخه‌ها را پین کنید

برای اطمینان از پایدار بودن buildها (یعنی اینکه ناگهان خراب نشوند)، باید همیشه ایمیج‌های پایه‌تان را به تگ‌های مشخص پین کنید. این یعنی به‌جای:

FROM alpine:latest

باید از این استفاده کنید:

FROM alpine:3.20.2

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

به‌عنوان کاربر غیر root اجرا کنید

به‌طور پیش‌فرض، بسیاری از ایمیج‌ها با کاربری اجرا می‌شوند که دسترسی root دارد. باید در نظر بگیرید که به‌عنوان یک کاربر غیر root اجرا کنید.

FROM alpine

RUN groupadd -r myuser && useradd -r -g myuser myuser

# RUN <COMMANDS THAT REQUIRE ROOT USER, E.G. INSTALLING PACKAGES>

USER myuser

مخازن بسته را به آخرین نسخه به‌روزرسانی کنید

(تقریباً) همیشه ایده‌ی خوبی است که آخرین نسخه‌ها را نصب کنید

RUN apt-get update && \
    apt-get install curl

از فایل‌سیستم فقط‌خواندنی پشتیبانی کنید

ما تشویق می‌کنیم Dockerfileها با استفاده از فایل‌سیستم فقط‌خواندنی نوشته شوند. تنها پوشه‌هایی که باید قابل نوشتن فرض کنید عبارت‌اند از:

  • پوشه‌ی راه‌حل (به‌عنوان آرگومان دوم پاس داده می‌شود)
  • پوشه‌ی خروجی (به‌عنوان آرگومان سوم پاس داده می‌شود)
  • پوشه‌ی /tmp
Caution

محیط production ما در حال حاضر فایل‌سیستم فقط‌خواندنی را اجبار نمی‌کند، اما ممکن است در آینده این کار را بکنیم. به همین دلیل، قالب پایه برای یک test runner/analyzer/representer جدید با یک فایل‌سیستم فقط‌خواندنی شروع می‌شود. اگر نمی‌توانید کارها را روی یک فایل فقط‌خواندنی راه بیندازید، با خیال راحت (برای حالا) یک فایل‌سیستم قابل نوشتن را فرض کنید.