أفضل الممارسات


اتبع أفضل الممارسات الرسمية

تضم أفضل ممارسات Dockerfile الرسمية الكثير من المحتوى الرائع حول كيفية تحسين ملفات Dockerfile لديك.

الأداء

ينبغي أن تُحسّن الأداء أولًا وقبل كل شيء (وخصوصًا لمُشغّلات الاختبارات). سيضمن ذلك أن تعمل أدواتك بأسرع ما يمكن وألّا تتوقف بسبب انتهاء المهلة.

القياس

غالبًا ما يكون قياس زمن التنفيذ طريقة رائعة لتكوين انطباع عن أداء الأدوات. اجعل من عادتك قياس زمن التنفيذ بعد أي تغيير وقبله أيضًا. وحتى عندما تشعر "باليقين" بأن تغييرًا ما سيحسّن الأداء، ينبغي أن تقيس زمن التنفيذ رغم ذلك.

السكربتات

حيثما أمكن، أنشئ سكربتات لقياس الأداء تلقائيًا (ويُعرف ذلك أيضًا بـ_قياس الأداء المرجعي_). إحدى الأدوات المساعدة جدًا في سطر الأوامر هي hyperfine، لكن لا تتردد في استخدام ما يناسب أدواتك أكثر من غيره.

ستتمكن مستودعات أدوات المسارات الأحدث من الوصول إلى السكربتين التاليين:

  1. ./bin/benchmark.sh: قياس أداء كود أدوات المسار (الكود المصدري)
  2. ./bin/benchmark-in-docker.sh: قياس أداء صورة Docker الخاصة بأدوات المسار (الكود المصدري)
Note

إذا كنت تعمل على مستودع أدوات لمسار لا يحتوي على هذين الملفين، فلا تتردد في نسخهما إلى مستودعك باستخدام روابط المصدر أعلاه.

Caution

يمكن لسكربتات قياس الأداء أن تساعد في تقدير أداء الأدوات. لكن ضع في اعتبارك أن الأداء على خوادم Exercism الفعلية غالبًا ما يكون أبطأ.

جرّب صورًا أساسية مختلفة

جرّب استخدام صور أساسية مختلفة (مثل Alpine بدلًا من Ubuntu)، لترى ما إذا كانت إحداها تتفوق (بشكل ملحوظ) على الأخرى. وإذا كان الأداء متقاربًا نسبيًا، فاختر الصورة الأصغر.

جرّب الشبكة الداخلية

تحقق مما إذا كان استخدام شبكة internal بدلًا من none يحسّن الأداء. راجع توثيق الشبكة لمزيد من المعلومات.

فضّل أوامر وقت البناء على أوامر وقت التشغيل

تُشغّل أدوات المسار حاوية Docker لمرة واحدة وقصيرة العمر تنفّذ الخطوات التالية.

  1. تُنشأ حاوية Docker.
  2. تُشغّل حاوية Docker بالوسائط الصحيحة.
  3. تُدمَّر حاوية Docker.

لذلك، يعمل الكود الذي يُنفَّذ في الخطوة 2 في كل عملية تشغيل للأدوات. ولهذا السبب فإن تقليل مقدار الكود الذي يعمل في الخطوة 2 طريقة رائعة لتحسين الأداء. ومن طرق ذلك نقل الكود من وقت التشغيل إلى وقت البناء. فبينما يعمل كود وقت التشغيل في كل عملية تشغيل للأدوات، لا يعمل كود وقت البناء إلا مرة واحدة (عند بناء صورة Docker).

يعمل كود وقت البناء مرة واحدة كجزء من سير عمل GitHub Actions. لذا لا بأس إن كان الكود الذي يعمل في وقت البناء بطيئًا (نسبيًا).

مثال: ترجمة المكتبات مسبقًا

عند تشغيل الاختبارات في مُشغّل اختبارات Haskell، يحتاج إلى ترجمة بعض المكتبات الأساسية. ولأن كل تشغيل للاختبارات يحدث في حاوية جديدة، فإن هذا يعني أن الترجمة كانت تتم في كل تشغيل للاختبارات! وللتحايل على ذلك، يحتوي ملف Dockerfile الخاص بمُشغّل اختبارات Haskell على الأمرين التاليين:

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

