تضم أفضل ممارسات Dockerfile الرسمية الكثير من المحتوى الرائع حول كيفية تحسين ملفات Dockerfile لديك.
ينبغي أن تُحسّن الأداء أولًا وقبل كل شيء (وخصوصًا لمُشغّلات الاختبارات). سيضمن ذلك أن تعمل أدواتك بأسرع ما يمكن وألّا تتوقف بسبب انتهاء المهلة.
غالبًا ما يكون قياس زمن التنفيذ طريقة رائعة لتكوين انطباع عن أداء الأدوات. اجعل من عادتك قياس زمن التنفيذ بعد أي تغيير وقبله أيضًا. وحتى عندما تشعر "باليقين" بأن تغييرًا ما سيحسّن الأداء، ينبغي أن تقيس زمن التنفيذ رغم ذلك.
حيثما أمكن، أنشئ سكربتات لقياس الأداء تلقائيًا (ويُعرف ذلك أيضًا بـ_قياس الأداء المرجعي_). إحدى الأدوات المساعدة جدًا في سطر الأوامر هي hyperfine، لكن لا تتردد في استخدام ما يناسب أدواتك أكثر من غيره.
ستتمكن مستودعات أدوات المسارات الأحدث من الوصول إلى السكربتين التاليين:
./bin/benchmark.sh: قياس أداء كود أدوات المسار (الكود المصدري)./bin/benchmark-in-docker.sh: قياس أداء صورة Docker الخاصة بأدوات المسار (الكود المصدري)إذا كنت تعمل على مستودع أدوات لمسار لا يحتوي على هذين الملفين، فلا تتردد في نسخهما إلى مستودعك باستخدام روابط المصدر أعلاه.
يمكن لسكربتات قياس الأداء أن تساعد في تقدير أداء الأدوات. لكن ضع في اعتبارك أن الأداء على خوادم Exercism الفعلية غالبًا ما يكون أبطأ.
جرّب استخدام صور أساسية مختلفة (مثل Alpine بدلًا من Ubuntu)، لترى ما إذا كانت إحداها تتفوق (بشكل ملحوظ) على الأخرى. وإذا كان الأداء متقاربًا نسبيًا، فاختر الصورة الأصغر.
تحقق مما إذا كان استخدام شبكة internal بدلًا من none يحسّن الأداء.
راجع توثيق الشبكة لمزيد من المعلومات.
تُشغّل أدوات المسار حاوية 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 إلى تثبيت حزم إضافية، ويُفعل ذلك عادةً عبر مدير حزم. يجب تثبيت هذه الحزم في وقت البناء (إذ لا يتوفر اتصال بالإنترنت في وقت التشغيل). لذلك ينبغي حذف أي ملفات تخزين مؤقت أو محاسبة خاصة بمدير الحزم بعد تثبيت الحزم الإضافية.
التوزيعات التي تستخدم مدير الحزم apk (مثل Alpine) ينبغي أن تستخدم العلامة --no-cache عند استخدام apk add لتثبيت الحزم:
RUN apk add --no-cache curl
التوزيعات التي تستخدم مدير الحزم 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 كما يلي:
مع هذا الإعداد، تُثبَّت الحزم الإضافية فقط في مرحلة "البناء" و_ليس_ في مرحلة "وقت التشغيل"، ما يعني أنها لن تظهر في صورة 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" ]
يفعل ملف Dockerfile الخاص بمُشغّل اختبارات C# شيئًا مشابهًا، غير أنه في هذه الحالة يمكن لمرحلة البناء استخدام صورة 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
لا تفرض بيئة الإنتاج لدينا حاليًا نظام ملفات للقراءة فقط، لكننا قد نفعل ذلك في المستقبل. ولهذا السبب، يبدأ القالب الأساسي لمُشغّل اختبارات/محلّل/ممثّل جديد بنظام ملفات للقراءة فقط. وإن لم تتمكن من تشغيل الأمور على نظام ملفات للقراءة فقط، فلا بأس أن تفترض (في الوقت الحالي) نظام ملفات قابلًا للكتابة.