EduFlow

الوحدة 1 · الـ Containers من المبادئ الأولى

أول Dockerfile لك

20 دقراءة 4 داكتشف الخطأ

الدرس في لمحة

اكتب Dockerfile يعمل لـ notes-api، وابنِه إلى image لها tag، وشغّله مع منفذ منشور، واعرف وظيفة كل تعليمة فيه.

  1. 1التطبيق الذي تغلّفه
  2. 2أول Dockerfile
  3. 3البناء والتشغيل
  4. 4صيغة exec، ولماذا تهمّ
  5. 5انظر إلى ما بنيته

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

تستطيع أن تصف بيئة تشغيل notes-api في جملة واحدة: «Node.js 24، وكودنا المترجَم، واعتمادياتنا، والتشغيل بالأمر node dist/server.js على المنفذ 3000». الـ Dockerfile هو هذه الجملة مكتوبة بطريقة تسمح لآلة بتكرارها حرفيًا، على أي جهاز أو خادم بناء، وإلى الأبد.

التطبيق الذي تغلّفه

المستودع يبدو هكذا:

Text
notes-api/
  package.json
  package-lock.json
  tsconfig.json
  src/
    server.ts
    db.ts

الأمر npm run build يترجم src/ إلى dist/ باستخدام tsc، والأمر npm start يشغّل node dist/server.js. هذا هو الجزء من الخادم الذي يهمّنا في سياق الـ containers:

src/server.ts
import express from 'express';
import { pool } from './db.js';

const app = express();
app.use(express.json());

app.get('/healthz', (_req, res) => res.send('ok'));
app.get('/readyz', async (_req, res) => {
  try {
    await pool.query('SELECT 1');
    res.send('ready');
  } catch {
    res.status(503).send('database unreachable');
  }
});
// …notes routes

const port = Number(process.env.PORT ?? 3000);
app.listen(port, '0.0.0.0', () => console.log(`notes-api listening on ${port}`));

لاحظ '0.0.0.0'، واحفظها في ذهنك؛ سنعود إليها.

أول Dockerfile

أنشئ ملفًا باسم Dockerfile (بحرف D كبير، ودون امتداد) في جذر المستودع:

Dockerfile
# syntax=docker/dockerfile:1
FROM node:24-slim
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/server.js"]

سطرًا بسطر:

  • # syntax=docker/dockerfile:1 يثبّت الـ frontend الخاص بالـ Dockerfile على أحدث إصدار من 1.x، فتحصل على الميزات الحديثة مثل أسرار البناء والـ cache mounts.
  • FROM node:24-slim يبدأ من الـ image الرسمية لـ Node.js 24 فوق أساس Debian مقلّص. كل image تبدأ من image أخرى.
  • WORKDIR /app ينشئ /app إن لزم، ويجعله مجلد العمل لكل تعليمة بعده، وللـ container أثناء التشغيل أيضًا.
  • COPY . . ينسخ الـ build context إلى /app.
  • RUN npm ci يثبّت بالضبط ما يقوله package-lock.json. استخدم ci لا install داخل الـ images: فهو يفشل إذا تعارض الـ lockfile مع package.json بدلًا من أن يعيد كتابة الـ lockfile بصمت.
  • RUN npm run build يترجم TypeScript إلى dist/.
  • EXPOSE 3000 يسجّل أن التطبيق يستمع على المنفذ 3000، لكنه لا ينشر شيئًا.
  • CMD هو الأمر الافتراضي حين يبدأ الـ container.

لماذا node:24-slim؟ لأن Node.js 24 خط دعم طويل الأمد تصله إصلاحات أمنية حتى 2028، والـ image الرسمية يُعاد بناؤها كلما أصدر Node أو Debian تصحيحًا. نسخة -slim تستبعد المترجِمات وصفحات الـ man وعشرات الحزم التي تضمّها الـ image الكاملة node:24 لبناء الوحدات الأصلية (native modules). لا تحتاج إليها لتشغيل JavaScript، وكل حزمة لا تشحنها هي حزمة لن يشتكي منها ماسح الثغرات. وإن احتاجت إحدى الاعتماديات يومًا إلى ترجمة كود أصلي، فستتعامل مع ذلك في build stage منفصلة في القسم القادم.

هذا الملف يعمل، لكن فيه مشكلة أداء ستصلحها في الدرس التالي. شغّله أولًا.

البناء والتشغيل

Terminal
docker build -t notes-api:dev .
docker run --rm -p 3000:3000 notes-api:dev

الخيار -t notes-api:dev يسمّي الـ image ‏(repository:tag). والنقطة . في النهاية هي الـ build context (سياق البناء): المجلد الذي يسلّمه Docker إلى أداة البناء. لا تستطيع COPY قراءة إلا الملفات الموجودة داخله، ولهذا يفشل COPY ../shared ..