أولًا، يُنسخ مجلد pre-compiled إلى الصورة. هذا المجلد مُهيّأ كتمرين اختباري ويعتمد على المكتبات الأساسية نفسها التي يعتمد عليها التمرين الفعلي. ثم نشغّل الاختبارات على ذلك المجلد، وهو ما يشبه طريقة تشغيل الاختبارات لتمرين فعلي. سيؤدي تشغيل الاختبارات إلى ترجمة الأساس، لكن الفرق أن هذا يحدث في وقت البناء. وهكذا تحتوي صورة Docker الناتجة على مكتباتها الأساسية مترجمة مسبقًا. وهذا يعني أن الترجمة لم تعد مطلوبة في وقت التشغيل، ما ينتج عنه تنفيذ أسرع (كثيرًا).

مثال: ترجمة الملفات التنفيذية مسبقًا

تسمح بعض اللغات بترجمة الكود مسبقًا أو في اللحظة المناسبة. وهذه مقايضة بين وقت البناء ووقت التشغيل، وكما في السابق، نفضّل التنفيذ في وقت البناء لأسباب تتعلق بالأداء.

يستخدم ملف Dockerfile الخاص بمُشغّل اختبارات C# هذا النهج، حيث يُترجم مُشغّل الاختبارات إلى ملف تنفيذي مسبقًا (في وقت البناء) بدلًا من ترجمة الكود في اللحظة المناسبة (في وقت التشغيل). وهذا يعني أن هناك عملًا أقل يُنجز في وقت التشغيل، ما ينبغي أن يساعد في زيادة الأداء.

الحجم

ينبغي أن تحاول تقليل حجم الصورة، وهذا يعني أنها سوف:

  • يكون نشرها أسرع
  • تقلل التكاليف علينا
  • تحسّن زمن بدء تشغيل كل حاوية

جرّب توزيعات مختلفة

تختلف أحجام صور التوزيعات المختلفة. على سبيل المثال، صورة 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
  • التوثيق

احذف ملفات مدير الحزم

تحتاج معظم صور Docker إلى تثبيت حزم إضافية، ويُفعل ذلك عادةً عبر مدير حزم. يجب تثبيت هذه الحزم في وقت البناء (إذ لا يتوفر اتصال بالإنترنت في وقت التشغيل). لذلك ينبغي حذف أي ملفات تخزين مؤقت أو محاسبة خاصة بمدير الحزم بعد تثبيت الحزم الإضافية.

apk

التوزيعات التي تستخدم مدير الحزم apk (مثل Alpine) ينبغي أن تستخدم العلامة --no-cache عند استخدام apk add لتثبيت الحزم:

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

التوزيعات التي تستخدم مدير الحزم 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 تثبيت حزم تكون مطلوبة فقط في وقت البناء. في هذه الحالة يبدو الهيكل العام لملف Dockerfile كما يلي:

  1. عرّف مرحلة جديدة (سنسميها مرحلة "البناء"). ستُستخدم هذه المرحلة فقط في وقت البناء.
  2. ثبّت الحزم الإضافية المطلوبة (في مرحلة "البناء").
  3. شغّل الأوامر التي تحتاج إلى الحزم الإضافية (داخل مرحلة "البناء").
  4. عرّف مرحلة جديدة (سنسميها مرحلة "وقت التشغيل"). ستشكّل هذه المرحلة صورة Docker الناتجة وستُنفَّذ في وقت التشغيل.
  5. انسخ النتيجة (أو النتائج) من الأوامر التي شُغّلت في الخطوة 3 (في مرحلة "البناء") إلى هذه المرحلة (مرحلة "وقت التشغيل").

مع هذا الإعداد، تُثبَّت الحزم الإضافية فقط في مرحلة "البناء" و_ليس_ في مرحلة "وقت التشغيل"، ما يعني أنها لن تظهر في صورة Docker الناتجة.

مثال: تنزيل الملفات

يحتاج مُشغّل اختبارات Fortran إلى curl لتنزيل بعض الملفات. غير أن صورة وقت تشغيله لا تحتاج إلى curl، ما يجعل هذه حالة مثالية لاستخدام البناء متعدد المراحل.

