
عندما يتعلق الأمر بإنشاء حاويات Docker ، فمن الأفضل السعي دائمًا لتقليل حجم الصور. الصور التي تستخدم نفس الطبقات وتزن أقل - يتم نقلها ونشرها بسرعة أكبر.
ولكن كيف تتحكم في الحجم عندما يقوم كل تنفيذ لبيان RUN
بإنشاء طبقة جديدة؟ بالإضافة إلى ذلك ، لا تزال بحاجة إلى قطع أثرية وسيطة قبل إنشاء الصورة نفسها ...
ربما تعلم أن معظم ملفات Docker لها ميزات غريبة إلى حد ما ، على سبيل المثال:
FROM ubuntu RUN apt-get update && apt-get install vim
حسنًا ، لماذا &&
هنا؟ ليس من السهل تشغيل بيانين RUN
، مثل هنا؟
FROM ubuntu RUN apt-get update RUN apt-get install vim
بدءًا من الإصدار Docker 1.10 ، يضيف مشغلو COPY
و ADD
و RUN
طبقة جديدة إلى الصورة. في المثال السابق ، تم إنشاء طبقتين بدلاً من طبقة واحدة.

طبقات كما يرتكب بوابة.
تحافظ طبقات عامل الميناء على الاختلافات بين الإصدار السابق والحالي للصورة. ومثل أوامر git ، تكون سهلة الاستخدام إذا قمت بمشاركتها مع مستودعات أو صور أخرى. في الواقع ، عند طلب صورة من السجل ، يتم تحميل الطبقات المفقودة فقط ، مما يبسط فصل الصور بين الحاويات.
ولكن في نفس الوقت ، تحدث كل طبقة ، وكلما زاد حجمها ، زادت الصورة النهائية أثقل. تتشابه مستودعات Git في هذا الصدد: ينمو حجم المستودع بعدد الطبقات ، لأنه يجب أن يخزن كل التغييرات بين عمليات الإقلاع. اعتادت أن تكون ممارسة جيدة لدمج عبارات RUN
متعددة على نفس السطر ، كما في المثال الأول. لكن الآن ، للأسف ، لا.
1. ادمج عدة طبقات في طبقة واحدة باستخدام التجميع المرحلي لصور Docker
عندما ينمو مستودع Git ، يمكنك ببساطة تلخيص سجل التغيير بأكمله في التزام واحد ونسيانه. اتضح أن شيئا مماثلا يمكن تنفيذه في Docker - من خلال التجمع على مراحل.
لنقم بإنشاء حاوية Node.js.
لنبدأ مع index.js
:
const express = require('express') const app = express() app.get('/', (req, res) => res.send('Hello World!')) app.listen(3000, () => { console.log(`Example app listening on port 3000!`) })
و package.json
:
{ "name": "hello-world", "version": "1.0.0", "main": "index.js", "dependencies": { "express": "^4.16.2" }, "scripts": { "start": "node index.js" } }
حزمة التطبيق مع Dockerfile
التالية:
FROM node:8 EXPOSE 3000 WORKDIR /app COPY package.json index.js ./ RUN npm install CMD ["npm", "start"]
قم بإنشاء صورة:
$ docker build -t node-vanilla .
تأكد من أن كل شيء يعمل:
$ docker run -p 3000:3000 -ti --rm --init node-vanilla
يمكنك الآن اتباع الرابط: http: // localhost: 3000 وراجع "Hello World!" هناك.
في Dockerfile
الآن مشغلي COPY
و RUN
، لذلك نقوم بإصلاح الزيادة بطبقتين على الأقل ، مقارنة بالصورة الأصلية:
$ docker history node-vanilla IMAGE CREATED BY SIZE 075d229d3f48 /bin/sh -c #(nop) CMD ["npm" "start"] 0B bc8c3cc813ae /bin/sh -c npm install 2.91MB bac31afb6f42 /bin/sh -c #(nop) COPY multi:3071ddd474429e1… 364B 500a9fbef90e /bin/sh -c #(nop) WORKDIR /app 0B 78b28027dfbf /bin/sh -c #(nop) EXPOSE 3000 0B b87c2ad8344d /bin/sh -c #(nop) CMD ["node"] 0B <missing> /bin/sh -c set -ex && for key in 6A010… 4.17MB <missing> /bin/sh -c #(nop) ENV YARN_VERSION=1.3.2 0B <missing> /bin/sh -c ARCH= && dpkgArch="$(dpkg --print… 56.9MB <missing> /bin/sh -c #(nop) ENV NODE_VERSION=8.9.4 0B <missing> /bin/sh -c set -ex && for key in 94AE3… 129kB <missing> /bin/sh -c groupadd --gid 1000 node && use… 335kB <missing> /bin/sh -c set -ex; apt-get update; apt-ge… 324MB <missing> /bin/sh -c apt-get update && apt-get install… 123MB <missing> /bin/sh -c set -ex; if ! command -v gpg > /… 0B <missing> /bin/sh -c apt-get update && apt-get install… 44.6MB <missing> /bin/sh -c #(nop) CMD ["bash"] 0B <missing> /bin/sh -c #(nop) ADD file:1dd78a123212328bd… 123MB
كما ترون ، زادت الصورة النهائية بخمس طبقات جديدة: واحدة لكل عامل في Dockerfile
لدينا. لنجرب الآن بناء Docker التدريجي. نستخدم نفس Dockerfile
، يتكون من جزأين:
FROM node:8 as build WORKDIR /app COPY package.json index.js ./ RUN npm install FROM node:8 COPY --from=build /app / EXPOSE 3000 CMD ["index.js"]
الجزء الأول من Dockerfile
يخلق ثلاث طبقات. ثم يتم دمج الطبقات ونسخها إلى المرحلتين الثانية والأخيرة. تتم إضافة طبقتين أخريين إلى الصورة أعلاه. نتيجة لذلك ، لدينا ثلاث طبقات.

