الوحدة 1 · الـ Containers من المبادئ الأولى
أول Dockerfile لك
20 دقراءة 4 داكتشف الخطأ
الدرس في لمحة
اكتب Dockerfile يعمل لـ notes-api، وابنِه إلى image لها tag، وشغّله مع منفذ منشور، واعرف وظيفة كل تعليمة فيه.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
تستطيع أن تصف بيئة تشغيل notes-api في جملة واحدة: «Node.js 24، وكودنا المترجَم، واعتمادياتنا، والتشغيل بالأمر node dist/server.js على المنفذ 3000». الـ Dockerfile هو هذه الجملة مكتوبة بطريقة تسمح لآلة بتكرارها حرفيًا، على أي جهاز أو خادم بناء، وإلى الأبد.
التطبيق الذي تغلّفه
المستودع يبدو هكذا:
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:
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 كبير، ودون امتداد) في جذر المستودع:
# 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 منفصلة في القسم القادم.
هذا الملف يعمل، لكن فيه مشكلة أداء ستصلحها في الدرس التالي. شغّله أولًا.
البناء والتشغيل
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 آخر:
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 مباشرة.
انظر إلى ما بنيته
docker image ls notes-api
docker image inspect notes-api:dev --format '{{.Config.Cmd}} {{.Config.ExposedPorts}}'الـ CMD قيمة افتراضية فقط. أي شيء تكتبه بعد اسم الـ image يحلّ محلّها، وهذا مفيد للفحوص السريعة:
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، وإلا فلن تصل إليه المنافذ المنشورة.