أولًا، يعرّف ملف Dockerfile الخاص به مرحلة (باسم "build") تُثبَّت فيها حزمة 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 مرحلة جديدة وينسخ الملفات المنزَّلة من مرحلة "البناء" إلى مرحلته الخاصة باستخدام الأمر 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"]
مثال: تثبيت المكتبات

يحتاج مُشغّل اختبارات Ruby إلى تثبيت حزم git وopenssh وbuild-base وgcc وwget قبل أن يمكن تثبيت مكتباته المطلوبة (gems). يبدأ ملف 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 لنسخ المكتبات المثبَّتة من مرحلة البناء إلى مرحلتها الخاصة:

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 الخاص بمُشغّل اختبارات C# شيئًا مشابهًا، غير أنه في هذه الحالة يمكن لمرحلة البناء استخدام صورة Docker موجودة ثبّتت مسبقًا الحزم الإضافية اللازمة لتثبيت المكتبات.

الاختبار

استخدم اختبارات التكامل

قد تكون اختبارات الوحدة مفيدة جدًا، لكننا نوصي بالتركيز على كتابة اختبارات التكامل. ميزتها الأساسية أنها تختبر بشكل أفضل كيف تعمل الأدوات في بيئة الإنتاج، وبذلك تساعد في زيادة الثقة في تنفيذ أدواتك.

استخدم Docker

لمحاكاة بيئة الإنتاج بأفضل شكل، ينبغي أن تشغّل اختبارات التكامل الأدوات كما في بيئة الإنتاج. وهذا يعني بناء صورة Docker ثم تشغيل الصورة المبنية على حل للتحقق من مخرجاتها.

استخدم اختبارات النتائج المرجعية

ينبغي تعريف اختبارات التكامل كـاختبارات النتائج المرجعية (golden tests)، وهي اختبارات تُخزَّن فيها المخرجات المتوقعة في ملف. وهذا مثالي لاختبارات تكامل أدوات المسار، لأن مخرجات الأدوات هي أيضًا ملفات.

مثال: مُشغّل الاختبارات

عند تشغيل مُشغّل الاختبارات على حل، تكون مخرجاته ملف results.json. يمكننا حينئذ مقارنة هذا الملف بملف مخرجات "معروف جيدًا" (أي "متوقع") (باسم expected_results.json) للتحقق من أن مُشغّل الاختبارات يعمل كما هو مقصود.

السلامة

السلامة سبب رئيسي لاستخدامنا حاويات Docker لتشغيل أدواتنا.

فضّل الصور الرسمية

هناك الكثير من صور Docker على Docker Hub، لكن حاول استخدام الصور الرسمية. فهذه الصور منسَّقة، واحتمال كونها غير آمنة (أقل بكثير).

ثبّت الإصدارات

لضمان استقرار عمليات البناء (أي ألّا تتعطل فجأة)، ينبغي أن تثبّت صورك الأساسية دائمًا على علامات محددة. وهذا يعني أنه بدلًا من:

FROM alpine:latest

ينبغي أن تستخدم:

FROM alpine:3.20.2

مع الأخيرة، ستستخدم عمليات البناء دائمًا الإصدار نفسه.

شغّل بمستخدم غير ذي صلاحيات

افتراضيًا، ستعمل كثير من الصور بمستخدم يملك صلاحيات الجذر (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

ادعم نظام ملفات للقراءة فقط

نشجّع على كتابة ملفات Docker باستخدام نظام ملفات للقراءة فقط. المجلدات الوحيدة التي ينبغي أن تفترض أنها قابلة للكتابة هي:

  • مجلد الحل (يُمرَّر كالوسيط الثاني)
  • مجلد المخرجات (يُمرَّر كالوسيط الثالث)
  • مجلد /tmp
Caution

لا تفرض بيئة الإنتاج لدينا حاليًا نظام ملفات للقراءة فقط، لكننا قد نفعل ذلك في المستقبل. ولهذا السبب، يبدأ القالب الأساسي لمُشغّل اختبارات/محلّل/ممثّل جديد بنظام ملفات للقراءة فقط. وإن لم تتمكن من تشغيل الأمور على نظام ملفات للقراءة فقط، فلا بأس أن تفترض (في الوقت الحالي) نظام ملفات قابلًا للكتابة.