NestJS — Node.js للمؤسسات

النشر: Docker والـ Monorepo والخوادم بلا خادم (Serverless)

16 دقيقة الدرس 48 من 48

النشر: Docker والـ Monorepo والخوادم بلا خادم (Serverless)

تنقّل تطبيق NestJS من حاسوبك إلى الإنتاج يتطلّب ثلاث مهارات مترابطة: تغليفه بشكل قابل للتكرار بـ Docker، وتنظيم قاعدة كود كبيرة على شكل Nest monorepo، ونشره اختياريًّا بوصفه دالة serverless على AWS Lambda. يستعرض هذا الدرس الثلاثة بأمثلة جاهزة للإنتاج.

Dockerfile متعدد المراحل

يُضمّن بناء Docker البسيط المرحلة الواحدة مجلد node_modules بالكامل — بما يشمل آلاف الاعتماديات التطويرية — في الصورة، مما يرفع حجمها إلى مئات الميغابايت. يستخدم البناء متعدد المراحل مراحل منفصلة للبناء والتشغيل حتى تحتوي الصورة النهائية على ما هو ضروري فقط:

# ---- مرحلة البناء ---- FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # ينتج dist/ # ---- مرحلة التشغيل ---- FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production COPY package*.json ./ RUN npm ci --omit=dev # اعتماديات الإنتاج فقط COPY --from=builder /app/dist ./dist EXPOSE 3000 CMD ["node", "dist/main"]
استخدم دائمًا npm ci، وليس npm install. يُثبّت npm ci ما هو موجود بالضبط في package-lock.json، مما يمنحك بناءً قابلًا للتكرار والتحديد في كل مرة. قد يُحدّث npm install بصمت إصدارات التصحيح.

تنسخ الصورة النهائية dist/ فقط وnode_modules للإنتاج من طبقة المُنشئ. والنتيجة عادةً أقل من 200 ميغابايت مقابل أكثر من 600 ميغابايت في حالة البناء البسيط.

إعداد الإنتاج

لا يُقبل أبدًا ترميز الأسرار أو عناوين URL بشكل ثابت. استخدم متغيرات البيئة وConfigModule الخاص بـ NestJS — المُهيَّأ مرة واحدة على مستوى الوحدة مع التحقق من الصحة عبر Joi أو مخطط class-validator:

// app.module.ts import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, validationSchema: Joi.object({ NODE_ENV: Joi.string().valid('development', 'production', 'test').required(), DATABASE_URL: Joi.string().uri().required(), JWT_SECRET: Joi.string().min(32).required(), PORT: Joi.number().default(3000), }), }), // ...وحدات أخرى ], }) export class AppModule {}
لا تلتزم ملفات .env أبدًا في نظام التحكم بالإصدار. خزّن الأسرار في مدير أسرار خط CI/CD الخاص بك (GitHub Actions Secrets، AWS Secrets Manager، إلخ) وأدخلها في وقت التشغيل كمتغيرات بيئة. ملف .env المُسرَّب في تاريخ git يصعب تطهيره بأمان.

مساحات عمل Nest monorepo

مع نمو مشروعك قد تحتاج إلى تطبيقات متعددة قابلة للنشر (مثل خادم API وعامل خلفي) تتشارك مكتبات مشتركة. تدعم واجهة CLI الخاصة بـ Nest وضع monorepo الذي يدير ذلك بملف واحد nest-cli.json:

  • apps/ — كل مجلد فرعي تطبيق مستقل قابل للنشر بملف main.ts الخاص به.
  • libs/ — كود مشترك (DTOs، أدوات مساعدة، نماذج قاعدة البيانات) يستورده أي تطبيق عبر مسارات مستعارة بأسلوب @app/shared.
  • بناء تطبيق محدد: nest build api — الناتج في dist/apps/api/.
  • التشغيل في وضع التطوير: nest start api --watch بالتوازي مع nest start worker --watch.
Monorepo مقابل Polyrepo. يُبقي monorepo كل الكود في مستودع git واحد، مما يجعل إعادة الهيكلة الذرية عبر التطبيقات أمرًا سهلًا. يتطلّب ذلك إعداد CI أكثر دقة (أعِد بناء التطبيقات المتغيرة فقط). بالنسبة للفِرَق الصغيرة، monorepo المنظَّم جيدًا هو الخيار الافتراضي الصحيح عادةً.

النشر بلا خادم على AWS Lambda

يُزيل Serverless الحاجة إلى إدارة مثيلات الخادم. يمكن تشغيل NestJS على AWS Lambda باستخدام محوّل @nestjs/platform-express أو @vendia/serverless-express، الذي يُغلّف تطبيق Express ويترجم أحداث Lambda إلى طلبات HTTP:

// lambda.ts (نقطة الدخول — تستبدل main.ts لبناءات Lambda) import { NestFactory } from '@nestjs/core'; import { ExpressAdapter } from '@nestjs/platform-express'; import serverlessExpress from '@vendia/serverless-express'; import * as express from 'express'; import { AppModule } from './app.module'; import { Handler } from 'aws-lambda'; let cachedServer: Handler; async function bootstrapLambda(): Promise<Handler> { const expressApp = express(); const adapter = new ExpressAdapter(expressApp); const nestApp = await NestFactory.create(AppModule, adapter); nestApp.enableCors(); await nestApp.init(); return serverlessExpress({ app: expressApp }); } export const handler: Handler = async (event, context) => { cachedServer = cachedServer ?? (await bootstrapLambda()); return cachedServer(event, context); };
خزّن مثيل الخادم في ذاكرة التخزين المؤقت. تُعيد Lambda استخدام نفس الحاوية عبر استدعاءات متعددة (التشغيل الدافئ). يُجنّب تخزين تطبيق NestJS المُهيَّأ في متغير على مستوى الوحدة إعادة تهيئة حاوية حقن الاعتماديات في كل طلب، مما يقلل زمن الاستجابة للتشغيل البارد بشكل كبير.

اختيار هدف النشر

  • حاويات (Docker + ECS / Kubernetes) — الأفضل للاتصالات طويلة الأمد وذات الحالة (WebSockets، SSE) وزمن الاستجابة المتوقع والتطبيقات الكبيرة.
  • بلا خادم (Lambda / Cloud Run) — الأفضل لواجهات API المدفوعة بالأحداث ذات حركة المرور المتفجرة، والتكاليف التشغيلية المنخفضة، والفوترة بحسب الطلب.
  • Monorepo — متعامد مع كليهما؛ يُحدد كيفية تنظيم الكود، لا مكان تشغيله.

الخلاصة

يجمع نشر NestJS الإنتاجي بين: Dockerfile متعدد المراحل يُبقي الصورة النهائية خفيفة بفصل البناء عن التشغيل، وConfigModule مع التحقق من الصحة بالمخطط لضمان وجود جميع متغيرات البيئة المطلوبة وأنواعها الصحيحة، وNest monorepo اختياري لمشاركة الكود بين تطبيقات متعددة عبر مجلدات apps/ وlibs/، ومحوّل serverless (@vendia/serverless-express) عند استهداف AWS Lambda مع بوتستراب مُخزَّن لتقليل زمن التشغيل البارد. اختَر هدف النشر بناءً على أنماط حركة المرور ومتطلبات التشغيل.