SSD Nodes Learn 🎉 VPS من $5.50/شهر
الأدلة Matt Connorبقلم Matt Connor

Docker Compose: الفرق بين build وimage على VPS

يفهمك هذا الدليل لماذا يتجاهل compose up تعديل Dockerfile، ومتى يسحب image tag منشوراً أو يبني محلياً باستخدام build، مع طريقة الإصلاح.

الفرق بين build وimage في Docker Compose: الإجابة المختصرة

في ملف Docker Compose، يحدّد image: صورة لسحبها من سجل، بينما يطلب build: من Compose إنشاء صورة على هذا الجهاز باستخدام Dockerfile. عند ضبط image: فقط، يسحب Compose الوسم المحدد ويشغّله. وعند ضبط build: فقط، ينشئ Compose الصورة هنا ويمنحها اسماً مشتقاً من اسم المشروع واسم الخدمة. وعند ضبط كليهما، ينشئ Compose الصورة محلياً، ثم يضع لها الوسم بالاسم المحدد في image:. بهذه الطريقة تنشئ صورة ثم تدفعها إلى سجل بالاسم الذي اخترته.

هذا هو الفرق بالكامل. يوضّح ما يلي أثر ذلك عملياً على الخادم. يفترض هذا أن Docker Engine وإضافة Compose مثبتان مسبقاً؛ يشرح تشغيل Docker على VPS هذا الجزء.

الصيغ الثلاث كاملة

اسحب tag منشوراً وشغّله. لا يُستخدم Dockerfile في أي مرحلة.

services:
  web:
    image: nginx:1.27
    restart: unless-stopped
    ports:
      - "80:80"

أنشئ الصورة من Dockerfile موجود في الدليل الحالي. لا يتم سحب أي شيء باستثناء الصورة الأساسية المحددة في FROM.

services:
  web:
    build: .
    restart: unless-stopped
    ports:
      - "80:80"

أنشئ الصورة محلياً وسمِّ النتيجة باستخدام tag. بعد ذلك، يستطيع docker compose push إرسال ذلك الـtag نفسه إلى registry.

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    image: registry.example.com/acme/web:1.4.2
    restart: unless-stopped
    ports:
      - "80:80"

context هو الدليل الذي يُرسل إلى أداة البناء. ويُحلّ dockerfile بالنسبة إلى ذلك السياق، لذلك يُعدّ استخدام context: . مع dockerfile: docker/prod.Dockerfile أمراً عادياً وصحيحاً. شغّل docker compose images لعرض اسم الصورة وimage ID المرتبطين بكل حاوية خدمة. هذه أسرع طريقة للتأكد من أي صيغة من الصيغ الثلاث كتبتها فعلياً.

لماذا لا يعيد docker compose up إنشاء الصورة بعد أن أغيّر Dockerfile؟

لأن up يتحقق من وجود الصورة، وليس من كونها محدّثة.

عندما يبدأ Compose خدمة تحتوي على قسم build:، يبحث عن الصورة في مخزن الصور المحلي. إذا كانت هناك صورة تحمل الاسم نفسه، يستخدمها Compose. ولا يقرأ Dockerfile، أو يقارن ملفات المصدر، أو يفحص أي طابع زمني. تحدد مواصفة Compose هذه القاعدة في السمة pull_policy، والسلوك الافتراضي هو إنشاء الصورة فقط عندما تكون مفقودة. ويُعامل وجود الصورة على أنه كافٍ.

لذلك تعدّل app.py، وتشغّل docker compose up -d، وترى Compose يذكر أن الحاوية قيد التشغيل، بينما تُقدّم الشيفرة القديمة. لم يحدث فشل، لذلك لم يظهر أي تحذير. هذه أكثر رسائل "لم يُطبَّق التغيير" شيوعاً مع Compose. وتظهر المشكلة بوضوح في كلمة الحالة التي يطبعها Compose بجانب اسم الحاوية: الحاوية التي استبدلها Compose تظهر بحالة recreated أو started، أما الحاوية التي قرر Compose إبقاءها فتظهر بحالة running.

