الإنتاج
ما يفصل بين حاوية تعمل وخدمة قابلة للتشغيل الفعلي.
النشر في بيئة الإنتاج
الصورة التي تعمل جيدًا على جهازك ليست بالضرورة جاهزة للإنتاج، ولا تُردم الفجوة بين الحالتين بأمر إضافي واحد. تُردم بسلسلة من القرارات لا يُجبرك شيء على اتخاذها، ولهذا السبب بالذات يُنسى كثير منها.
والحوادث الناتجة عن ذلك لها بصمة يسهل التعرف عليها، لأنها تتكرر في كل مكان. خادم يبلغ قرصه 100 % خلال ثلاثة أسابيع: لم يضبط أحد تدوير السجلات، وDocker يحتفظ بها دون حد افتراضيًا. حاوية فيها تسرّب في الذاكرة تُسقط في النهاية قاعدة البيانات المجاورة لها لغياب أي سقف. عملية نشر تقطع الخدمة أربعين ثانية لأن العملية لا تتلقى إشارة الإيقاف أبدًا. حركة مرور تُوجَّه إلى حاوية «بدأت» لكنها لا تقبل اتصالات بعد، لأن لا شيء كان يميّز «أُطلقت» عن «جاهزة». وبشكل أخفى، صورة لا يعلم أحد أنها تحمل منذ ثمانية أشهر مكتبة بها ثغرة أمنية.
لا شيء من هذه المشكلات صعب. لكنها كلها غير مرئية أثناء التطوير، حيث تعمل الخدمة عشر دقائق، والذاكرة وفيرة، ولا يتضرر أحد من انقطاع. هي لا تظهر إلا مع طول المدة وارتفاع الحمل، أي في أسوأ لحظة ممكنة.
يستعرض هذا الدرس الأخير هذه القرارات واحدًا واحدًا. الهدف ليس تحويل خادم إلى عنقود (cluster): فـ Compose على مضيف واحد بنية مشروعة تمامًا لكثير من الخدمات. الهدف أن يصمد هذا الخادم ستة أشهر دون تدخل طارئ.
صورة الإنتاج: صغيرة الحجم ومثبّتة
الصورة الأصغر تُنقل أسرع، وتبدأ أسرع، وتوفر شيفرة أقل قابلة للاستغلال؛ والصورة المثبّتة يمكن إعادة بنائها مطابقةً لما كانت عليه بعد ستة أشهر. يؤدي multi-stage الذي رأيناه في الدرس 3 معظم العمل؛ ويبقى اختيار القاعدة النهائية.
| القاعدة | الحجم التقريبي | المحتوى | التشخيص |
|---|---|---|---|
debian:12 | ~120 ميغابايت | توزيعة كاملة | مريح |
python:3.12-slim | ~45 ميغابايت | Debian مخفّفة، glibc | الـ shell موجود |
alpine:3.20 | ~8 ميغابايت | musl بدل glibc | shell من نوع sh |
distroless | ~20 ميغابايت | لا shell ولا مدير حزم | لا يمكن استخدام exec |
scratch | 0 | لا شيء إطلاقًا | لا يوجد |
الاختيار ليس «الأصغر هو الأفضل». فـ Alpine تفرض musl، الذي يعطّل بعض الملفات التنفيذية المبنية مسبقًا ويغيّر سلوك تحليل DNS؛ أما النسخ -slim فتقدّم غالبًا أفضل توازن بين الحجم وراحة البال. وتمنح Distroless وscratch أصغر سطح هجوم، لكن على حساب التشخيص: فبلا shell لا فائدة من docker exec، ويجب الاعتماد كليًا على السجلات والمقاييس.
أما التثبيت، فالوسم (tag) وحده لا يكفي في الإنتاج: الوسم متحرك، أما الـ digest (البصمة الرقمية) فثابت.
# قد يشير الوسم إلى محتوى مختلف غدًا
FROM node:22-slim
# الـ digest يحدد محتوى بعينه، إلى الأبد
FROM node:22-slim@sha256:d3e2b17e0f5a9b1d9c8f6e5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c
يمكن قراءة الـ digest المراد تثبيته بالأمر docker inspect node:22-slim --format '{{index .RepoDigests 0}}'.
وأخيرًا، إذا كانت بنية خوادمك تختلف عن بنية أجهزتك — وهي حالة شائعة منذ ظهور أجهزة Mac بمعالجات Apple Silicon — فابنِ صورًا متعددة البنى بالأمر docker buildx build --platform linux/amd64,linux/arm64 --push. الخيار --push ليس اختياريًا: فمثل هذه الصورة فهرس يشير إلى عدة manifests، وهي بنية لا يعرف المخزن المحلي التقليدي تخزينها.
Healthchecks: التمييز بين «بدأ» و«جاهز»
بدون healthcheck لا يعرف Docker سوى حالة واحدة مفيدة: العملية تعمل أو لا تعمل. غير أن عملية تعمل قد تكون عالقة على قفل، أو فقدت اتصالها بقاعدة البيانات، أو لم تنتهِ من تحميل ذاكرتها المؤقتة.
# --interval : وتيرة الفحوصات في الوضع العادي
# --timeout : بعد هذه المدة يُحتسب الفحص فاشلًا
# --start-period : فترة سماح عند البدء، لا تُحتسب فيها الإخفاقات
# --retries : عدد الإخفاقات المتتالية قبل الوصول إلى حالة "unhealthy"
HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
اختيار أمر الفحص مهم. curl -f هو الخيار المعتاد، لكنه يفترض أن curl مثبّت — وهذا غير صحيح في صورة minimal، كما أن تثبيته لمجرد الـ healthcheck يوسّع سطح الهجوم. أما استخدام بيئة التشغيل الموجودة أصلًا، كما في المثال أعلاه مع Node، فيتفادى هذه المفاضلة.
المعامل --start-period هو الأكثر إهمالًا والأكثر تسببًا في النتائج الإيجابية الكاذبة. قد يستغرق تطبيق Java، أو تطبيق ينفّذ migrations عند بدء التشغيل، ثلاثين ثانية قبل أن يستجيب: وبدون فترة سماح يُعلَن أنه unhealthy فيُعاد تشغيله، ولا ينجح أبدًا في الإقلاع. وتلاحظ حلقة إعادة تشغيل بلا سبب ظاهر.
على جانب التطبيق، يجب أن تتحقق نقطة النهاية من شيء حقيقي. فمسار يردّ بـ 200 دون اختبار أي شيء لا يكشف أي عطل.
app.get('/health', async (req, res) => {
try {
await db.query('SELECT 1'); // هل تستجيب قاعدة البيانات فعلًا؟
await cache.ping(); // وماذا عن الذاكرة المؤقتة؟
res.status(200).json({ status: 'ok' });
} catch (err) {
res.status(503).json({ status: 'degraded', error: err.message });
}
});
docker ps # تظهر حالة الصحة في عمود STATUS
# سجل الفحوصات، مع مخرجات كل محاولة:
# ضروري لفهم "سبب" كون الخدمة unhealthy
docker inspect --format '{{json .State.Health}}' mon-api
وملاحظة صريحة: Docker وحده لا يفعل شيئًا بحاوية unhealthy، فهو لا يعيد تشغيلها. تُستخدم هذه الحالة من قبل Compose (condition: service_healthy)، ومن قبل المنسّقات (orchestrators)، ومن قبل نظام المراقبة لديك — وهو من يجب أن يُطلق التنبيه.
حدود الموارد: منع حاوية من إسقاط الحاويات الأخرى
بدون حدود، يمكن لحاوية أن تستهلك كل ذاكرة المضيف ومعالجه. وينتهي الأمر بالنواة إلى استدعاء قاتل العمليات فيها، ولا شيء يضمن أنه سيختار المسؤولة عن المشكلة: فغالبًا ما تسقط العملية الأكثر استهلاكًا، أي قاعدة بياناتك.
docker run -d \
--memory 512m \ # سقف صارم للذاكرة
--memory-swap 512m \ # مساوٍ لـ --memory: يمنع أي swap
--memory-reservation 256m \ # حد مرن، يُطبَّق تحت الضغط
--cpus 1.5 \ # نواة ونصف كحد أقصى
--pids-limit 200 \ # الحد الأقصى لعدد العمليات (ضد fork bomb)
--restart unless-stopped \
mon-api:1.2.3
يستحق ضبط --memory-swap شرحًا. فإن لم يُحدَّد، يسمح Docker بحجم swap مساوٍ لحد الذاكرة: فتبطئ العملية المتسرّبة الجهاز بشدة بدل أن تتوقف. أما ضبط القيمتين على الرقم نفسه فيمنع الـ swap ويجعل الحاوية تفشل بوضوح — وهو شبه دائمًا أفضل، لأن الفشل الصريح يُكتشف بينما البطء المتناثر لا يُكتشف.
في Compose تمرّ هذه الحدود عبر deploy.resources، الذي يحترمه Compose v2 حتى خارج العنقود:
services:
api:
image: registre.exemple.com/equipe/mon-api:1.2.3
deploy:
resources:
limits:
cpus: "1.5"
memory: 512M
reservations:
memory: 256M
pids_limit: 200
restart: unless-stopped
كيف تختار القيم؟ راقب الاستهلاك تحت الحمل العادي بالأمر docker stats، ثم أضف هامشًا يتراوح بين 50 و100 %. واحفظ في ذهنك بصمة السقف المنخفض أكثر من اللازم: رمز الخروج 137، أي 128 + 9، وهو SIGKILL أرسلته النواة.
docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}'
docker inspect mon-api --format '{{.State.ExitCode}} {{.State.OOMKilled}}'
تسجيل السجلات: السبب الأول لامتلاء القرص
المشغّل الافتراضي json-file لا يطبّق أي تدوير. فالتطبيق الثرثار يكتب بلا توقف في /var/lib/docker/containers/<id>/<id>-json.log. وعلى خادم يعمل منذ ستة أشهر، قد يبلغ هذا الملف الواحد عشرات الغيغابايتات ويملأ القسم — فيشلّ كل الخدمات الأخرى على الجهاز.
يُكتب الحل لكل حاوية على حدة (--log-opt max-size=10m --log-opt max-file=3، أي ثلاثة ملفات من 10 ميغابايت كحد أقصى)، لكن الممارسة الجيدة هي ضبطه مرة واحدة على مستوى الـ daemon، ليكون الافتراضي سليمًا لكل ما سيُشغَّل على الجهاز.
// /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"live-restore": true
}
يطبّق الأمر sudo systemctl reload docker الملف، ويؤكد الأمر docker info --format '{{.LoggingDriver}}' أخذه في الحسبان.
ويستحق live-restore أن يُعرف: فهو يسمح للحاويات بمواصلة العمل أثناء إعادة تشغيل الـ daemon، مما يجعل تحديثات Docker أقل إثارة للقلق بكثير. لكن انتبه: إعدادات الـ daemon لا تنطبق إلا على الحاويات المنشأة بعد إعادة التحميل، أما الحاويات الموجودة فتحتفظ بإعداداتها.
أما المشغّلات الأخرى فتلبّي حاجات المركزية — local (أكثر إحكامًا)، وjournald، وsyslog، وfluentd، وgelf، وawslogs. ومن المهم معرفة أمر قبل التحويل: مع أغلبها يتوقف docker logs عن العمل، لأن المحرك لا يحتفظ بشيء محليًا. لا يتيح ذلك إلا json-file وlocal وjournald.
وهناك قاعدتان على جانب التطبيق، تحددان جدوى كل ما سواهما. اكتب إلى المخرج القياسي، لا إلى ملف داخل الحاوية. واكتب بصيغة JSON منظّمة بدل النص الحر: ففي خمسين حاوية، يتحدد الفرق بين سجلات قابلة للاستغلال وجدار من النصوص عند هذه النقطة.
الإيقاف السليم: التفصيل الذي يتسبب في انقطاع الخدمة
يرسل docker stop الإشارة SIGTERM إلى العملية ذات PID 1، وينتظر عشر ثوانٍ، ثم يرسل SIGKILL. وإذا كان تطبيقك لا يعالج SIGTERM، فإن كل عملية نشر ستقطع الطلبات الجارية بشكل مفاجئ. هناك ثلاثة أسباب محتملة، وثلاثة حلول.
السبب الأول: صيغة shell من CMD. فتكون PID 1 هي /bin/sh، التي لا تمرّر الإشارات.
# PID 1 = sh. تطبيقك لا يتلقى SIGTERM أبدًا.
CMD node server.js
# PID 1 = node. تصل الإشارة.
CMD ["node", "server.js"]
السبب الثاني: التطبيق لا يعالج الإشارة.
process.on('SIGTERM', () => {
server.close(async () => { // لا اتصالات جديدة
await db.end(); // إغلاق سليم لمجمّع الاتصالات
process.exit(0);
});
setTimeout(() => process.exit(1), 25_000); // شبكة أمان
});
السبب الثالث: العمليات الزومبي. فـ PID 1 الحقيقية يجب أن تحصد العمليات اليتيمة، وهو ما لا تفعله أغلب التطبيقات. يُدرج الخيار --init مديرًا minimal (tini) يتولى ذلك ويمرّر الإشارات.
services:
api:
image: registre.exemple.com/equipe/mon-api:1.2.3
init: true
# مهلة السماح: يجب أن تتجاوز مدة إيقافك السليم
stop_grace_period: 30s
# قِس زمن الإيقاف. إن اقترب من 10 ثوانٍ (أو من stop_grace_period) =>
# فالإشارة لا تُعالَج. وإن كان أقل من ثانية => فكل شيء سليم.
time docker stop mon-api
السجل الخاص: رفع الصور وجلبها
في الإنتاج لا نبني على الخادم: بل نجلب صورة مبنية ومختبَرة مسبقًا. وهذا يستلزم سجلًا (registry).
# --password-stdin يمنع ظهور السر في
# سجل الـ shell أو في مخرجات "ps".
echo "$REGISTRY_TOKEN" | docker login registre.exemple.com \
-u ci-bot --password-stdin
docker tag mon-api:1.2.3 registre.exemple.com/equipe/mon-api:1.2.3
docker push registre.exemple.com/equipe/mon-api:1.2.3
docker logout registre.exemple.com
وهذه اصطلاح لوسم الصور يُستحسن اعتماده منذ البداية: وسمان لكل عملية بناء، أحدهما مقروء والآخر قابل للتتبع.
DEPOT="registre.exemple.com/equipe/mon-api"
docker build -t "$DEPOT:1.2.3" -t "$DEPOT:$(git rev-parse --short HEAD)" .
docker push --all-tags "$DEPOT"
يجيب وسم الـ commit عن السؤال الوحيد المهم في الثالثة فجرًا: ما الشيفرة الدقيقة التي تعمل في الإنتاج؟ وعلى جانب الخادم، يُختزل النشر حينئذٍ في ثلاثة أوامر.
docker compose pull # جلب الصورة الجديدة
docker compose up -d # إعادة إنشاء الخدمات التي تغيّرت صورتها
docker compose ps
# لمس خدمة واحدة فقط، دون إعادة تشغيل ما تعتمد عليه
docker compose up -d --no-deps api
ولنكن واضحين بشأن الحدود: هذه العملية تعيد إنشاء الحاوية، وبالتالي تقطع الخدمة لوقت وجيز. أما النشر دون انقطاع فيتطلب نسختين خلف موزّع أحمال، وهذا يخرج عن نطاق Compose وحده. وفي أغلب الخدمات يكون انقطاع من ثانية إلى ثانيتين مقبولًا؛ وحين لا يكون كذلك، فهذه إشارة إلى أن المنسّق (orchestrator) أصبح ضروريًا.
تحليل الثغرات الأمنية وتقوية بيئة التنفيذ
تحمل الصورة مئات الحزم لم تختر منها سوى حفنة. وتُنشر فيها ثغرات باستمرار، حتى في صورة لم تعدّلها منذ ستة أشهر — ومن هنا ضرورة إعادة البناء بانتظام، لا فقط عندما تتغير الشيفرة.
# ملخص: عدد الثغرات حسب مستوى الخطورة
docker scout quickview mon-api:1.2.3
# التفاصيل، مع التصفية على المستويات التي تتطلب إجراءً
docker scout cves --only-severity critical,high mon-api:1.2.3
# اقتراحات لتحديث الصورة الأساسية
docker scout recommendations mon-api:1.2.3
أداة Trivy، وهي أداة حرة واسعة الانتشار، تُستخدم داخل حاوية وتناسب التكامل المستمر. وتحذير بشأن هذا الأمر: فربط /var/run/docker.sock يمنح الحاوية تحكمًا كاملًا في الـ daemon، وبالتالي في الجهاز. وهذا مقبول لأداة موثوقة على وكيل بناء مخصص؛ لكنه ليس أمرًا هيّنًا أبدًا.
# --exit-code 1 يجعل الأمر يفشل إن بقيت ثغرات
# من المستوى المطلوب: وهكذا تُوقَف سلسلة التسليم.
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest image \
--severity HIGH,CRITICAL --exit-code 1 \
mon-api:1.2.3
وتقوية بيئة التنفيذ تكمّل التحليل بشكل مفيد: فهذه الخيارات تقلّص ما يستطيع المهاجم فعله حتى في حال وجود ثغرة في التطبيق.
docker run -d \
--read-only \ # الملفات للقراءة فقط
--tmpfs /tmp:rw,noexec,nosuid,size=64m \ # باستثناء /tmp، في الذاكرة
--cap-drop ALL \ # إزالة كل القدرات (capabilities)
--cap-add NET_BIND_SERVICE \ # ثم إعادة القدرة الوحيدة المفيدة
--security-opt no-new-privileges \ # لا رفع للصلاحيات عبر setuid
--user 1001:1001 \ # لا تعمل كـ root أبدًا
mon-api:1.2.3
الخيار --read-only فعّال بوجه خاص: فهو يمنع المهاجم من كتابة ملف تنفيذي داخل الحاوية. وكل ما يتطلبه هو تحديد المسارات القليلة التي يجب أن يكتب فيها التطبيق، وربطها (mount) بشكل منفصل.
حزمة إنتاج متكاملة
لنجمع كل ما سبق في ملف يُستخدم مكمّلًا لملف compose.yaml الخاص ببيئة التطوير.
# compose.prod.yaml
# الاستخدام : docker compose -f compose.yaml -f compose.prod.yaml up -d --wait
services:
api:
# صورة من السجل، مثبّتة : لا بناء على الخادم
image: registre.exemple.com/equipe/mon-api:${VERSION:?VERSION est obligatoire}
build: !reset null # يلغي البناء الموروث من الملف الأساسي
restart: unless-stopped
init: true
stop_grace_period: 30s
read_only: true
tmpfs:
- /tmp:size=64m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
user: "1001:1001"
environment:
NODE_ENV: production
DATABASE_URL: postgres://app:${DB_PASSWORD:?}@db:5432/appdb
ports:
- "127.0.0.1:3000:3000" # سيتولى وكيل (proxy) في الواجهة الأمامية الكشف للعموم
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 30s
timeout: 3s
retries: 3
start_period: 20s
deploy:
resources:
limits:
cpus: "1.5"
memory: 512M
reservations:
memory: 256M
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
networks:
- frontend
- backend
db:
image: postgres:16-alpine
restart: unless-stopped
stop_grace_period: 1m # اترك Postgres ينهي نقطة التحقق (checkpoint) الخاصة به
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD:?}
POSTGRES_DB: appdb
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
deploy:
resources:
limits:
memory: 1G
networks:
- backend # لا يمكن الوصول إليه أبدًا من الخارج
networks:
frontend:
backend:
internal: true
volumes:
db-data:
# تحقق دائمًا من نتيجة الدمج قبل النشر
docker compose -f compose.yaml -f compose.prod.yaml config
VERSION=1.2.3 docker compose -f compose.yaml -f compose.prod.yaml up -d --wait
لاحظ ${VERSION:?...}: فالنشر دون تحديد إصدار صريح يفشل فورًا بدلًا من الانزلاق بصمت إلى latest. ولاحظ أيضًا استخدام restart: unless-stopped بدلًا من always: الفرق أن الحاوية التي أُوقفت عمدًا لا يُعاد تشغيلها بعد إعادة إقلاع الخادم، وهذا يتجنب إيقاظ خدمة أُخرجت من الخدمة عن قصد.
ما ينبغي مراقبته بعد ذلك
النشر ليس نهاية المطاف. تأتي مساحة القرص أولًا، لأنها أكثر الأعطال شيوعًا. ثم الحالة الصحية وعدّاد إعادات التشغيل، لأن القيمة المتزايدة تشير إلى مشكلة قد لا ينبّه إليها أي إنذار. وأخيرًا الاستهلاك مقارنةً بالحدود المضبوطة، لاكتشاف الانحراف قبل وقوع الحادثة.
docker system df ; df -h /var/lib/docker
docker ps --format 'table {{.Names}}\t{{.Status}}'
docker inspect mon-api --format '{{.RestartCount}}'
يبقى أمر حداثة الصور: فالصورة التي لم يُعَد بناؤها منذ أشهر تراكم ثغرات اعتماداتها حتى لو لم يتغير شيفرتك. إن إعادة البناء وإعادة النشر دوريًا دون أي تغيير وظيفي ممارسة صيانة، وليست مضيعة للوقت.
ويتطلب التنظيف الآلي حذرًا كذلك. فالأمر docker system prune -af على الخادم يحذف الصور غير المستخدمة، ومنها الإصدار السابق الذي قد تريد الرجوع إليه عند حدوث مشكلة. فضّل أمرًا مستهدفًا مثل docker image prune -a --filter "until=720h" --force، الذي لا يمس سوى الصور غير المستخدمة منذ أكثر من ثلاثين يومًا.
الخلاصة
- صورة الإنتاج minimale ومثبّتة: أي بناء متعدد المراحل (multi-stage)، وصورة أساس من نوع
-slimأو distroless بحسب الحاجة إلى التشخيص، مع الإشارة إليها بالـ digest بدلًا من وسم (tag) متحرك. - يميّز healthcheck مضبوط بقيمة
--start-periodواقعية بين «بدأت» و«جاهزة». ويجب أن تختبر نقطة النهاية قاعدة البيانات والذاكرة المؤقتة (cache)، وإلا فلن تكتشف شيئًا. لا يعيد Docker تشغيل حاوية في حالةunhealthy: إنها إشارة موجهة إلى Compose وإلى نظام المراقبة لديك. - اضبط دائمًا
--memoryو--cpus. فبلا سقف، يؤدي تسرّب الذاكرة في حاوية واحدة إلى إسقاط جاراتها. ويعني رمز الخروج137«أُنهيت بسبب تجاوز الذاكرة». - اضبط تدوير السجلات في
/etc/docker/daemon.jsonمرة واحدة وللأبد. فالإعداد الافتراضيjson-fileدون تدوير هو السبب الأول لامتلاء القرص على خادم Docker. - تحقق من أن
docker stopيستغرق أقل من ثانية. وإلا فإن تطبيقك لا يعالجSIGTERM— بسبب صيغة shell للأمرCMD، أو غياب معالج الإشارات — وسيقطع كل نشر الطلبات الجارية. - لا تبنِ الصورة على خادم الإنتاج: ادفع صورة موسومة بإصدارها وبـ commit معًا، ثم نفّذ
docker compose pull && docker compose up -d. - حلّل صورك (
docker scout cves، Trivy) وأعد بناءها دوريًا حتى دون تغيير في الشيفرة. وشدّد التنفيذ باستخدام--read-onlyو--cap-drop ALLوno-new-privilegesومستخدم غير root.
للمزيد: حين لا يعود مضيف واحد كافيًا — توافر عالٍ، نشر تدريجي دون انقطاع، توسيع تلقائي — فهذا مجال المنسّقات (orchestrators). وكل ما تعلمته هنا يبقى صالحًا: فـ Kubernetes يستهلك صور OCI نفسها، ويتوقع بالضبط healthchecks وحدود الموارد والسجلات على المخرج القياسي ذاتها. ويبقى المرجع الكامل للخيارات متاحًا على docs.docker.com.
التعليقات
التعليقات مدعومة عبر GitHub Discussions
سجّل الدخول باستخدام GitHub للمشاركة في النقاش