كل مفهوم من الدروس التسعة السابقة — الصور الأساسية، ترتيب الطبقات، وحدات التخزين، الشبكات، Compose — يتلاقى في مهارة عملية واحدة: أخذ تطبيق حقيقي وجعله يعمل بشكل متطابق على أي جهاز، في أي بيئة، بأمر واحد. يستعرض هذا الدرس ذلك من البداية إلى النهاية: إعداد متعدد الخدمات بجودة إنتاجية يضم Node.js API وقاعدة بيانات PostgreSQL وخادم Nginx عكسي وملف Docker Compose كامل يمكن إرسالته للتجهيز اليوم.
الهدف ليس مجرد حاوية تعمل. الهدف إعداد لا يحتاج مهندس أول إلى إعادة كتابته: التعامل الصحيح مع الأسرار، وفحوصات الصحة، والمستخدمون غير الجذريون، ونظافة ذاكرة التخزين المؤقت في وقت البناء، وملف Compose الذي يفصل بين متطلبات التطوير والإنتاج.
المعمارية المستهدفة
سنحوّل واجهة برمجية REST تضم المكونات التالية:
app — Node.js 22 API (Express)، Dockerfile متعدد المراحل، يعمل كمستخدم غير جذري.
db — PostgreSQL 16 مع وحدة تخزين مُسمّاة لاستمرارية البيانات.
تتواصل الخدمات الثلاث عبر شبكة Docker خاصة. فقط الـ proxy مكشوف على منفذ المضيف. التطبيق وقاعدة البيانات لا يمكن الوصول إليهما مباشرة من خارج شبكة Docker.
المعمارية الثلاثية: Nginx يستقبل الطلبات العامة ويُعيد توجيهها إلى Node API، الذي هو وحده الذي يتصل بقاعدة البيانات. لا التطبيق ولا قاعدة البيانات مكشوفان على المضيف مباشرة.
الخطوة الأولى: هيكل المشروع وملف .dockerignore
قبل كتابة أي تعليمة Dockerfile، حدد ما ينتمي إلى سياق البناء وما لا ينتمي إليه. ملف .dockerignore الصحيح هو أول ملف تكتبه.
# .dockerignore — ابقِ سياق البناء خفيفاً
node_modules
npm-debug.log
.env
.env.*
.git
.gitignore
*.md
coverage
.nyc_output
dist # سيُنشئه Builder؛ لا تُرسل نسخاً قديمة
docker-compose*.yml
nginx/
سياق البناء المنتفخ هو أحد أكثر مسببات التباطؤ في CI شيوعاً. إرسال node_modules/ (في الغالب مئات الميغابايتات) إلى daemon Docker هو I/O مهدر — Builder يُثبّت نسخه الخاصة. دائماً راجع .dockerignore بنفس العناية التي تراجع بها .gitignore.
الخطوة الثانية: Dockerfile بجودة إنتاجية
يستخدم هذا الملف بناء ثنائي المراحل: مرحلة builder تُثبّت جميع الاعتمادية وتُجمّع التطبيق، ومرحلة runtime خفيفة تحمل فقط ما هو مطلوب للتشغيل. الصورة النهائية لا تحتوي على أدوات بناء ولا devDependencies ولا source maps.
# syntax=docker/dockerfile:1.7
# ---- المرحلة الأولى: البناء ----
FROM node:22.3-alpine3.20 AS builder
WORKDIR /app
# نسخ ملفات البيان أولاً للاستفادة من التخزين المؤقت للطبقات
COPY package.json package-lock.json ./
RUN npm ci --include=dev
# نسخ المصدر والبناء
COPY src/ ./src/
RUN npm run build # الإخراج إلى /app/dist
# ---- المرحلة الثانية: التشغيل ----
FROM node:22.3-alpine3.20 AS runtime
WORKDIR /app
# تثبيت اعتمادية الإنتاج فقط في مرحلة التشغيل
COPY package.json package-lock.json ./
RUN npm ci --omit=dev \
&& npm cache clean --force
# نسخ الإخراج المُجمَّع من Builder
COPY --from=builder /app/dist ./dist
# مستخدم غير جذري — مبدأ أقل الامتيازات
RUN addgroup -S appgroup \
&& adduser -S appuser -G appgroup
USER appuser
# وسم لقابلية التتبع في docker inspect وواجهات السجل
ARG GIT_SHA=unknown
LABEL org.opencontainers.image.revision="${GIT_SHA}"
ENV NODE_ENV=production \
PORT=3000
EXPOSE 3000
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:3000/healthz || exit 1
ENTRYPOINT ["node"]
CMD ["dist/index.js"]
تعليمة HEALTHCHECK تُخبر Docker بكيفية تحديد ما إذا كانت الحاوية بصحة جيدة. Compose ومنسّقو Kubernetes يستخدمون هذه الحالة — حاوية تعمل لكن ترجع 500 ستُعلَّم كغير صحية ويمكن إعادة تشغيلها أو استبعادها من موازنة الحمل. دائماً نفّذ نقطة نهاية خفيفة /healthz في API الخاص بك وأشر إليها هنا.
الخطوة الثالثة: إعداد Nginx
يجلس Nginx أمام الـ API. يُعيد كتابة بادئة /api ويُعيد التوجيه إلى اسم المضيف app — DNS الداخلي لـ Docker يحل app إلى IP حاوية الـ API تلقائياً.
# nginx/default.conf
server {
listen 80;
server_name _;
# تمرير IP العميل الحقيقي إلى التطبيق الخلفي
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
location /api/ {
# حذف بادئة /api قبل التوجيه
rewrite ^/api(/.*)$ $1 break;
proxy_pass http://app:3000;
proxy_http_version 1.1;
proxy_set_header Connection ""; # تفعيل keep-alive للخادم الخلفي
proxy_read_timeout 30s;
}
# نقطة نهاية الصحة لفحوصات موازن الحمل (لا تُوجَّه — يُعالجها Nginx مباشرة)
location /nginx-health {
access_log off;
return 200 "ok\n";
}
}
الخطوة الرابعة: Docker Compose — ملف الإنتاج
ملف Compose يُربط الخدمات الثلاث معاً، يُعرّف الشبكة الخاصة، يُعلن وحدة التخزين المُسمّاة، ويُطبّق ترتيب بدء التشغيل عبر فحوصات الصحة — وليس مجرد depends_on بدون شرط، الذي ينتظر فقط بدء تشغيل الحاوية دون أن ينتظر استعداد الخدمة داخلها.
لا تُضمّن كلمات مرور قاعدة البيانات مباشرة في docker-compose.yml. استخدم ملف .env (الذي يجب أن يكون في .gitignore) لتزويد DB_PASSWORD والأسرار المماثلة. في pipelines CI/CD، احقنها كمتغيرات بيئة محمية من مدير الأسرار الخاص بك (GitHub Actions secrets، Vault، AWS Secrets Manager). بيانات اعتماد مسرّبة في تاريخ مستودع عام هي حادث أمني، وليست مجرد إزعاج.
الخطوة الخامسة: تجاوزات التطوير
ملف docker-compose.yml الأساسي مصمم للإنتاج. للتطوير المحلي، استخدم ملف تجاوز يضيف إعادة التحميل الفوري ويتخطى البناء متعدد المراحل:
# docker-compose.override.yml (للتطوير فقط — لا تُنشره)
services:
app:
build:
target: builder # استخدم مرحلة builder مع devDependencies
command: ["node", "--watch", "src/index.js"]
volumes:
- ./src:/app/src:ro # تحميل المصدر لإعادة التحميل الفوري
environment:
NODE_ENV: development
db:
ports:
- "5432:5432" # كشف Postgres للمضيف لأدوات محلية
يُدمج Compose ملف docker-compose.override.yml تلقائياً عند تشغيل docker compose up محلياً. في CI والإنتاج، مرّر -f docker-compose.yml صراحةً حتى لا يُطبَّق ملف التجاوز أبداً.
الخطوة السادسة: تشغيل المجموعة
# التشغيل الأول: بناء الصور وتشغيل جميع الخدمات
docker compose up --build -d
# متابعة سجلات جميع الخدمات
docker compose logs -f
# التحقق من حالة صحة كل حاوية
docker compose ps
# تشغيل ترحيل قاعدة البيانات (مثال باستخدام حاوية مؤقتة)
docker compose exec app node dist/migrate.js
# إعادة بناء صورة التطبيق فقط بعد تغيير الكود ثم إعادة تشغيل بدون توقف
docker compose build app
docker compose up -d --no-deps app
# إيقاف التشغيل (وحدات التخزين محفوظة)
docker compose down
# إيقاف التشغيل وحذف جميع البيانات (تدميري — للتطوير فقط)
docker compose down -v
علم --no-deps في docker compose up يُعيد تشغيل الخدمة المحددة فقط دون المساس باعتمادياتها. هذا هو نمط نشر صورة API جديدة دون إعادة تشغيل قاعدة البيانات — ضروري عندما لا يمكنك تحمّل توقف طبقة الاستمرارية.
أنماط فشل الإنتاج التي يجب معرفتها
حاوية تبدأ ليست نفس خدمة جاهزة. أكثر ثلاثة حالات فشل إنتاجية شيوعاً عند احتواء تطبيق لأول مرة:
حالة السباق عند الإقلاع — يبدأ التطبيق ويحاول فوراً الاتصال بقاعدة البيانات قبل أن تُكمل Postgres تسلسل تهيئتها. الحل: استخدم depends_on: condition: service_healthy مع healthcheck حقيقي على خدمة db، كما هو موضح أعلاه.
معالجة إشارات PID 1 — Node.js لا يعالج SIGTERM افتراضياً إذا كان PID 1 في حاوية. سيُقتل قسراً بعد مهلة الإيقاف (افتراضي 10 ثوان). الحل: استخدم شكل exec ENTRYPOINT ["node", "dist/index.js"] وسجّل معالج process.on("SIGTERM") في التطبيق لاستنزاف الطلبات الجارية قبل الخروج.
الأسرار في طبقات الصورة — تمرير الأسرار كـ ARG أو ENV في وقت البناء يُضمّنها في الصورة وتكون مرئية عبر docker history وdocker inspect. الحل: مرّر الأسرار في وقت التشغيل كمتغيرات بيئة (عبر .env أو مدير الأسرار)، وليس في وقت البناء.
ملخص
احتواء تطبيق بشكل صحيح يعني ثلاثة أشياء تعمل معاً: Dockerfile مُحصَّن للإنتاج (متعدد المراحل، غير جذري، HEALTHCHECK، entrypoint بشكل exec)، وملف Compose يُطبّق ترتيب بدء التشغيل الصحيح ويفصل الأسرار عن الإعدادات، ونظافة تشغيلية حول وحدات التخزين والشبكات وملفات التجاوز. ابنِ هذا مرة واحدة وسيصبح القالب القابل للتكرار لكل خدمة يُشحنها فريقك.
نستخدم ملفات تعريف الارتباط لتشغيل هذا الموقع وتحليل الزيارات وعرض إعلانات مخصّصة. يمكنك قبول كل ملفات تعريف الارتباط أو رفض غير الأساسية منها.
سياسة الخصوصية