يكفي فحصان لحسم الأمر. يطبع docker compose images معرّف الصورة التي تستخدمها كل حاوية، لذلك سجّله قبل النشر وقارنه بعده. ويحتوي docker image ls على عمود CREATED، وتكون الصورة التي أُنشئت قبل آخر commit صورة قديمة، بغض النظر عمّا طبعه سكربت النشر.

الخيارات التي تفرض إعادة البناء

  • docker compose up -d --build ينفّذ البناء أولاً، ثم يعيد إنشاء أي حاوية تغيّرت صورتها. هذا هو الخيار الذي يبحث عنه معظم المستخدمين.
  • docker compose build web يبني خدمة واحدة ولا يشغّل أي شيء. استخدمه مع docker compose up --no-deps -d web لاستبدال تلك الحاوية فقط وترك بقية المكدس قيد التشغيل.
  • docker compose build --no-cache web يتجاهل كل طبقات التخزين المؤقت ويعيد البناء بدءاً من التعليمة الأولى.
  • docker compose build --pull يحاول سحب إصدار أحدث من الصورة الأساسية في FROM، لذلك تحصل علامة متحركة مثل node:22 على محتواها الحالي بدلاً من النسخة التي نزّلتها في March.
  • docker compose up -d --force-recreate يعيد إنشاء الحاويات من الصورة التي تستخدمها حالياً. ولا ينفّذ عملية بناء أبداً. استخدام هذا الخيار عندما يكون المقصود هو --build يؤدي غالباً إلى طريق مسدود.

يمكنك أيضاً نقل هذا القرار إلى الملف. وفقاً لنص مواصفة Compose، يعني pull_policy: build أن Compose يبني الصورة ويعيد بناءها إذا كانت موجودة مسبقاً. يؤدي كل up إلى تنفيذ عملية بناء، وهذا مناسب على جهاز محمول ونادراً ما يكون مناسباً على خادم.

services:
  web:
    build: .
    image: registry.example.com/acme/web:dev
    pull_policy: build

هناك تفاعل آخر تجدر معرفته. يحاول docker compose pull أيضاً سحب الصور للخدمات التي تحتوي على قسم build، وإذا فشلت عملية السحب فإنه يخبرك بضرورة بناء الصورة بدلاً من ذلك. مرّر --ignore-buildable لتجاوز هذه الخدمات بصمت.

كيف تحدد ذاكرة التخزين المؤقت للبناء مدة النشر

ينتج كل تعليمة في Dockerfile طبقة، ويعيد البنّاء استخدام الطبقة المخزنة مؤقتاً عندما لا تتغير تلك التعليمة ومدخلاتها. بالنسبة إلى COPY، تتمثل المدخلات في محتويات الملفات التي يجري نسخها. بمجرد إخفاق طبقة واحدة في العثور على نسخة مخزنة مؤقتاً، يعاد بناء كل طبقة بعدها، لأن كل طبقة تُبنى على نظام الملفات الذي أنتجته الطبقة السابقة.

تحدد هذه القاعدة وحدها ما إذا كان النشر سيستغرق ثواني أو دقائق. رتّب Dockerfile بدءاً بما نادراً ما يتغير، وانتهاءً بما يتغير مع كل commit.

FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]

يأتي npm ci قبل COPY . .، لذلك يترك تعديل ملف مصدر طبقة التثبيت مخزنة مؤقتاً، ويستأنف البناء عند خطوة النسخ. إذا عكست ترتيب هذين السطرين، فإن تغيير حرف واحد يعيد تثبيت كل تبعية، لأن COPY . . يبطل صلاحية الطبقة التي يُبنى عليها npm ci. وينطبق الترتيب نفسه على pip install -r requirements.txt وعلى go mod download.

