بهترین شیوههای رسمی Dockerfile محتوای عالی زیادی دربارهی نحوهی بهبود Dockerfileهایتان دارند.
باید در درجهی اول برای کارایی بهینهسازی کنید (بهویژه برای test runnerها). این تضمین میکند ابزارهایتان تا حد امکان سریع اجرا شوند و تایماوت نشوند.
اندازهگیری مکرر زمان اجرا راهی عالی برای درک کارایی ابزارهاست. عادت کنید زمان اجرا را هم بعد و هم قبل از یک تغییر اندازهگیری کنید. حتی وقتی احساس میکنید «مطمئن» هستید که تغییری کارایی را بهتر میکند، باز هم باید زمان اجرا را اندازهگیری کنید.
در صورت امکان، اسکریپتهایی بسازید که کارایی را بهطور خودکار اندازهگیری کنند (که به آن بنچمارک کردن هم میگویند). یک ابزار خط فرمان بسیار مفید hyperfine است، اما با خیال راحت از هر چیزی که برای ابزارهایتان منطقیتر است استفاده کنید.
مخازن جدیدتر ابزارهای track به دو اسکریپت زیر دسترسی خواهند داشت:
./bin/benchmark.sh: بنچمارک کردن کد ابزارهای track (کد منبع)./bin/benchmark-in-docker.sh: بنچمارک کردن ایمیج Docker ابزارهای track (کد منبع)اگر روی مخزن ابزارهای track کار میکنید و این فایلها را ندارید، با خیال راحت با استفاده از لینکهای منبع بالا آنها را در مخزن خود کپی کنید.
اسکریپتهای بنچمارک میتوانند به تخمین کارایی ابزارها کمک کنند. اما در نظر داشته باشید که کارایی روی سرورهای production Exercism اغلب پایینتر است.
امتحان کنید با ایمیجهای پایهی مختلف (مثلاً Alpine بهجای Ubuntu) کار کنید تا ببینید آیا یکی (بهطور قابلتوجهی) از دیگری بهتر عمل میکند. اگر کارایی تقریباً برابر بود، ایمیجی را انتخاب کنید که کوچکترین است.
بررسی کنید که استفاده از شبکهی internal بهجای none کارایی را بهتر میکند یا نه.
برای اطلاعات بیشتر مستندات شبکه را ببینید.
ابزارهای track یک کانتینر 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 انجام میشود. این بستهها باید در زمان build نصب شوند (چون در زمان run اتصال اینترنت در دسترس نیست). بنابراین، هر فایل کش/نگهداری سوابق package manager باید پس از نصب بستههای اضافی حذف شود.
توزیعهایی که از package manager apk استفاده میکنند (مانند Alpine) باید هنگام استفاده از apk add برای نصب بستهها، پرچم --no-cache را به کار ببرند:
RUN apk add --no-cache curl
توزیعهایی که از 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 شما به این شکل است:
با این چیدمان، بستههای اضافی فقط در مرحلهی «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" ]
Dockerfile مربوط به test runner C# کار مشابهی انجام میدهد، فقط در این مورد مرحلهی build میتواند از یک ایمیج Docker موجود استفاده کند که بستههای اضافی لازم برای نصب کتابخانهها را از قبل نصب کرده است.
تستهای واحد میتوانند بسیار مفید باشند، اما ما تمرکز روی نوشتن تستهای یکپارچگی را توصیه میکنیم. مزیت اصلی آنها این است که بهتر تست میکنند ابزارها در production چگونه اجرا میشوند، و بنابراین به افزایش اطمینان در پیادهسازی ابزارهایتان کمک میکنند.
برای بهترین شبیهسازی محیط production، تستهای یکپارچگی باید ابزارها را مثل محیط production اجرا کنند. این یعنی ساختن ایمیج Docker و سپس اجرای ایمیج ساختهشده روی یک راهحل برای بررسی خروجی آن.
تستهای یکپارچگی باید بهصورت تستهای golden تعریف شوند، که تستهایی هستند که خروجی مورد انتظار در یک فایل ذخیره میشود. این برای تستهای یکپارچگی ابزارهای track عالی است، چون خروجی ابزارها هم فایل است.
وقتی 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 اجرا کنید.
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
محیط production ما در حال حاضر فایلسیستم فقطخواندنی را اجبار نمیکند، اما ممکن است در آینده این کار را بکنیم. به همین دلیل، قالب پایه برای یک test runner/analyzer/representer جدید با یک فایلسیستم فقطخواندنی شروع میشود. اگر نمیتوانید کارها را روی یک فایل فقطخواندنی راه بیندازید، با خیال راحت (برای حالا) یک فایلسیستم قابل نوشتن را فرض کنید.