قبل أن تدخل الميزة مرحلة الإنتاج، في هذه الأيام التي تتسم بوجود منسقين معقدين وCI/CD، لا يزال هناك طريق طويل لنقطعه من الالتزام بالاختبارات والتسليم. في السابق، كان بإمكانك تحميل ملفات جديدة عبر FTP (لم يعد أحد يفعل ذلك، أليس كذلك؟)، وكانت عملية "النشر" تستغرق ثوانٍ. أنت الآن بحاجة إلى إنشاء طلب دمج والانتظار لفترة طويلة حتى تصل الميزة إلى المستخدمين.
جزء من هذا المسار هو بناء صورة Docker. في بعض الأحيان يستمر التجميع دقائق، وأحيانا عشرات الدقائق، والتي بالكاد يمكن أن تسمى طبيعية. في هذه المقالة، سنتناول تطبيقًا بسيطًا سنجمعه في صورة، ونطبق عدة طرق لتسريع عملية البناء، وننظر إلى الفروق الدقيقة في كيفية عمل هذه الطرق.
لدينا خبرة جيدة في إنشاء ودعم المواقع الإعلامية:
نحن ننشر في GitLab. نحن نجمع الصور وندفعها إلى GitLab Registry ونطرحها في مرحلة الإنتاج. أطول شيء في هذه القائمة هو تجميع الصور. على سبيل المثال: بدون التحسين، استغرق كل إنشاء للواجهة الخلفية 14 دقيقة.
وفي النهاية، أصبح من الواضح أننا لم يعد بإمكاننا العيش بهذه الطريقة، وجلسنا لمعرفة سبب استغراق الصور وقتًا طويلاً لجمعها. ونتيجة لذلك، تمكنا من تقليل وقت التجميع إلى 30 ثانية!
بالنسبة لهذه المقالة، حتى لا تكون مرتبطة ببيئة التذكير، دعونا نلقي نظرة على مثال لتجميع تطبيق Angular فارغ. لذلك، دعونا ننشئ تطبيقنا:
ng n app
أضف PWA إليها (نحن تقدميون):
ng add @angular/pwa --project app
بينما يتم تنزيل مليون حزمة npm، فلنتعرف على كيفية عمل صورة عامل الإرساء. يوفر Docker القدرة على حزم التطبيقات وتشغيلها في بيئة معزولة تسمى الحاوية. بفضل العزلة، يمكنك تشغيل العديد من الحاويات في وقت واحد على خادم واحد. الحاويات أخف بكثير من الأجهزة الافتراضية لأنها تعمل مباشرة على نواة النظام. لتشغيل حاوية مع تطبيقنا، نحتاج أولاً إلى إنشاء صورة سنحزم فيها كل ما هو ضروري لتشغيل تطبيقنا. في الأساس، الصورة هي نسخة من نظام الملفات. على سبيل المثال، خذ ملف Dockerfile:
FROM node:12.16.2
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod
ملف Dockerfile عبارة عن مجموعة من التعليمات؛ من خلال القيام بكل واحدة منها، سيقوم Docker بحفظ التغييرات على نظام الملفات وتراكبها على التغييرات السابقة. يقوم كل فريق بإنشاء طبقته الخاصة. والصورة النهائية عبارة عن طبقات مدمجة معًا.
ما هو مهم معرفته: يمكن لكل طبقة Docker تخزينها مؤقتًا. إذا لم يتغير شيء منذ البناء الأخير، فبدلا من تنفيذ الأمر، سيأخذ عامل الإرساء طبقة جاهزة. نظرًا لأن الزيادة الرئيسية في سرعة البناء ستكون بسبب استخدام ذاكرة التخزين المؤقت، فعند قياس سرعة البناء، سنولي اهتمامًا خاصًا لبناء صورة باستخدام ذاكرة تخزين مؤقت جاهزة. لذلك، خطوة بخطوة:
- نقوم بحذف الصور محليًا حتى لا تؤثر عمليات التشغيل السابقة على الاختبار.
docker rmi $(docker images -q)
- نطلق البناء لأول مرة.
time docker build -t app .
- نقوم بتغيير ملف src/index.html - فنحن نقلد عمل المبرمج.
- نقوم بتشغيل البناء مرة ثانية.
time docker build -t app .
إذا تم تكوين بيئة إنشاء الصور بشكل صحيح (المزيد حول ذلك أدناه)، فعند بدء الإنشاء، سيكون لدى Docker بالفعل مجموعة من ذاكرات التخزين المؤقت على متنه. مهمتنا هي معرفة كيفية استخدام ذاكرة التخزين المؤقت حتى تتم عملية الإنشاء في أسرع وقت ممكن. وبما أننا نفترض أن تشغيل إصدار بدون ذاكرة تخزين مؤقت يحدث مرة واحدة فقط — المرة الأولى — فيمكننا بالتالي تجاهل مدى بطء تلك المرة الأولى. في الاختبارات، تكون الجولة الثانية من البناء أمرًا مهمًا بالنسبة لنا، عندما تكون المخابئ دافئة بالفعل ونصبح جاهزين لخبز الكعكة. ومع ذلك، ستؤثر بعض النصائح أيضًا على البناء الأول.
لنضع ملف Dockerfile الموصوف أعلاه في مجلد المشروع ونبدأ في الإنشاء. وقد تم تكثيف كافة القوائم لسهولة القراءة.
$ time docker build -t app .
Sending build context to Docker daemon 409MB
Step 1/5 : FROM node:12.16.2
Status: Downloaded newer image for node:12.16.2
Step 2/5 : WORKDIR /app
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
added 1357 packages in 22.47s
Step 5/5 : RUN npm run build --prod
Date: 2020-04-16T19:20:09.664Z - Hash: fffa0fddaa3425c55dd3 - Time: 37581ms
Successfully built c8c279335f46
Successfully tagged app:latest
real 5m4.541s
user 0m0.000s
sys 0m0.000s
نقوم بتغيير محتويات src/index.html وتشغيله مرة أخرى.
$ time docker build -t app .
Sending build context to Docker daemon 409MB
Step 1/5 : FROM node:12.16.2
Step 2/5 : WORKDIR /app
---> Using cache
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
added 1357 packages in 22.47s
Step 5/5 : RUN npm run build --prod
Date: 2020-04-16T19:26:26.587Z - Hash: fffa0fddaa3425c55dd3 - Time: 37902ms
Successfully built 79f335df92d3
Successfully tagged app:latest
real 3m33.262s
user 0m0.000s
sys 0m0.000s
لمعرفة ما إذا كانت لدينا الصورة، قم بتشغيل الأمر docker images
:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 79f335df92d3 About a minute ago 1.74GB
قبل الإنشاء، يأخذ عامل الإرساء جميع الملفات الموجودة في السياق الحالي ويرسلها إلى البرنامج الخفي الخاص به Sending build context to Docker daemon 409MB
. يتم تحديد سياق البناء باعتباره الوسيطة الأخيرة لأمر البناء. في حالتنا، هذا هو الدليل الحالي - "."، - ويقوم Docker بسحب كل ما لدينا في هذا المجلد. 409 ميجابايت هي مساحة كبيرة: فلنفكر في كيفية إصلاحها.
تقليل السياق
لتقليل السياق، هناك خياران. أو ضع جميع الملفات اللازمة للتجميع في مجلد منفصل وقم بتوجيه سياق عامل الإرساء إلى هذا المجلد. قد لا يكون هذا مناسبًا دائمًا، لذلك من الممكن تحديد الاستثناءات: ما لا ينبغي سحبه إلى السياق. للقيام بذلك، ضع ملف .dockerignore في المشروع وأشر إلى ما هو غير مطلوب للبناء:
.git
/node_modules
وقم بتشغيل الإنشاء مرة أخرى:
$ time docker build -t app .
Sending build context to Docker daemon 607.2kB
Step 1/5 : FROM node:12.16.2
Step 2/5 : WORKDIR /app
---> Using cache
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
added 1357 packages in 22.47s
Step 5/5 : RUN npm run build --prod
Date: 2020-04-16T19:33:54.338Z - Hash: fffa0fddaa3425c55dd3 - Time: 37313ms
Successfully built 4942f010792a
Successfully tagged app:latest
real 1m47.763s
user 0m0.000s
sys 0m0.000s
607.2 كيلو بايت أفضل بكثير من 409 ميجا بايت. قمنا أيضًا بتقليل حجم الصورة من 1.74 إلى 1.38 جيجابايت:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 4942f010792a 3 minutes ago 1.38GB
دعونا نحاول تقليل حجم الصورة بشكل أكبر.
نحن نستخدم جبال الألب
هناك طريقة أخرى لتوفير حجم الصورة وهي استخدام صورة رئيسية صغيرة. الصورة الأبوية هي الصورة التي يتم على أساسها إعداد صورتنا. يتم تحديد الطبقة السفلية بواسطة الأمر FROM
في ملف دوكر. في حالتنا، نحن نستخدم صورة مبنية على Ubuntu والتي تم تثبيت Nodejs عليها بالفعل. و يزن...
$ docker images -a | grep node
node 12.16.2 406aa3abbc6c 17 minutes ago 916MB
... غيغابايت تقريبًا. يمكنك تقليل مستوى الصوت بشكل كبير باستخدام صورة تعتمد على Alpine Linux. Alpine هو نظام Linux صغير جدًا. تزن صورة عامل الإرساء الخاصة بـnodejs المستندة إلى جبال الألب 88.5 ميجابايت فقط. لذلك دعونا نستبدل صورتنا الحية في المنازل:
FROM node:12.16.2-alpine3.11
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod
كان علينا تثبيت بعض الأشياء الضرورية لبناء التطبيق. نعم، لا يتم إنشاء Angular بدون Python ¯(°_o)/¯
لكن حجم الصورة انخفض إلى 150 ميجابايت:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest aa031edc315a 22 minutes ago 761MB
دعونا نذهب أبعد من ذلك.
تجميع متعدد المراحل
ليس كل ما في الصورة هو ما نحتاجه في الإنتاج.
$ docker run app ls -lah
total 576K
drwxr-xr-x 1 root root 4.0K Apr 16 19:54 .
drwxr-xr-x 1 root root 4.0K Apr 16 20:00 ..
-rwxr-xr-x 1 root root 19 Apr 17 2020 .dockerignore
-rwxr-xr-x 1 root root 246 Apr 17 2020 .editorconfig
-rwxr-xr-x 1 root root 631 Apr 17 2020 .gitignore
-rwxr-xr-x 1 root root 181 Apr 17 2020 Dockerfile
-rwxr-xr-x 1 root root 1020 Apr 17 2020 README.md
-rwxr-xr-x 1 root root 3.6K Apr 17 2020 angular.json
-rwxr-xr-x 1 root root 429 Apr 17 2020 browserslist
drwxr-xr-x 3 root root 4.0K Apr 16 19:54 dist
drwxr-xr-x 3 root root 4.0K Apr 17 2020 e2e
-rwxr-xr-x 1 root root 1015 Apr 17 2020 karma.conf.js
-rwxr-xr-x 1 root root 620 Apr 17 2020 ngsw-config.json
drwxr-xr-x 1 root root 4.0K Apr 16 19:54 node_modules
-rwxr-xr-x 1 root root 494.9K Apr 17 2020 package-lock.json
-rwxr-xr-x 1 root root 1.3K Apr 17 2020 package.json
drwxr-xr-x 5 root root 4.0K Apr 17 2020 src
-rwxr-xr-x 1 root root 210 Apr 17 2020 tsconfig.app.json
-rwxr-xr-x 1 root root 489 Apr 17 2020 tsconfig.json
-rwxr-xr-x 1 root root 270 Apr 17 2020 tsconfig.spec.json
-rwxr-xr-x 1 root root 1.9K Apr 17 2020 tslint.json
استخدام docker run app ls -lah
أطلقنا حاوية بناءً على صورتنا app
ونفذ الأمر فيه ls -lah
وبعد ذلك أكملت الحاوية عملها.
في الإنتاج نحتاج فقط إلى مجلد dist
. في هذه الحالة، الملفات تحتاج بطريقة أو بأخرى إلى أن تعطى للخارج. يمكنك تشغيل بعض خوادم HTTP على العقدة. لكننا سنجعل الأمر أسهل. خمن كلمة روسية مكونة من أربعة أحرف "y". يمين! ينجينيكي. لنلتقط صورة باستخدام nginx، ونضع مجلدًا فيها dist
وتكوين صغير:
server {
listen 80 default_server;
server_name localhost;
charset utf-8;
root /app/dist;
location / {
try_files $uri $uri/ /index.html;
}
}
سيساعدنا البناء متعدد المراحل على القيام بكل هذا. دعونا نغير ملف Dockerfile الخاص بنا:
FROM node:12.16.2-alpine3.11 as builder
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod
FROM nginx:1.17.10-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/static.conf /etc/nginx/conf.d
COPY --from=builder /app/dist/app .
الآن لدينا تعليماتين FROM
في ملف Dockerfile، يقوم كل واحد منهم بتنفيذ خطوة بناء مختلفة. اتصلنا بالأول builder
، ولكن بدءًا من آخر FROM، سيتم إعداد صورتنا النهائية. الخطوة الأخيرة هي نسخ قطعة أثرية من مجموعتنا في الخطوة السابقة إلى الصورة النهائية باستخدام nginx. انخفض حجم الصورة بشكل ملحوظ:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 2c6c5da07802 29 minutes ago 36MB
لنقم بتشغيل الحاوية التي تحتوي على صورتنا ونتأكد من أن كل شيء يعمل:
docker run -p8080:80 app
باستخدام الخيار -p8080:80، قمنا بإعادة توجيه المنفذ 8080 على الجهاز المضيف لدينا إلى المنفذ 80 داخل الحاوية حيث يعمل nginx. فتح في المتصفح
يؤدي تقليل حجم الصورة من 1.74 جيجابايت إلى 36 ميجابايت إلى تقليل الوقت المستغرق لتسليم تطبيقك إلى مرحلة الإنتاج بشكل كبير. لكن دعنا نعود إلى وقت التجميع.
$ time docker build -t app .
Sending build context to Docker daemon 608.8kB
Step 1/11 : FROM node:12.16.2-alpine3.11 as builder
Step 2/11 : RUN apk --no-cache --update --virtual build-dependencies add python make g++
---> Using cache
Step 3/11 : WORKDIR /app
---> Using cache
Step 4/11 : COPY . .
Step 5/11 : RUN npm ci
added 1357 packages in 47.338s
Step 6/11 : RUN npm run build --prod
Date: 2020-04-16T21:16:03.899Z - Hash: fffa0fddaa3425c55dd3 - Time: 39948ms
---> 27f1479221e4
Step 7/11 : FROM nginx:stable-alpine
Step 8/11 : WORKDIR /app
---> Using cache
Step 9/11 : RUN rm /etc/nginx/conf.d/default.conf
---> Using cache
Step 10/11 : COPY nginx/static.conf /etc/nginx/conf.d
---> Using cache
Step 11/11 : COPY --from=builder /app/dist/app .
Successfully built d201471c91ad
Successfully tagged app:latest
real 2m17.700s
user 0m0.000s
sys 0m0.000s
تغيير ترتيب الطبقات
تم تخزين خطواتنا الثلاث الأولى مؤقتًا (تلميح Using cache
). في الخطوة الرابعة، يتم نسخ كافة ملفات المشروع ويتم تثبيت التبعيات في الخطوة الخامسة RUN npm ci
- بقدر 47.338 ثانية. لماذا إعادة تثبيت التبعيات في كل مرة إذا كانت تتغير نادرًا جدًا؟ دعونا نكتشف سبب عدم تخزينها مؤقتًا. النقطة المهمة هي أن Docker سيتحقق من طبقة تلو الأخرى لمعرفة ما إذا كان الأمر والملفات المرتبطة به قد تغيرت. في الخطوة الرابعة، نقوم بنسخ جميع ملفات مشروعنا، ومن بينها، بالطبع، هناك تغييرات، لذلك لا يأخذ Docker هذه الطبقة من ذاكرة التخزين المؤقت فحسب، بل أيضًا جميع الطبقات اللاحقة! دعونا نجري بعض التغييرات الصغيرة على ملف Dockerfile.
FROM node:12.16.2-alpine3.11 as builder
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build --prod
FROM nginx:1.17.10-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/static.conf /etc/nginx/conf.d
COPY --from=builder /app/dist/app .
أولاً، يتم نسخ package.json وpackage-lock.json، ثم يتم تثبيت التبعيات، وبعد ذلك فقط يتم نسخ المشروع بأكمله. نتيجة ل:
$ time docker build -t app .
Sending build context to Docker daemon 608.8kB
Step 1/12 : FROM node:12.16.2-alpine3.11 as builder
Step 2/12 : RUN apk --no-cache --update --virtual build-dependencies add python make g++
---> Using cache
Step 3/12 : WORKDIR /app
---> Using cache
Step 4/12 : COPY package*.json ./
---> Using cache
Step 5/12 : RUN npm ci
---> Using cache
Step 6/12 : COPY . .
Step 7/12 : RUN npm run build --prod
Date: 2020-04-16T21:29:44.770Z - Hash: fffa0fddaa3425c55dd3 - Time: 38287ms
---> 1b9448c73558
Step 8/12 : FROM nginx:stable-alpine
Step 9/12 : WORKDIR /app
---> Using cache
Step 10/12 : RUN rm /etc/nginx/conf.d/default.conf
---> Using cache
Step 11/12 : COPY nginx/static.conf /etc/nginx/conf.d
---> Using cache
Step 12/12 : COPY --from=builder /app/dist/app .
Successfully built a44dd7c217c3
Successfully tagged app:latest
real 0m46.497s
user 0m0.000s
sys 0m0.000s
46 ثانية بدلاً من 3 دقائق - أفضل بكثير! الترتيب الصحيح للطبقات مهم: أولاً نقوم بنسخ ما لا يتغير، ثم ما يتغير نادرًا، وأخيرًا ما يتغير كثيرًا.
بعد ذلك، بضع كلمات حول تجميع الصور في أنظمة CI/CD.
استخدام الصور السابقة لذاكرة التخزين المؤقت
إذا استخدمنا نوعًا ما من حلول SaaS للبناء، فقد تكون ذاكرة التخزين المؤقت المحلية لـ Docker نظيفة وحديثة. لمنح عامل الإرساء مكانًا للحصول على الطبقات المخبوزة، أعطه الصورة المبنية السابقة.
لنأخذ مثالاً على بناء تطبيقنا في GitHub Actions. نحن نستخدم هذا التكوين
on:
push:
branches:
- master
name: Test docker build
jobs:
deploy:
name: Build
runs-on: ubuntu-latest
env:
IMAGE_NAME: docker.pkg.github.com/${{ github.repository }}/app
IMAGE_TAG: ${{ github.sha }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Login to GitHub Packages
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
docker login docker.pkg.github.com -u $GITHUB_ACTOR -p $TOKEN
- name: Build
run: |
docker build
-t $IMAGE_NAME:$IMAGE_TAG
-t $IMAGE_NAME:latest
.
- name: Push image to GitHub Packages
run: |
docker push $IMAGE_NAME:latest
docker push $IMAGE_NAME:$IMAGE_TAG
- name: Logout
run: |
docker logout docker.pkg.github.com
يتم تجميع الصورة ودفعها إلى حزم GitHub في دقيقتين و20 ثانية:
الآن دعنا نغير البنية بحيث يتم استخدام ذاكرة التخزين المؤقت بناءً على الصور المبنية السابقة:
on:
push:
branches:
- master
name: Test docker build
jobs:
deploy:
name: Build
runs-on: ubuntu-latest
env:
IMAGE_NAME: docker.pkg.github.com/${{ github.repository }}/app
IMAGE_TAG: ${{ github.sha }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Login to GitHub Packages
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
docker login docker.pkg.github.com -u $GITHUB_ACTOR -p $TOKEN
- name: Pull latest images
run: |
docker pull $IMAGE_NAME:latest || true
docker pull $IMAGE_NAME-builder-stage:latest || true
- name: Images list
run: |
docker images
- name: Build
run: |
docker build
--target builder
--cache-from $IMAGE_NAME-builder-stage:latest
-t $IMAGE_NAME-builder-stage
.
docker build
--cache-from $IMAGE_NAME-builder-stage:latest
--cache-from $IMAGE_NAME:latest
-t $IMAGE_NAME:$IMAGE_TAG
-t $IMAGE_NAME:latest
.
- name: Push image to GitHub Packages
run: |
docker push $IMAGE_NAME-builder-stage:latest
docker push $IMAGE_NAME:latest
docker push $IMAGE_NAME:$IMAGE_TAG
- name: Logout
run: |
docker logout docker.pkg.github.com
نحتاج أولاً أن نخبرك عن سبب إطلاق أمرين build
. الحقيقة هي أنه في التجميع متعدد المراحل، ستكون الصورة الناتجة عبارة عن مجموعة من الطبقات من المرحلة الأخيرة. في هذه الحالة، لن يتم تضمين طبقات من الطبقات السابقة في الصورة. لذلك، عند استخدام الصورة النهائية من بناء سابق، لن يتمكن Docker من العثور على طبقات جاهزة لبناء الصورة باستخدام Nodejs (مرحلة البناء). من أجل حل هذه المشكلة، يتم إنشاء صورة وسيطة $IMAGE_NAME-builder-stage
ويتم دفعه إلى حزم GitHub بحيث يمكن استخدامه في إنشاء لاحق كمصدر للتخزين المؤقت.
تم تقليل إجمالي وقت التجميع إلى دقيقة ونصف. يتم قضاء نصف دقيقة في سحب الصور السابقة.
التصوير المسبق
هناك طريقة أخرى لحل مشكلة ذاكرة التخزين المؤقت النظيفة لـ Docker وهي نقل بعض الطبقات إلى ملف Dockerfile آخر، وإنشائها بشكل منفصل، ودفعها إلى سجل الحاوية واستخدامها كأصل.
نقوم بإنشاء صورة Nodejs الخاصة بنا لإنشاء تطبيق Angular. قم بإنشاء Dockerfile.node في المشروع
FROM node:12.16.2-alpine3.11
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++
نقوم بجمع صورة عامة ودفعها في Docker Hub:
docker build -t exsmund/node-for-angular -f Dockerfile.node .
docker push exsmund/node-for-angular:latest
الآن في ملف Dockerfile الرئيسي، نستخدم الصورة النهائية:
FROM exsmund/node-for-angular:latest as builder
...
في مثالنا، لم ينخفض وقت الإنشاء، لكن الصور المعدة مسبقًا يمكن أن تكون مفيدة إذا كان لديك العديد من المشاريع وكان عليك تثبيت نفس التبعيات في كل منها.
لقد بحثنا في عدة طرق لتسريع إنشاء صور عامل الإرساء. إذا كنت تريد أن تتم عملية النشر بسرعة، فحاول استخدام هذا في مشروعك:
- تقليل السياق؛
- استخدام صور الوالدين الصغيرة؛
- تجميع متعدد المراحل
- تغيير ترتيب التعليمات في ملف Dockerfile للاستفادة بكفاءة من ذاكرة التخزين المؤقت؛
- إعداد ذاكرة تخزين مؤقت في أنظمة CI/CD؛
- الإنشاء الأولي للصور.
آمل أن يوضح المثال كيفية عمل Docker، وستكون قادرًا على تكوين النشر على النحو الأمثل. من أجل اللعب بالأمثلة الواردة في المقالة، تم إنشاء مستودع
المصدر: www.habr.com