يُعد --no-cache الأداة المناسبة عندما تشك في أن طبقة قديمة تخفي إصلاحك. لكنه ليس خياراً افتراضياً جيداً، لأنه يلغي إعادة الاستخدام التي يهدف ترتيب Dockerfile إلى تحقيقها.

هناك إعداد واحد يمكن لـimage sets وCompose تجاوزه: تحدد CMD في Dockerfile ما يشغّله image افتراضياً، ويستبدله المفتاح command: في الخدمة. وتهم هنا كيفية تفاعل command وentrypoint، لأن تجاوزاً في Compose قد يجعل image المبني حديثاً يتصرف تماماً مثل الإصدار القديم.

سياق البناء وملف .dockerignore

يعني context: . أن Compose يضم ذلك الدليل ويرسله إلى أداة البناء قبل تنفيذ التعليمة الأولى. يُرسَل كل ما يحتويه الدليل، بما في ذلك .git وأي دليل بيانات تحتفظ به بجانب الشيفرة المصدرية. إذا توقّف بناء مشروع لم يتغير عند خطوة نقل السياق، فهذا يعني أن السياق كبير جداً.

يستبعد ملف .dockerignore الموجود في جذر السياق المسارات من عملية النقل. وتشبه صيغته صيغة .gitignore.

.git
node_modules
*.log
data/
.env

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

حالة البناء البطيء التي تزداد سوءاً مع الوقت هي bind mount. يوجد named volume خارج دليل المشروع، لكن bind mount مثل ./data:/var/lib/postgresql/data يوجد داخل سياق البناء، لذلك تصبح عمليات البناء أبطأ كل أسبوع مع نمو قاعدة البيانات. يعالج ذلك سطر واحد في .dockerignore. يشرح Bind mounts مقابل named volumes المفاضلة الأوسع.

تحمل معاملات البناء نسخة أصغر من الخطر نفسه. تظهر القيم التي تمررها عبر args: في سجل الصورة لأي شخص يملك الصورة، لذلك ضع هناك رقم إصدار، ولا تضع token مطلقاً. يشرح ملفات البيئة والأسرار في Compose المكان المناسب لبيانات الاعتماد بدلاً من ذلك.

هل ينبغي أن تبني على VPS أم تبني في مكان آخر وتسحب الصورة؟

يُعد البناء على الخادم الذي يمرّر حركة الشبكة الخيار الافتراضي، لأنه أقصر مسار: git pull، ثم docker compose up -d --build. وهذا مناسب على خادم صغير لا يعتمد عليه أحد بعد. لكنه يصبح غير مناسب لسببين يمكن قياسهما، وسبب ثالث لا يظهر إلا في يوم سيئ.

الذاكرة. يشغّل البناء المترجمات وأدوات التجميع بجانب تطبيقك قيد التشغيل، وهذه الأدوات تستهلك عادةً أكبر قدر من الذاكرة في معظم حزم البرامج. على VPS بسعة 1 GB، يكون مجمّع JavaScript أو عملية ترجمة Rust عادةً أكبر عملية على الخادم. عندما تنفد ذاكرة kernel، فإنها تقتل أكبر عملية: إما أن يتوقف البناء مع Killed وحالة الخروج 137، أو تُقتل قاعدة البيانات بدلاً منه ويتوقف الموقع في منتصف النشر. يطبع dmesg -T | grep -i oom سطر القتل مع اسم العملية، وبذلك تعرف أي الحالتين حدثت بدلاً من التخمين.

القرص. يترك كل بناء طبقات، ويحتفظ أداة البناء بذاكرة تخزين مؤقت خاصة بها منفصلة عن صورك. يعرض docker system df كليهما، ولا يتوقف صف ذاكرة التخزين المؤقت للبناء عن النمو. استعد المساحة باستخدام docker image prune للصور المعلّقة، وdocker builder prune للطبقات المخزّنة مؤقتاً. امتلاء القرص يوقف أكثر من عملية البناء. إذ تتوقف قاعدة البيانات أيضاً عن الكتابة، وتكون تكلفة هذا الفشل أكبر بكثير من تكلفة نشر بطيء.

