Docker والنشر السحابي
من جهازك إلى رابط يفتحه أي أحد ⏱ نحو 45 دقيقة.
- تشرح الصورة والحاوية ولماذا نستخدم
Docker. - تكتب
Dockerfileمتعدد المراحل لتطبيقReact + Express. - تشغّل عدة خدمات بـ
docker compose. - تجهّز الخادم للإنتاج وتنشره على منصة سحابية.
١«يعمل على جهازي!»
تطبيقك يعمل على جهازك لأن عليه إصدارًا معيّنًا من Node، ومكتبات مثبتة، ومتغيرات بيئة. على خادم آخر قد يختلف أي منها فيتعطل. الحاوية Container تحزم التطبيق مع كل ما يحتاجه (نظام تشغيل مصغّر، و Node، والمكتبات، والكود) في وحدة واحدة تعمل بالطريقة نفسها في أي مكان فيه Docker.
| المصطلح | المعنى | تشبيه |
|---|---|---|
| الصورة Image | قالب جاهز للقراءة فقط: النظام + Node + الكود | وصفة مطبوخة ومجمّدة |
| الحاوية Container | نسخة تعمل من الصورة | طبق مسخّن من الوصفة |
Dockerfile | تعليمات بناء الصورة خطوة بخطوة | الوصفة نفسها |
| السجل Registry | مستودع صور مثل Docker Hub | مكتبة الوصفات |
٢Dockerfile لتطبيقنا الكامل
تطبيقنا جزآن: واجهة React تُبنى بـ Vite، وخادم Express يقدّم الواجهة المبنية والـ API (كما في الدرس السابق). نستخدم بناءً متعدد المراحل: مرحلة فيها أدوات البناء، ثم صورة نهائية صغيرة فيها ما يلزم للتشغيل فقط:
shop/ ├── client/ ← مشروع Vite ├── server/ ← Express (server.js, routes/, models/) ├── Dockerfile └── .dockerignore
# ---------- المرحلة 1: بناء الواجهة ----------FROM node:24-alpine AS clientWORKDIR /app/clientCOPY client/package*.json ./RUN npm ci # تثبيت دقيق من package-lock.jsonCOPY client/ ./RUN npm run build # ← /app/client/dist # ---------- المرحلة 2: صورة التشغيل ----------FROM node:24-alpineWORKDIR /appENV NODE_ENV=productionCOPY server/package*.json ./RUN npm ci --omit=dev # مكتبات التشغيل فقطCOPY server/ ./COPY --from=client /app/client/dist ./distUSER node # لا تشغّل التطبيق بصلاحيات rootEXPOSE 3000HEALTHCHECK CMD wget -qO- http://localhost:3000/health || exit 1CMD ["node", "server.js"]
node_modules**/node_modulesclient/dist.env.git*.log
| التعليمة | وظيفتها |
|---|---|
FROM | الصورة الأساس: Node 24 (إصدار طويل الدعم LTS) على Alpine الصغير |
WORKDIR | مجلد العمل داخل الحاوية |
COPY package*.json ثم RUN npm ci | نسخ ملفات الاعتماديات أولًا: إن لم تتغير يُعاد استخدام طبقة التثبيت المخزّنة فيكون البناء أسرع |
ENV | متغير بيئة ثابت في الصورة |
EXPOSE | توثيق المنفذ الذي يستمع عليه التطبيق |
CMD | الأمر الذي يعمل عند تشغيل الحاوية |
٣البناء والتشغيل
$ docker build -t shop:1.0 .$ docker images shop$ docker run -d --name shop -p 8080:3000 -e JWT_SECRET=طويل-وعشوائي shop:1.0$ docker ps$ docker logs -f shop$ docker stop shop && docker rm shop
| الأمر | المعنى |
|---|---|
-t shop:1.0 | اسم الصورة ووسمها (الإصدار) |
-d | تشغيل في الخلفية |
-p 8080:3000 | منفذ جهازك 8080 ← منفذ الحاوية 3000؛ افتح http://localhost:8080 |
-e KEY=value | متغير بيئة للحاوية |
docker logs -f | متابعة ما يطبعه التطبيق (console.log) |
٤عدة خدمات معًا: docker compose
التطبيق الحقيقي يحتاج قاعدة بيانات أيضًا. ملف compose.yaml يصف الخدمات وشبكتها ومتغيراتها، ويشغّلها كلها بأمر واحد. وفي Sequelize لا يتغير إلا الاتصال: من SQLite إلى PostgreSQL:
services: app: build: . ports: ["8080:3000"] environment: DATABASE_URL: postgres://shop:secret@db:5432/shop JWT_SECRET: ${JWT_SECRET} depends_on: [db] db: image: postgres:17-alpine environment: POSTGRES_USER: shop POSTGRES_PASSWORD: secret POSTGRES_DB: shop volumes: ["dbdata:/var/lib/postgresql/data"] # البيانات تبقى بعد إيقاف الحاويةvolumes: dbdata:
// db.js في الإنتاجconst sequelize = process.env.DATABASE_URL ? new Sequelize(process.env.DATABASE_URL, { logging: false }) // PostgreSQL : new Sequelize({ dialect: 'sqlite', storage: 'dev.sqlite' }); // جهازك
$ docker compose up -d --build$ docker compose logs app$ docker compose down
٥خادم جاهز للإنتاج
قبل النشر يحتاج الخادم أربعة أشياء: أن يقرأ المنفذ من البيئة (المنصات تحدده هي)، وأن يعرف أنه في الإنتاج، وأن يوفّر مسار فحص صحة تسأله المنصة دوريًّا، وألا يكشف تفاصيل الأخطاء. جرّب الفرق بين وضعي التطوير والإنتاج في صفحة الخطأ الافتراضية:
const express = require('express');const app = express();const PORT = process.env.PORT || 3000; app.get('/health', (req, res) => { res.json({ status: 'ok', env: app.get('env'), uptime: Math.round(process.uptime()) });}); app.get('/boom', () => { throw new Error('تفاصيل داخلية سرية'); }); app.listen(PORT, () => console.log(`يعمل على ${PORT} في وضع ${app.get('env')}`));
شغّلنا الخادم هنا بالبيئة PORT=8080 و NODE_ENV=production (انظر الطرفية). في الإنتاج يرد Express على الخطأ بعبارة عامة «Internal Server Error» فقط ويطبع التفاصيل في السجل؛ أما إن نسيت ضبط NODE_ENV فيعمل في وضع التطوير ويعرض مكدّس الخطأ كاملًا للزائر.
| البند | لماذا |
|---|---|
PORT من البيئة | المنصة تحدد المنفذ |
NODE_ENV=production | أداء أفضل في Express وإخفاء تفاصيل الأخطاء |
/health | المنصة تعيد تشغيل الخدمة إن فشل |
| الأسرار في متغيرات المنصة | لا في الكود ولا في الصورة |
| قاعدة بيانات مُدارة | ملفات الحاوية تُمحى مع كل نشر |
| HTTPS | المنصات توفره تلقائيًّا بشهادة مجانية |
| السجلات Logs | console.log يظهر في لوحة المنصة |
٦النشر على منصة سحابية
منصات «المنصة كخدمة» PaaS تأخذ مستودع GitHub (أو Dockerfile) وتبنيه وتشغّله وتعطيك رابطًا بشهادة HTTPS. الخطوات العامة متشابهة:
- ارفع المشروع إلى GitHub (بلا .env ولا node_modules).
- أنشئ خدمة ويب جديدة في المنصة واربطها بالمستودع.
- حدّد أمر البناء والتشغيل، أو اختر أنها تُبنى من Dockerfile.
- أضف متغيرات البيئة:
NODE_ENVوJWT_SECRETوDATABASE_URL. - أنشئ قاعدة بيانات مُدارة
(PostgreSQL)من المنصة نفسها وانسخ عنوانها. - كل دفع push جديد إلى الفرع الرئيسي يعيد النشر تلقائيًّا.
| ما تنشره | خيارات شائعة | ملاحظة |
|---|---|---|
| الواجهة وحدها (ملفات dist) | Cloudflare Pages، Netlify، Vercel، GitHub Pages | استضافة ثابتة سريعة، وغالبًا مجانية للمشاريع الصغيرة |
| خادم Express (أو الكل معًا) | Render، Railway، Fly.io، أو خادم افتراضي VPS | الخطط المجانية محدودة وتتغير كثيرًا |
| قاعدة البيانات | PostgreSQL مُدارة من المنصة، أو Neon، أو Supabase | لا تعتمد على ملف SQLite داخل الحاوية |
توقّع وتحقّق
ما الفرق بين الصورة والحاوية؟
ما معنى -p 8080:3000 ؟
host:container.لماذا ننسخ package.json ونثبّت قبل نسخ بقية الكود؟
أين توضع JWT_SECRET عند النشر؟
الخلاصة
- الحاوية = التطبيق + كل ما يحتاجه؛ تعمل في أي مكان.
Dockerfile: FROMوWORKDIRوCOPYوRUNوCMD؛ الاعتماديات قبل الكود.- متعدد المراحل: بناء الواجهة ثم صورة تشغيل صغيرة.
composeيشغّل التطبيق وقاعدة البيانات معًا.- الإنتاج:
PORTوNODE_ENVمن البيئة، و /health، وأسرار في المنصة، وقاعدة بيانات مُدارة.
تحدٍّ برمجي: جاهز للإنتاج
متوسطخادم صغير كُتب للتطوير. عدّله ليصبح جاهزًا للنشر على منصة سحابية (يشغّله التحقق بالبيئة PORT=8080 و NODE_ENV=production).
- يستمع على
process.env.PORT(أو 3000 إن لم يوجد)، ويطبع عند البدءlistening onثم المنفذ. GET /healthيرد{"status": "ok", "uptime": رقم}.- السر
JWT_SECRETيُقرأ من البيئة، ولا يُكتب نصًّا في الكود. - وسيط أخطاء في النهاية يرد
{"error": "خطأ في الخادم"}بالحالة 500 دون كشف رسالة الخطأ الأصلية.
👀 اعرض الاستجابات المطلوبة
GET /healthGET /api/dataGET /api/fail
- الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
- يستمع على المنفذ من البيئة
(8080) - /health فيه
status = ok - … و uptime رقم
- السر يُقرأ من البيئة ويعمل
- الخطأ يرد برسالة عامة
- … ولا يكشف الرسالة الأصلية
- لا سر مكتوبًا في الكود
💡 تلميح 1
const PORT = process.env.PORT || 3000;💡 تلميح 2
حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
const express = require('express');const app = express();const SECRET = process.env.JWT_SECRET;const PORT = process.env.PORT || 3000; app.get('/health', (req, res) => res.json({ status: 'ok', uptime: process.uptime() }));app.get('/api/data', (req, res) => res.json({ items: [1, 2, 3], signedWith: SECRET.length }));app.get('/api/fail', () => { throw new Error('كلمة سر قاعدة البيانات: abc'); }); app.use((err, req, res, next) => { console.error(err.message); res.status(500).json({ error: 'خطأ في الخادم' });}); app.listen(PORT, () => console.log('listening on ' + PORT));