لنجربها. أولاً ، قم بإنشاء حاوية:
$ docker build -t node-multi-stage .
التحقق من التاريخ:
$ docker history node-multi-stage IMAGE CREATED BY SIZE 331b81a245b1 /bin/sh -c #(nop) CMD ["index.js"] 0B bdfc932314af /bin/sh -c #(nop) EXPOSE 3000 0B f8992f6c62a6 /bin/sh -c #(nop) COPY dir:e2b57dff89be62f77… 1.62MB b87c2ad8344d /bin/sh -c #(nop) CMD ["node"] 0B <missing> /bin/sh -c set -ex && for key in 6A010… 4.17MB <missing> /bin/sh -c #(nop) ENV YARN_VERSION=1.3.2 0B <missing> /bin/sh -c ARCH= && dpkgArch="$(dpkg --print… 56.9MB <missing> /bin/sh -c #(nop) ENV NODE_VERSION=8.9.4 0B <missing> /bin/sh -c set -ex && for key in 94AE3… 129kB <missing> /bin/sh -c groupadd --gid 1000 node && use… 335kB <missing> /bin/sh -c set -ex; apt-get update; apt-ge… 324MB <missing> /bin/sh -c apt-get update && apt-get install… 123MB <missing> /bin/sh -c set -ex; if ! command -v gpg > /… 0B <missing> /bin/sh -c apt-get update && apt-get install… 44.6MB <missing> /bin/sh -c #(nop) CMD ["bash"] 0B <missing> /bin/sh -c #(nop) ADD file:1dd78a123212328bd… 123MB
معرفة ما إذا كان حجم الملف قد تغير:
$ docker images | grep node- node-multi-stage 331b81a245b1 678MB node-vanilla 075d229d3f48 679MB
نعم ، لقد أصبح أصغر ، ولكن ليس بشكل كبير حتى الآن.
2. نزيل كل ما لا لزوم له من الحاوية باستخدام distroless
توفر لنا الصورة الحالية Node.js yarn
و npm
و bash
والعديد من الثنائيات المفيدة الأخرى. أيضا ، لأنه يقوم على أوبونتو. وبالتالي ، عند نشره ، نحصل على نظام تشغيل متكامل مع العديد من الثنائيات والمرافق المفيدة.
ومع ذلك ، نحن لسنا بحاجة لهم لتشغيل الحاوية. التبعية الوحيدة المطلوبة هي Node.js.
يجب أن تدعم حاويات الرصيف تشغيل عملية واحدة وتحتوي على الحد الأدنى الضروري من الأدوات لتشغيلها. نظام التشغيل بأكمله غير مطلوب لهذا الغرض.
حتى نتمكن من الحصول على كل شيء منه باستثناء Node.js.
لكن كيف؟
لقد توصلت Google بالفعل إلى حل مماثل - GoogleCloudPlatform / distroless .
يقرأ وصف المستودع:
تحتوي الصور غير الموزعة فقط على التطبيق وتبعياته. لا يوجد مديرو حزم أو أصداف أو برامج أخرى توجد عادةً في توزيع Linux القياسي.
هذا هو ما تحتاجه!
قم بتشغيل Dockerfile
للحصول على صورة جديدة:
FROM node:8 as build WORKDIR /app COPY package.json index.js ./ RUN npm install FROM gcr.io/distroless/nodejs COPY --from=build /app / EXPOSE 3000 CMD ["index.js"]
نجمع الصورة كالمعتاد:
$ docker build -t node-distroless .
يجب أن يعمل التطبيق بشكل جيد. للتحقق ، قم بتشغيل الحاوية:
$ docker run -p 3000:3000 -ti --rm --init node-distroless
وانتقل إلى http: // localhost: 3000 . هل أصبحت الصورة أسهل بدون ثنائيات إضافية؟
$ docker images | grep node-distroless node-distroless 7b4db3b7f1e5 76.7MB
تماما مثل ذلك! الآن يزن 76.7 ميغابايت فقط ، بقدر أقل من 600 ميغابايت!
كل شيء رائع ، ولكن هناك نقطة واحدة مهمة. عند تشغيل الحاوية وتحتاج إلى التحقق من ذلك ، يمكنك الاتصال باستخدام:
$ docker exec -ti <insert_docker_id> bash
الاتصال بحاوية قيد التشغيل وبدء تشغيل bash
يشبه إلى حد بعيد إنشاء جلسة SSH.
ولكن نظرًا لأن distroless هي نسخة مجردة من نظام التشغيل الأصلي ، فلا توجد ثنائيات إضافية ، أو في الواقع ، shell!
كيفية الاتصال بحاوية التشغيل إذا لم يكن هناك قذيفة؟
الشيء الأكثر إثارة للاهتمام هو أن لا شيء.
هذا ليس جيدًا جدًا ، حيث يمكن تنفيذ الثنائيات فقط في حاوية. والوحيد الذي يمكن إطلاقه هو Node.js:
$ docker exec -ti <insert_docker_id> node
في الواقع ، هناك علامة زائد في هذا ، لأنه إذا تمكن بعض المهاجمين من الوصول إلى الحاوية ، فستتسبب في ضرر أقل بكثير مما لو كان بإمكانه الوصول إلى القشرة. وبعبارة أخرى ، عدد أقل من الثنائيات - وزن أقل وأمن أعلى. ولكن ، على حساب تصحيح الأخطاء أكثر تعقيدا.
هنا تجدر الإشارة إلى أنه لا يستحق ربط وتصحيح الحاويات على البيئة همز. من الأفضل الاعتماد على أنظمة التسجيل والمراقبة التي تم تكوينها بشكل صحيح.
ولكن ماذا لو كنا لا نزال بحاجة إلى تصحيح الأخطاء ، ومع ذلك نريد أن تكون صورة عامل الإرساء أصغر؟
3. تقليل الصور الأساسية مع جبال الألب
يمكنك استبدال distroless مع صورة جبال الألب.
Alpine Linux هو توزيع خفيف الوزن موجه للأمان يعتمد على musl libc و busybox . لكننا لن نأخذ كلمة ، بل نراجعها.
Dockerfile
باستخدام node:8-alpine
Dockerfile
:
FROM node:8 as build WORKDIR /app COPY package.json index.js ./ RUN npm install FROM node:8-alpine COPY --from=build /app / EXPOSE 3000 CMD ["npm", "start"]
قم بإنشاء صورة:
$ docker build -t node-alpine .
تحقق الحجم:
$ docker images | grep node-alpine node-alpine aa1f85f8e724 69.7MB
في الإخراج ، لدينا 69.7 ميغابايت - وهذا أقل من صورة غير منتظمة.
دعونا نتحقق مما إذا كان من الممكن الاتصال بحاوية عاملة (في حالة صورة المنحل ، لم نتمكن من القيام بذلك).
إطلاق الحاوية:
$ docker run -p 3000:3000 -ti --rm --init node-alpine Example app listening on port 3000!
والاتصال:
$ docker exec -ti 9d8e97e307d7 bash OCI runtime exec failed: exec failed: container_linux.go:296: starting container process caused "exec: \"bash\": executable file not found in $PATH": unknown
دون جدوى. ولكن ربما تحتوي الحاوية على ...
$ docker exec -ti 9d8e97e307d7 sh / #
عظيم! لقد نجحنا في الاتصال بالحاوية ، وفي نفس الوقت تكون صورتها أصغر أيضًا. ولكن هنا كانت هناك بعض الفروق الدقيقة.
تعتمد صور جبال الألب على muslc ، وهي مكتبة قياسية بديلة لـ C. بينما تعتمد معظم توزيعات Linux ، مثل Ubuntu و Debian و CentOS على glibc. ويعتقد أن كل من هاتين المكتبات توفر واجهة واحدة للعمل مع النواة.
ومع ذلك ، لديهم أهداف مختلفة: glibc هو الأكثر شيوعًا وسريعًا ، بينما يشغل muslc مساحة أقل ويتم كتابته بانحياز أمان. عندما يجمع تطبيق ما ، كقاعدة عامة ، يتم تجميعه إلى مكتبة C. معينة ، وإذا كنت بحاجة إلى استخدامه مع مكتبة أخرى ، فسيتعين عليك إعادة الترجمة.
بمعنى آخر ، قد يؤدي إنشاء حاويات على صور جبال الألب إلى أحداث غير متوقعة ، لأن مكتبة C القياسية المستخدمة فيها مختلفة. سيكون الاختلاف ملحوظًا عند العمل مع الثنائيات المترجمة مسبقًا ، مثل امتدادات Node.js لـ C ++.
على سبيل المثال ، لا تعمل حزمة PhantomJS الجاهزة على جبال الألب.
فما هي الصورة الأساسية للاختيار؟
نظرة على جبال الألب أو العفة أو الفانيليا - بالطبع ، من الأفضل أن تقرر وفقًا للموقف.
إذا كنت تتعامل مع همز وكان الأمن مهمًا ، فربما يكون النقل غير المناسب هو الأفضل.
يضيف كل ثنائي تمت إضافته إلى صورة Docker مخاطرة معينة لاستقرار التطبيق بأكمله. يمكن تقليل هذا الخطر من خلال تثبيت ثنائي واحد فقط في الحاوية.
على سبيل المثال ، إذا تمكن المهاجم من العثور على ثغرة أمنية في أحد التطبيقات التي تعمل على أساس صورة غير منتظمة ، فإنه لا يمكنه تشغيل الصدفة في الحاوية لأنه لم يكن هناك!
إذا كان حجم صورة عامل الإرساء مهمًا للغاية بالنسبة لك لسبب ما ، فمن المؤكد أنه يستحق إلقاء نظرة فاحصة على الصور المستندة إلى جبال الألب.
إنها صغيرة جدًا ، ولكن على حساب التوافق. يستخدم Alpine مكتبة C مختلفة مختلفة قليلاً ، muslc ، لذلك في بعض الأحيان سوف تظهر المشاكل. يمكن العثور على الأمثلة على الرابطين: https://github.com/grpc/grpc/issues/8528 و https://github.com/grpc/grpc/issues/6126 .
تعتبر صور الفانيليا مثالية للاختبار والتطوير.
نعم ، إنها كبيرة ، لكنها تبدو أكبر قدر ممكن على جهاز كامل مع تثبيت Ubuntu. بالإضافة إلى ذلك ، تتوفر جميع الثنائيات في نظام التشغيل.
تلخيص حجم صور Docker المستلمة:
node:8
681 ميجابايت
node:8
مع 678MB بناء تزايدي
gcr.io/distroless/nodejs
76.7 ميجابايت
node:8-alpine
69.7 ميغابايت
فراق الكلمات من المترجم
اقرأ مقالات أخرى على مدونتنا:
النسخ الاحتياطية الدولة في Kubernetes
النسخ الاحتياطي لعدد كبير من مشاريع الويب غير المتجانسة
الروبوت برقية ل Redmine. كيفية تبسيط الحياة لنفسك والناس