قابلية إعادة الإنتاج. توجد الصورة المبنية على الخادم في ذلك الخادم وحده. تعني عملية التراجع استخراج الالتزام القديم وإعادة البناء، ولا يوجد ضمان بأن ينتج هذا البناء ما كان لديك، لأن وسم base تحرّك، كما تحرّكت مرايا الحزم معه. أما البناء في مكان آخر ودفع وسم، فيحوّلان التراجع إلى تعديل: وجّه image: إلى الوسم السابق وشغّل docker compose up -d.

الترتيب العملي المستقر بسيط. يشغّل نظام التكامل المستمر لديك البناء ويدفع registry.example.com/acme/web:<git-sha>، بينما يحتوي ملف Compose على VPS على image: من دون مفتاح build: إطلاقاً. يصبح النشر عندئذ أمرين يحتاجان إلى قدر ضئيل جداً من الذاكرة.

docker compose pull
docker compose up -d

شغّل docker login registry.example.com مرة واحدة على الخادم، وبعد ذلك سيتمكن Compose من سحب الوسوم الخاصة.

احتفظ بقسم البناء لأغراض التطوير بدلاً من حذفه، في ملف تختار اسمه بنفسك.

# compose.dev.yaml
services:
  web:
    build:
      context: .
    pull_policy: build
docker compose -f compose.yaml -f compose.dev.yaml up -d --build

سمِّ ذلك الملف compose.dev.yaml وليس compose.override.yaml. يحمّل Compose ملف override تلقائياً كلما كان موجوداً، ولذلك قد يؤدي وجود ملف override منسوخ بالخطأ إلى الخادم إلى بدء البناء عليه مجدداً دون تنبيه. يشرح توزيع عدة ملفات Compose كيفية حل الدمج لكل مفتاح.

فخ البنية عند الإنشاء في بيئة أخرى

تحمل الصورة بنية المعالج التي أُنشئت لها. إذا أنشأت الصورة على حاسوب محمول مزوّد بمعالج Apple Silicon، ثم دفعتها، وبعد ذلك سحبت الوسم نفسه إلى VPS بمعمارية x86_64، فسيحذّر Docker من أن منصة الصورة المطلوبة لا تطابق منصة المضيف المكتشفة. تتوقف العملية بعد ذلك مع exec format error، وهي رسالة تبدو كأنها تشير إلى ملف ثنائي تالف، لكنها لا تعني ذلك. أنشئ الصورة للهدف صراحةً:

docker buildx build --platform linux/amd64 \
  -t registry.example.com/acme/web:1.4.2 --push .

يحدث عدم التطابق نفسه بالعكس إذا كان حاسوبك المحمول بمعمارية x86 وشغّلت VPS بمعمارية ARM بدلاً من x86. يزيل جعل CI ينشئ الصورة على المعمارية التي ستنشرها هذا الالتباس.

ما الذي يجب التحقق منه بعد النشر

  • docker compose images يعرض الصورة والوسم المستخدمين لكل حاوية قيد التشغيل. ويثبت تغيّر معرّف الصورة أن الإصدار الجديد قيد الخدمة.
  • docker compose config يعرض الملف المدمج بعد استبدال المتغيرات، حتى تتمكن من قراءة اسم الصورة النهائي الذي سيستخدمه Compose قبل تشغيل أي شيء.
  • docker compose logs -f web خلال أول 30 ثانية بعد التبديل. الحاوية التي تبدأ ثم تتوقف تعيد التشغيل في حلقة بدلاً من البقاء قيد التشغيل، وتظل هذه الحلقة صامتة ما لم تراقبها.
  • docker image ls يعرض عمود CREATED. الصورة الأقدم من آخر commit لم تُعَدْ بناؤها.

