أطلق واربح من عملك
النشر للإنتاج
5 دقيقة للقراءة
Taking your app from development to production requires proper configuration, security, and monitoring. We'll deploy using Vercel for frontend and configure Docker for services that need custom infrastructure.
Environment Configuration
// env.ts
import { z } from 'zod';
const envSchema = z.object({
// App
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
NEXT_PUBLIC_APP_URL: z.string().url(),
// Database
DATABASE_URL: z.string().url(),
DATABASE_POOL_SIZE: z.coerce.number().default(10),
// Auth
AUTH_SECRET: z.string().min(32),
AUTH_GOOGLE_ID: z.string().optional(),
AUTH_GOOGLE_SECRET: z.string().optional(),
// Payments
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'),
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith('pk_'),
// Services
REDIS_URL: z.string().url().optional(),
RESEND_API_KEY: z.string().optional(),
// Feature flags
ENABLE_ANALYTICS: z.coerce.boolean().default(true),
ENABLE_RATE_LIMITING: z.coerce.boolean().default(true),
});
// Validate at build time
export const env = envSchema.parse(process.env);
// Type-safe environment access
declare global {
namespace NodeJS {
interface ProcessEnv extends z.infer<typeof envSchema> {}
}
}
Vercel Deployment
// vercel.json
{
"buildCommand": "pnpm build",
"installCommand": "pnpm install",
"framework": "nextjs",
"regions": ["iad1", "sfo1"],
"functions": {
"app/api/**/*.ts": {
"maxDuration": 30
}
},
"crons": [
{
"path": "/api/cron/daily-cleanup",
"schedule": "0 0 * * *"
},
{
"path": "/api/cron/send-reports",
"schedule": "0 9 * * 1"
}
],
"headers": [
{
"source": "/api/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "no-store, max-age=0" }
]
},
{
"source": "/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "X-XSS-Protection", "value": "1; mode=block" }
]
}
]
}
GitHub Actions CI/CD
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
jobs:
lint-and-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run linter
run: pnpm lint
- name: Type check
run: pnpm type-check
- name: Run migrations
run: pnpm db:migrate
- name: Run tests
run: pnpm test
- name: Build
run: pnpm build
deploy-preview:
needs: lint-and-test
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to Vercel Preview
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
deploy-production:
needs: lint-and-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to Vercel Production
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
Docker Configuration
# Dockerfile
FROM node:20-alpine AS base
# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
# Dependencies stage
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Build stage
FROM base AS builder
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
# Generate Prisma client if using Prisma
# RUN pnpm prisma generate
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
RUN pnpm build
# Production stage
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy built assets
COPY /app/public ./public
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/app
- REDIS_URL=redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
db:
image: postgres:16-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
redis:
image: redis:alpine
volumes:
- redis_data:/data
command: redis-server --appendonly yes
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- app
restart: unless-stopped
volumes:
postgres_data:
redis_data:
Database Migrations
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
import { env } from './env';
export default defineConfig({
schema: './lib/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: env.DATABASE_URL,
},
verbose: true,
strict: true,
});
// package.json scripts
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"db:seed": "tsx scripts/seed.ts"
}
}
// scripts/migrate.ts
// Safe migration script for production
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { Pool } from 'pg';
async function runMigrations() {
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 1, // Single connection for migrations
});
const db = drizzle(pool);
console.log('Running migrations...');
try {
await migrate(db, { migrationsFolder: './drizzle' });
console.log('Migrations completed successfully');
} catch (error) {
console.error('Migration failed:', error);
process.exit(1);
} finally {
await pool.end();
}
}
runMigrations();
Health Checks
// app/api/health/route.ts
import { db } from '@/lib/db';
import { sql } from 'drizzle-orm';
import { NextResponse } from 'next/server';
export async function GET() {
const checks: Record<string, 'healthy' | 'unhealthy'> = {};
// Database check
try {
await db.execute(sql`SELECT 1`);
checks.database = 'healthy';
} catch {
checks.database = 'unhealthy';
}
// Redis check (if used)
try {
const redis = await import('@/lib/redis').then((m) => m.redis);
await redis.ping();
checks.redis = 'healthy';
} catch {
checks.redis = 'unhealthy';
}
const isHealthy = Object.values(checks).every((v) => v === 'healthy');
return NextResponse.json(
{
status: isHealthy ? 'healthy' : 'unhealthy',
timestamp: new Date().toISOString(),
checks,
},
{ status: isHealthy ? 200 : 503 }
);
}
Error Monitoring with Sentry
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
integrations: [
Sentry.replayIntegration({
maskAllText: true,
blockAllMedia: true,
}),
],
});
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config');
}
if (process.env.NEXT_RUNTIME === 'edge') {
await import('./sentry.edge.config');
}
}
Rate Limiting
// lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const redis = new Redis({
url: process.env.UPSTASH_REDIS_URL!,
token: process.env.UPSTASH_REDIS_TOKEN!,
});
// Different rate limiters for different use cases
export const rateLimiters = {
// General API: 100 requests per minute
api: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(100, '1 m'),
prefix: 'ratelimit:api',
}),
// Auth endpoints: 5 attempts per minute
auth: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(5, '1 m'),
prefix: 'ratelimit:auth',
}),
// AI endpoints: 20 requests per minute
ai: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(20, '1 m'),
prefix: 'ratelimit:ai',
}),
};
// Middleware helper
export async function checkRateLimit(
identifier: string,
type: keyof typeof rateLimiters = 'api'
) {
const { success, limit, reset, remaining } = await rateLimiters[type].limit(identifier);
return {
success,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
},
};
}
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { checkRateLimit } from './lib/rate-limit';
export async function middleware(request: NextRequest) {
// Skip rate limiting for static assets
if (request.nextUrl.pathname.startsWith('/_next')) {
return NextResponse.next();
}
// Get identifier (IP or user ID)
const ip = request.ip ?? request.headers.get('x-forwarded-for') ?? 'anonymous';
// Check rate limit for API routes
if (request.nextUrl.pathname.startsWith('/api')) {
const type = request.nextUrl.pathname.startsWith('/api/auth') ? 'auth' : 'api';
const { success, headers } = await checkRateLimit(ip, type);
if (!success) {
return new NextResponse('Too Many Requests', {
status: 429,
headers,
});
}
const response = NextResponse.next();
Object.entries(headers).forEach(([key, value]) => {
response.headers.set(key, value);
});
return response;
}
return NextResponse.next();
}
export const config = {
matcher: ['/api/:path*'],
};
What You've Learned
In this lesson, you've configured:
- Environment validation - Type-safe env vars with Zod
- Vercel deployment - Configuration, crons, and headers
- CI/CD pipeline - GitHub Actions for testing and deployment
- Docker setup - Multi-stage builds and compose configuration
- Production essentials - Health checks, monitoring, rate limiting
Proper deployment configuration prevents production issues and makes your app reliable and scalable.
النشر للإنتاج
نقل تطبيقك من التطوير للإنتاج يتطلب تكويناً وأماناً ومراقبة مناسبة. سننشر باستخدام Vercel للواجهة الأمامية ونكوّن Docker للخدمات التي تحتاج بنية تحتية مخصصة.
تكوين البيئة
// env.ts
import { z } from 'zod';
const envSchema = z.object({
// التطبيق
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
NEXT_PUBLIC_APP_URL: z.string().url(),
// قاعدة البيانات
DATABASE_URL: z.string().url(),
DATABASE_POOL_SIZE: z.coerce.number().default(10),
// المصادقة
AUTH_SECRET: z.string().min(32),
AUTH_GOOGLE_ID: z.string().optional(),
AUTH_GOOGLE_SECRET: z.string().optional(),
// المدفوعات
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'),
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith('pk_'),
// الخدمات
REDIS_URL: z.string().url().optional(),
RESEND_API_KEY: z.string().optional(),
// أعلام الميزات
ENABLE_ANALYTICS: z.coerce.boolean().default(true),
ENABLE_RATE_LIMITING: z.coerce.boolean().default(true),
});
// التحقق في وقت البناء
export const env = envSchema.parse(process.env);
// الوصول الآمن للأنواع للبيئة
declare global {
namespace NodeJS {
interface ProcessEnv extends z.infer<typeof envSchema> {}
}
}
نشر Vercel
// vercel.json
{
"buildCommand": "pnpm build",
"installCommand": "pnpm install",
"framework": "nextjs",
"regions": ["iad1", "sfo1"],
"functions": {
"app/api/**/*.ts": {
"maxDuration": 30
}
},
"crons": [
{
"path": "/api/cron/daily-cleanup",
"schedule": "0 0 * * *"
},
{
"path": "/api/cron/send-reports",
"schedule": "0 9 * * 1"
}
],
"headers": [
{
"source": "/api/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "no-store, max-age=0" }
]
},
{
"source": "/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "X-XSS-Protection", "value": "1; mode=block" }
]
}
]
}
GitHub Actions CI/CD
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
jobs:
lint-and-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: تثبيت الاعتماديات
run: pnpm install --frozen-lockfile
- name: تشغيل المدقق
run: pnpm lint
- name: فحص الأنواع
run: pnpm type-check
- name: تشغيل الترحيلات
run: pnpm db:migrate
- name: تشغيل الاختبارات
run: pnpm test
- name: البناء
run: pnpm build
deploy-preview:
needs: lint-and-test
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: النشر لمعاينة Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
deploy-production:
needs: lint-and-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: النشر لإنتاج Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
تكوين Docker
# Dockerfile
FROM node:20-alpine AS base
# تثبيت pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
# مرحلة الاعتماديات
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# مرحلة البناء
FROM base AS builder
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
# توليد عميل Prisma إذا كنت تستخدم Prisma
# RUN pnpm prisma generate
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
RUN pnpm build
# مرحلة الإنتاج
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# نسخ الأصول المبنية
COPY /app/public ./public
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/app
- REDIS_URL=redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
db:
image: postgres:16-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
redis:
image: redis:alpine
volumes:
- redis_data:/data
command: redis-server --appendonly yes
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- app
restart: unless-stopped
volumes:
postgres_data:
redis_data:
ترحيلات قاعدة البيانات
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
import { env } from './env';
export default defineConfig({
schema: './lib/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: env.DATABASE_URL,
},
verbose: true,
strict: true,
});
// package.json scripts
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"db:seed": "tsx scripts/seed.ts"
}
}
// scripts/migrate.ts
// سكربت ترحيل آمن للإنتاج
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { Pool } from 'pg';
async function runMigrations() {
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 1, // اتصال واحد للترحيلات
});
const db = drizzle(pool);
console.log('تشغيل الترحيلات...');
try {
await migrate(db, { migrationsFolder: './drizzle' });
console.log('اكتملت الترحيلات بنجاح');
} catch (error) {
console.error('فشل الترحيل:', error);
process.exit(1);
} finally {
await pool.end();
}
}
runMigrations();
فحوصات الصحة
// app/api/health/route.ts
import { db } from '@/lib/db';
import { sql } from 'drizzle-orm';
import { NextResponse } from 'next/server';
export async function GET() {
const checks: Record<string, 'healthy' | 'unhealthy'> = {};
// فحص قاعدة البيانات
try {
await db.execute(sql`SELECT 1`);
checks.database = 'healthy';
} catch {
checks.database = 'unhealthy';
}
// فحص Redis (إذا استُخدم)
try {
const redis = await import('@/lib/redis').then((m) => m.redis);
await redis.ping();
checks.redis = 'healthy';
} catch {
checks.redis = 'unhealthy';
}
const isHealthy = Object.values(checks).every((v) => v === 'healthy');
return NextResponse.json(
{
status: isHealthy ? 'healthy' : 'unhealthy',
timestamp: new Date().toISOString(),
checks,
},
{ status: isHealthy ? 200 : 503 }
);
}
مراقبة الأخطاء مع Sentry
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
integrations: [
Sentry.replayIntegration({
maskAllText: true,
blockAllMedia: true,
}),
],
});
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config');
}
if (process.env.NEXT_RUNTIME === 'edge') {
await import('./sentry.edge.config');
}
}
تحديد المعدل
// lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const redis = new Redis({
url: process.env.UPSTASH_REDIS_URL!,
token: process.env.UPSTASH_REDIS_TOKEN!,
});
// محددات معدل مختلفة لحالات استخدام مختلفة
export const rateLimiters = {
// API عام: 100 طلب في الدقيقة
api: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(100, '1 m'),
prefix: 'ratelimit:api',
}),
// نقاط المصادقة: 5 محاولات في الدقيقة
auth: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(5, '1 m'),
prefix: 'ratelimit:auth',
}),
// نقاط AI: 20 طلب في الدقيقة
ai: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(20, '1 m'),
prefix: 'ratelimit:ai',
}),
};
// مساعد الوسيط
export async function checkRateLimit(
identifier: string,
type: keyof typeof rateLimiters = 'api'
) {
const { success, limit, reset, remaining } = await rateLimiters[type].limit(identifier);
return {
success,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
},
};
}
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { checkRateLimit } from './lib/rate-limit';
export async function middleware(request: NextRequest) {
// تخطي تحديد المعدل للأصول الثابتة
if (request.nextUrl.pathname.startsWith('/_next')) {
return NextResponse.next();
}
// الحصول على المعرف (IP أو معرف المستخدم)
const ip = request.ip ?? request.headers.get('x-forwarded-for') ?? 'anonymous';
// فحص حد المعدل لمسارات API
if (request.nextUrl.pathname.startsWith('/api')) {
const type = request.nextUrl.pathname.startsWith('/api/auth') ? 'auth' : 'api';
const { success, headers } = await checkRateLimit(ip, type);
if (!success) {
return new NextResponse('طلبات كثيرة جداً', {
status: 429,
headers,
});
}
const response = NextResponse.next();
Object.entries(headers).forEach(([key, value]) => {
response.headers.set(key, value);
});
return response;
}
return NextResponse.next();
}
export const config = {
matcher: ['/api/:path*'],
};
ما تعلمته
في هذا الدرس، كوّنت:
- التحقق من البيئة - متغيرات بيئة آمنة الأنواع مع Zod
- نشر Vercel - التكوين والمهام المجدولة والترويسات
- خط أنابيب CI/CD - GitHub Actions للاختبار والنشر
- إعداد Docker - بناء متعدد المراحل وتكوين compose
- أساسيات الإنتاج - فحوصات الصحة والمراقبة وتحديد المعدل
تكوين النشر المناسب يمنع مشاكل الإنتاج ويجعل تطبيقك موثوقاً وقابلاً للتوسع.