الأمر docker run ينشئ container من الـ image. الخيار --rm يحذفه عند توقّفه، فلا تتراكم لديك containers ميتة. والخيار -p 3000:3000 يربط المنفذ 3000 على جهازك بالمنفذ 3000 داخل الـ container. في terminal آخر:

Terminal
curl localhost:3000/healthz

تحصل على ok. أما /readyz فيُرجع 503 لأنه لا توجد قاعدة بيانات بعد؛ وسيحلّ Compose ذلك في القسم الثالث.

صيغة exec، ولماذا تهمّ

للتعليمة CMD صيغتان. صيغة exec، ‏CMD ["node", "dist/server.js"]، تشغّل node مباشرة كـ PID 1. وصيغة shell، ‏CMD node dist/server.js، تشغّل /bin/sh -c "node dist/server.js"، ومرجع الـ Dockerfile يحذّر من أن الأمر المُشغَّل بهذه الطريقة لن يستقبل إشارات Unix. حين يطلب Docker أو Kubernetes من تطبيقك أن يتوقّف، يرسل SIGTERM إلى PID 1. فإذا كان PID 1 هو shell لا يمرّر الإشارة، فلن يسمعها تطبيقك أبدًا، وسيُقتل قسرًا بعد انتهاء المهلة، ويُسقط الطلبات التي كان يخدمها.

وCMD npm start هي المشكلة نفسها مع عملية إضافية في السلسلة. شغّل node مباشرة.

انظر إلى ما بنيته

Terminal
docker image ls notes-api
docker image inspect notes-api:dev --format '{{.Config.Cmd}} {{.Config.ExposedPorts}}'

الـ CMD قيمة افتراضية فقط. أي شيء تكتبه بعد اسم الـ image يحلّ محلّها، وهذا مفيد للفحوص السريعة:

Terminal
docker run --rm notes-api:dev node --version
docker run --rm notes-api:dev ls dist

هناك أيضًا ENTRYPOINT، التي تثبّت الملف التنفيذي وتعامل CMD كمعاملاته الافتراضية. بعض الـ images تستخدمها (ستقابل في القسم القادم image نقطة دخولها هي node نفسه). أما في images تطبيقاتك أنت، فتعليمة CMD عادية بصيغة exec هي أبسط حلّ يؤدّي الغرض، وتُبقي استبدالها سهلًا كما في الأمثلة أعلاه.

حجم الـ image بضع مئات من الميغابايتات: Node.js، وأساس Debian، واعتماديات التطوير (بما فيها TypeScript)، والكود المصدري. القسم الثاني سيقلّص ذلك. لكن قبل هذا، كل تغيير في server.ts يعيد حاليًا تثبيت كل الاعتماديات، وهذا موضوع الدرس التالي.

أهم النقاط

  • الـ Dockerfile وصفة من التعليمات؛ وكل FROM وCOPY وRUN تنتج layer من نظام الملفات داخل الـ image.
  • الـ build context هو المجلد الذي تمرّره إلى docker build، وCOPY لا ترى إلا الملفات الموجودة داخله.
  • التعليمة EXPOSE توثّق المنفذ فقط؛ أما الخيار -p host:container في docker run فهو ما يجعله قابلًا للوصول.
  • اكتب CMD بصيغة exec ‏(["node", "dist/server.js"]) حتى يكون تطبيقك هو PID 1 ويستقبل إشارات الإيقاف.
  • الخادم داخل الـ container يجب أن يستمع على 0.0.0.0 لا على 127.0.0.1، وإلا فلن تصل إليه المنافذ المنشورة.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

تشغّل docker build -t notes-api:dev . من جذر المستودع. ماذا تعني النقطة . في النهاية؟

السؤال 2 من 4

في الـ Dockerfile السطر EXPOSE 3000. تشغّل docker run notes-api:dev ثم يفشل curl localhost:3000 على جهازك. لماذا؟

السؤال 3 من 4

أي CMD تسمح لعملية Node.js باستقبال SIGTERM مباشرة عند إيقاف الـ container؟

السؤال 4 من 4

يكتب notes-api في سجلّه listening on 127.0.0.1:3000 داخل الـ container. نشرت المنفذ بـ -p 3000:3000، لكن كل طلب يحصل على connection reset. ما الحل؟

تدريب

اكتشف الخطأ

راجع أول Dockerfile كتبه زميلك

هذا الـ Dockerfile يُبنى بنجاح، لكن الملفات تنتهي في المكان الخطأ، والتطبيق يُقتل قسرًا في كل مرة يتوقّف فيها الـ container. اختر السطرين الاثنين المسؤولين عن هاتين المشكلتين.

حدّد السطر أو الأسطر التي تظنها خاطئة، ثم تحقّق.