إذا كنت لا تزال تُعدّ الملف الذي ستُجرى عليه هذه الفحوص، يشرح أساسيات ملف Compose على VPS المفاتيح ذات الصلة، بينما تسرد ورقة الغش لأوامر Compose بقية الأوامر الفرعية.

FAQ

هل يمكنني استخدام build وimage في الخدمة نفسها؟

نعم، وهذا هو الإعداد المعتاد لمشروع تنشئه بنفسك. ينشئ Compose الصورة من قسم build: ويضع عليها الوسم الذي تحدده قيمة image:. يرسل docker compose push هذا الوسم إلى registry، وتسحبه آلة أخرى. إذا لم يتضمن الإعداد مفتاح image:، يواصل Compose إنشاء الصورة، لكنه يسمّيها باستخدام اسم المشروع واسم الخدمة، ويحذّر من أن غياب هذه الخاصية يمنع دفع الصورة إلى registry.

لماذا لا يلتقط docker compose up التغيير الذي أجريته على Dockerfile؟

لأن up يتحقق فقط من وجود صورة بهذا الاسم. إذا كانت الصورة موجودة، يبدأ Compose تشغيلها ولا يقارنها مطلقاً بـDockerfile أو ملفات المصدر لديك. شغّل docker compose up -d --build، أو شغّل docker compose build web ثم docker compose up --no-deps -d web لاستبدال خدمة واحدة. يؤدي ضبط pull_policy: build على الخدمة إلى إعادة إنشاء كل up، وهذا مناسب لآلة التطوير.

ما الفرق بين --build و--force-recreate؟

يعيد --build إنشاء الصورة، ثم يعيد إنشاء الحاويات التي تغيرت صورتها. يعيد --force-recreate إنشاء الحاويات من الصورة الموجودة لديها، ولذلك لا يمكنه مطلقاً التقاط تغيير في الشفرة. إذا كان التغيير في المصدر أو في Dockerfile، فإن --build هو الخيار الذي تحتاج إليه. أما --force-recreate فهو مخصص لإعادة ضبط الحاوية نفسها، مثلاً لمسح طبقتها القابلة للكتابة مع الإبقاء على الصورة نفسها.

هل ينبغي أن أنشئ صور Docker على VPS أم في مكان آخر؟

أنشئها في مكان آخر واسحب الوسم مرة واحدة عندما يبدأ الخادم أيضاً في استقبال حركة الشبكة. تنافس عملية الإنشاء تطبيقك على الذاكرة. وفي VPS صغير، يحل kernel هذا التنافس بإنهاء العملية الأكبر، وقد تكون عملية الإنشاء أو قاعدة البيانات. تترك عمليات الإنشاء أيضاً ذاكرة تخزين مؤقت على القرص ولا يستعيدها أي مكوّن نيابةً عنك. يظل الإنشاء على الخادم مناسباً لمشروع صغير بلا مستخدمين. ويمكنك نقل العملية لاحقاً بتكلفة قليلة إذا أبقيت قسم build: في ملف Compose مخصص للتطوير فقط.

كيف أمنع ذاكرة التخزين المؤقت لعملية إنشاء Docker من ملء القرص؟

شغّل docker system df لمعرفة مقدار المساحة التي تشغلها الصور وذاكرة التخزين المؤقت للإنشاء كل على حدة. يزيل docker builder prune الطبقات المخزنة مؤقتاً، ويزيل docker image prune الصور غير المرتبطة التي خلّفتها عمليات إنشاء سابقة. إضافة -a إلى أي منهما أكثر شمولاً، وتجبر عملية الإنشاء التالية على البدء دون ذاكرة تخزين مؤقت. لا تُجدول docker system prune -af --volumes على خادم، لأن --volumes يحذف أي volume لا تستخدمه حاوية حالياً. وتكون قاعدة بيانات المكدس الذي أوقفته للصيانة في volume من هذا النوع تحديداً.