Next.js: Docker للنشر الذاتي والاستضافة
آخر تحديث: 2026-08-26
يمنحك النشر الذاتي تحكما كاملا في بيئة تشغيل تطبيقك — عندما تمنعك متطلبات الامتثال أو التكلفة أو الشبكة من استخدام منصة سحابية، يكون Docker هو شريكك الأكثر موثوقية.
1. ما ستتعلمه
- فهم حالات الاستخدام الأساسية للنشر الذاتي: الامتثال للبيانات / التحكم في التكلفة / النشر المحلي
- إعداد
next.config.jsفي وضع النشر المستقلoutput: 'standalone' - كتابة Dockerfile متعدد المراحل (التبعيات ← البناء ← التشغيل) لبناء صورة مصغرة
- استخدام Nginx كوكيل عكسي وخادم محتوى ثابت
- تنفيذ إدارة العمليات كخدمة وإعادة التشغيل التلقائي باستخدام PM2
- استخدام Docker Compose لتنسيق ثلاث حاويات: App و Nginx و PostgreSQL
2. قصة حقيقية لمهندس DevOps
(1) نقطة الألم: العميل يطلب عدم نقل البيانات خارج البلاد
يعمل تشارلي في شركة SaaS تخدم المؤسسات المالية في الشرق الأوسط. يحتاج منتجهم TaskFlow إلى النشر في مركز بيانات محلي في المملكة العربية السعودية — يطلب العميل تخزين جميع بيانات المستخدمين فعليا داخل المملكة العربية السعودية.
لكن Vercel ليس لديها مركز بيانات في المملكة العربية السعودية. المشكلة التي يواجهها تشارلي:
| المشكلة | التأثير |
|---|---|
| الامتثال لسيادة البيانات | المتطلبات التنظيمية المالية السعودية تمنع نقل البيانات للخارج |
| زمن انتقال الشبكة | زمن الوصول من الخوادم الأوروبية > 200 مللي ثانية |
| الارتباط بالمزود (Vendor Lock-in) | فاتورة Vercel الشهرية تتجاوز 2,000$ |
| متطلبات الشبكة الداخلية | يرغب العميل في نشر الحل على الشبكة الداخلية للمؤسسة |
(2) حلول النشر الذاتي باستخدام Docker
بنى تشارلي حزم نشر محمولة باستخدام Docker:
# ابني مرة واحدة، وشغل في كل مكان
docker build -t taskflow:latest .
docker run -p 3000:3000 \
-e DATABASE_URL="postgresql://..." \
-e AUTH_SECRET="..." \
taskflow:latest
(3) النتائج
| البعد | Vercel | النشر الذاتي مع Docker |
|---|---|---|
| موقع البيانات | مناطق Vercel فقط | أي مركز بيانات |
| التكاليف الشهرية | 2,000$+ | 300$ (الخادم) |
| زمن انتقال النشر | عالمي ~100 مللي ثانية | محلي < 20 مللي ثانية |
| الارتباط بالمزود | مرتفع | منخفض (قابل للترحيل) |
3. إعداد output: 'standalone'
ينشئ وضع output: 'standalone' في Next.js 16 خادم Node.js مستقلا يحتوي على جميع الملفات اللازمة لتشغيل التطبيق.
graph TB
A[next.config.js] --> B[output: 'standalone']
B --> C[عملية البناء]
C --> D[مجلد .next/standalone/]
D --> E[server.js — خادم HTTP مستقل]
D --> F[.next/static — موارد ثابتة]
D --> G[node_modules — تبعيات مصغرة]
D --> H[package.json — إعدادات الإدخال]
style A fill:#cce5ff
style D fill:#d4edda
(1) إعداد next.config.js
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
// التبعيات التي تتطلب معالجة خارجية
serverExternalPackages: ['@prisma/client'],
// تحسين بيئة الإنتاج
productionBrowserSourceMaps: false,
swcMinify: true,
// الاحتفاظ بتحسين الصور
images: {
unoptimized: false
}
}
module.exports = nextConfig
(2) هيكل مجلد المخرجات المستقلة
.next/standalone/
├── server.js # خادم HTTP مستقل (المدخل)
├── package.json # تعريفات تبعيات وقت التشغيل
├── node_modules/ # تبعيات البناء فقط
├── .next/
│ ├── server/ # كود جانب الخادم
│ ├── static/ # موارد ثابتة
│ ├── build-manifest.json
│ └── ...
├── public/ # موارد ثابتة عامة
└── trace # تتبع البناء
▶ مثال: التحقق من البناء المستقل
# بناء المشروع
npm run build
# عرض حجم مجلد standalone
du -sh .next/standalone/
# تشغيل خادم مخصص
node .next/standalone/server.js
# التحقق في طرفية أخرى
curl http://localhost:3000
.next/standalone/ 358M # الحجم الإجمالي
.next/standalone/server.js # ملف الإدخال (مولد تلقائيا)
المخرجات:
.next/standalone/ 358M
node .next/standalone/server.js
▲ Next.js 16.0.0
- Local: http://localhost:3000
✓ Ready in 1.2s
4. بناء Docker متعدد المراحل
يقسم البناء متعدد المراحل الصورة إلى ثلاث مراحل: تثبيت التبعيات ← بناء التطبيق ← بيئة تشغيل مصغرة.
# ============================================
# Dockerfile — Next.js 16 بناء متعدد المراحل
# ============================================
# --- المرحلة 1: تثبيت التبعيات ---
FROM node:20-alpine AS deps
LABEL stage=deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json pnpm-lock.yaml ./
RUN npm ci --only=production && \
npm cache clean --force
# --- المرحلة 2: البناء ---
FROM node:20-alpine AS build
LABEL stage=build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
RUN npm run build
# --- المرحلة 3: التشغيل ---
FROM node:20-alpine AS runner
LABEL stage=runner
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
WORKDIR /app
# نسخ مخرجات البناء
COPY --from=build --chown=nextjs:nodejs \
/app/.next/standalone ./
COPY --from=build --chown=nextjs:nodejs \
/app/.next/static ./.next/static
COPY --from=build --chown=nextjs:nodejs \
/app/public ./public
# فحص الصحة
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
ENV NODE_ENV=production
CMD ["node", "server.js"]
(1) البناء والتشغيل
# بناء الصورة
docker build -t taskflow:latest .
# عرض حجم الصورة
docker images taskflow:latest
# تشغيل الحاوية
docker run -d \
--name taskflow-app \
-p 3000:3000 \
-e DATABASE_URL="postgresql://user:pass@host:5432/taskflow" \
-e AUTH_SECRET="your-secret-key" \
-e NEXT_PUBLIC_API_URL="https://api.taskflow.local" \
--restart unless-stopped \
taskflow:latest
(2) مقارنة أحجام الصور عبر المراحل
| المرحلة | الصورة الأساسية | الحجم | المحتويات |
|---|---|---|---|
| deps | node:20-alpine | ~150 MB | node_modules + تبعيات النظام |
| build | node:20-alpine | ~450 MB | الكود المصدري + node_modules + مخرجات البناء |
| runner | node:20-alpine | ~358 MB | standalone + تبعيات الإنتاج |
| node:20-alpine bare | — | ~126 MB | النظام الأساسي |
▶ مثال: حقن متغيرات البيئة في Docker
المخرجات:
Sending build context to Docker daemon 4.096kB
Step 1/8 : FROM node:20-alpine AS deps
---> 1dd67de0f936
...
Successfully built a7b3c2d1e0f9
Successfully tagged shophub:latest
Docker command completed successfully.
d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4
# استخدام ملف .env لحقن متغيرات البيئة
cat > .env.production << EOF
DATABASE_URL=postgresql://user:pass@db:5432/taskflow
AUTH_SECRET=super-secret-key
NEXT_PUBLIC_API_URL=https://api.taskflow.local
NEXT_PUBLIC_POSTHOG_KEY=phc_xxxx
REDIS_URL=redis://redis:6379
EOF
docker run -d \
--name taskflow-app \
--env-file .env.production \
-p 3000:3000 \
--network taskflow-net \
taskflow:latest
المخرجات:
Docker operation completed.
5. Nginx كوكيل عكسي
يتولى Nginx إنهاء SSL والتخزين المؤقت للموارد الثابتة وموازنة التحميل، مما يجعله مكونا أساسيا في بيئات الإنتاج.
graph LR
A[متصفح المستخدم] --> B[Nginx :443]
B --> C{مطابقة المسار}
C -->|/_next/static/*| D[خدمة Nginx المباشرة<br/>تخزين مؤقت سنة]
C -->|/api/health| E[Next.js :3000]
C -->|/*| E
B --> F[إنهاء SSL<br/>Let's Encrypt]
style B fill:#cce5ff
style D fill:#d4edda
إعدادات Nginx
# nginx/nginx.conf
upstream nextjs_upstream {
server app:3000;
keepalive 64;
}
server {
listen 80;
server_name taskflow.local;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name taskflow.local;
# شهادة SSL
ssl_certificate /etc/nginx/ssl/taskflow.crt;
ssl_certificate_key /etc/nginx/ssl/taskflow.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# رؤوس الأمان
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# السجلات
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# تخزين مؤقت للموارد الثابتة (معالجة بواسطة Next.js)
location /_next/static/ {
proxy_pass http://nextjs_upstream;
expires 365d;
add_header Cache-Control "public, immutable";
}
location /static/ {
proxy_pass http://nextjs_upstream;
expires 30d;
add_header Cache-Control "public";
}
# نقاط نهاية فحص الصحة
location /api/health {
proxy_pass http://nextjs_upstream;
access_log off;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
# جميع الطلبات الأخرى توجه إلى Next.js
location / {
proxy_pass http://nextjs_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
}
6. مدير العمليات PM2
يضمن PM2 إعادة تشغيل عمليات Node.js تلقائيا بعد الانهيار ويوفر إدارة السجلات ووضع الكتلة (cluster mode).
(1) إعداد PM2
// ecosystem.config.js
module.exports = {
apps: [{
name: 'taskflow',
script: 'server.js',
cwd: '/app',
// وضع الكتلة (استخدام جميع أنوية CPU)
exec_mode: 'cluster',
instances: 'max',
// متغيرات البيئة
env: {
NODE_ENV: 'production',
PORT: 3000,
HOSTNAME: '0.0.0.0'
},
// إعدادات السجلات
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
error_file: '/var/log/pm2/taskflow-error.log',
out_file: '/var/log/pm2/taskflow-out.log',
merge_logs: true,
// إعادة التشغيل التلقائي
max_restarts: 10,
restart_delay: 1000,
min_uptime: 5000,
// مراقبة الذاكرة
max_memory_restart: '500M',
// فحص الصحة
listen_timeout: 3000,
kill_timeout: 5000
}]
}
(2) دمج PM2 مع Docker
# تثبيت PM2 في مرحلة runner
FROM node:20-alpine AS runner
RUN npm install -g pm2 && \
addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
WORKDIR /app
COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=build --chown=nextjs:nodejs /app/public ./public
COPY --chown=nextjs:nodejs ecosystem.config.js ./
USER nextjs
EXPOSE 3000
# استخدام PM2 لتفعيل وضع الكتلة
CMD ["pm2-runtime", "start", "ecosystem.config.js"]
▶ مثال: أوامر PM2 الشائعة
المخرجات:
Docker image built and container started successfully.
# عرض جميع العمليات
pm2 list
# عرض السجلات
pm2 logs taskflow
pm2 logs taskflow --lines 100
# مراقبة الموارد
pm2 monit
# إعادة التحميل (بدون توقف)
pm2 reload taskflow
# إيقاف/إعادة تشغيل
pm2 stop taskflow
pm2 restart taskflow
# حفظ قائمة العمليات الحالية
pm2 save
pm2 startup
المخرجات:
PM2 command executed successfully.
PM2 command executed successfully.
PM2 command executed successfully.
PM2 command executed successfully.
PM2 command executed successfully.
PM2 command executed successfully.
┌────┬──────────┬─────────┬─────────┐
│ id │ name │ status │ cpu │
├────┼──────────┼─────────┼─────────┤
│ 0 │ shophub │ online │ 0% │
└────┴──────────┴─────────┴─────────┘
PM2 command executed successfully.
┌────┬──────────┬─────────┬─────────┐
│ id │ name │ status │ cpu │
├────┼──────────┼─────────┼─────────┤
│ 0 │ shophub │ online │ 0% │
└────┴──────────┴─────────┴─────────┘
7. Docker Compose: تنسيق ثلاث حاويات
ينسق Docker Compose ثلاث حاويات — App و Nginx و PostgreSQL — لتشغيل البيئة بأكملها بنقرة واحدة.
# docker-compose.yml
version: '3.8'
networks:
taskflow-net:
driver: bridge
volumes:
postgres-data:
driver: local
nginx-logs:
driver: local
services:
# === 1. قاعدة بيانات PostgreSQL ===
db:
image: postgres:16-alpine
container_name: taskflow-db
restart: unless-stopped
networks:
- taskflow-net
volumes:
- postgres-data:/var/lib/postgresql/data
- ./db/init:/docker-entrypoint-initdb.d
environment:
POSTGRES_USER: taskflow
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: taskflow
healthcheck:
test: ["CMD-SHELL", "pg_isready -U taskflow"]
interval: 10s
timeout: 5s
retries: 5
ports:
- "5432:5432"
# === 2. تطبيق Next.js ===
app:
build:
context: .
dockerfile: Dockerfile
target: runner
image: taskflow:latest
container_name: taskflow-app
restart: unless-stopped
networks:
- taskflow-net
depends_on:
db:
condition: service_healthy
environment:
NODE_ENV: production
PORT: 3000
HOSTNAME: "0.0.0.0"
DATABASE_URL: postgresql://taskflow:${DB_PASSWORD}@db:5432/taskflow
AUTH_SECRET: ${AUTH_SECRET}
AUTH_URL: ${AUTH_URL}
NEXT_PUBLIC_API_URL: ${PUBLIC_API_URL}
NEXT_PUBLIC_POSTHOG_KEY: ${POSTHOG_KEY:-}
env_file:
- .env.production
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"]
interval: 30s
timeout: 3s
retries: 3
# === 3. Nginx كوكيل عكسي ===
nginx:
image: nginx:alpine
container_name: taskflow-nginx
restart: unless-stopped
networks:
- taskflow-net
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- nginx-logs:/var/log/nginx
depends_on:
app:
condition: service_healthy
(1) ملف متغيرات البيئة
# .env.production (لا ترفعه إلى Git)
DB_PASSWORD=StrongPassword123!
AUTH_SECRET=your-auth-secret-key-min-32-chars
AUTH_URL=https://auth.taskflow.local
PUBLIC_API_URL=https://api.taskflow.local
POSTHOG_KEY=phc_exampleKey123
(2) التشغيل والإدارة
# التشغيل الأول
docker compose up -d
# عرض السجلات
docker compose logs -f app
docker compose logs -f nginx
# إعادة بناء التطبيق
docker compose build app
docker compose up -d app
# تحديث ترحيل قاعدة البيانات
docker compose exec app npx prisma migrate deploy
# عرض حالة التشغيل
docker compose ps
# إيقاف جميع الخدمات
docker compose down
# تنظيف كامل (بما في ذلك وحدات التخزين)
docker compose down -v
▶ مثال: docker-compose.override.yml (بيئة التطوير)
المخرجات:
احفظ إعدادات YAML أعلاه في مسار الملف المحدد. ستصبح الإعدادات سارية عند إعادة تشغيل الخادم التالية.
# docker-compose.override.yml
version: '3.8'
services:
app:
build:
target: build # استخدام build بدلا من runner في مرحلة التطوير
environment:
NODE_ENV: development
volumes:
- ./src:/app/src:ro
- ./public:/app/public:ro
command: npm run dev # استخدام خادم التطوير
db:
ports:
- "5432:5432" # كشف منفذ قاعدة البيانات أثناء التطوير
nginx:
ports:
- "3000:80" # تبسيط تعيين المنافذ أثناء التطوير
المخرجات:
احفظ إعدادات YAML أعلاه في مسار الملف المحدد. ستصبح الإعدادات سارية عند إعادة تشغيل الخادم التالية.
8. حقن متغيرات البيئة في وقت التشغيل
يتم حقن متغيرات البيئة لحاويات Docker في وقت التشغيل، وليس أثناء عملية البناء — وهذا يسمح بنشر صورة واحدة إلى بيئات متعددة.
graph LR
A[Docker Build] --> B[الصورة<br/>(بدون متغيرات بيئة)]
B --> C[الحقن في وقت التشغيل]
C --> D[بيئة التطوير .env.dev]
C --> E[بيئة الاختبار .env.test]
C --> F[بيئة الإنتاج .env.prod]
D --> G[بدء تشغيل الحاوية]
E --> G
F --> G
G --> H[server.js يقرأ process.env]
style B fill:#cce5ff
style G fill:#d4edda
(1) متغيرات وقت البناء مقابل متغيرات وقت التشغيل
| نوع المتغير | توقيت الحقن | مثال | موقع التخزين |
|---|---|---|---|
| وقت البناء | docker build |
NEXT_PUBLIC_*، رقم الإصدار |
ARG في Dockerfile |
| وقت التشغيل | docker run |
DATABASE_URL، AUTH_SECRET |
env_file في docker compose |
| مختلط | مطلوب في كليهما | NEXT_PUBLIC_API_URL |
يحقن في الواجهة الأمامية وقت البناء، وفي الخلفية وقت التشغيل |
(2) حقن المتغيرات أثناء البناء
# استخدام ARG في Dockerfile لتمرير متغيرات وقت البناء
FROM node:20-alpine AS build
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ARG SENTRY_DSN
ENV SENTRY_DSN=$SENTRY_DSN
RUN npm run build
# تمرير المتغيرات أثناء البناء
docker build \
--build-arg NEXT_PUBLIC_API_URL=https://api.taskflow.com \
--build-arg SENTRY_DSN=https://xxx@sentry.io/123 \
-t taskflow:latest .
▶ مثال: سكريبت التحقق من متغيرات البيئة في وقت التشغيل
المخرجات:
Sending build context to Docker daemon 4.096kB
Step 1/8 : FROM node:20-alpine AS deps
---> 1dd67de0f936
...
Successfully built a7b3c2d1e0f9
Successfully tagged shophub:latest
// src/lib/env.ts
// التحقق من متغيرات البيئة في وقت التشغيل
function getRequiredEnvVar(name: string): string {
const value = process.env[name]
if (!value) {
throw new Error(`Missing required environment variable: ${name}`)
}
return value
}
export const env = {
databaseUrl: getRequiredEnvVar('DATABASE_URL'),
authSecret: getRequiredEnvVar('AUTH_SECRET'),
authUrl: process.env.AUTH_URL || 'http://localhost:3000',
publicApiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000',
posthogKey: process.env.NEXT_PUBLIC_POSTHOG_KEY,
nodeEnv: process.env.NODE_ENV || 'development',
isProduction: process.env.NODE_ENV === 'production',
port: parseInt(process.env.PORT || '3000', 10)
}
المخرجات:
Exports: env.
9. مثال كامل: نشر TaskFlow باستخدام Docker
# ============================================
# سكريبت النشر للإنتاج: deploy.sh
# الميزات: بناء ← ترحيل ← تشغيل ← فحص الصحة
# ============================================
#!/bin/bash
set -euo pipefail
echo "=== نشر TaskFlow للإنتاج ==="
# 1. تحميل متغيرات البيئة
if [ ! -f .env.production ]; then
echo "خطأ: .env.production غير موجود"
exit 1
fi
source .env.production
# 2. بناء صورة Docker
echo "جاري بناء صورة Docker..."
docker compose build app
# 3. تشغيل قاعدة البيانات (إذا لم تكن قيد التشغيل)
echo "جاري تشغيل قاعدة البيانات..."
docker compose up -d db
echo "في انتظار جاهزية قاعدة البيانات..."
sleep 5
# 4. تشغيل ترحيل قاعدة البيانات
echo "جاري تشغيل ترحيل قاعدة البيانات..."
docker compose run --rm app npx prisma migrate deploy
# 5. تشغيل التطبيق و Nginx
echo "جاري تشغيل التطبيق و Nginx..."
docker compose up -d app nginx
# 6. فحص الصحة
echo "جاري تشغيل فحص الصحة..."
for i in {1..10}; do
if curl -s -o /dev/null -w "%{http_code}" http://localhost:80/api/health | grep -q 200; then
echo "تم اجتياز فحص الصحة!"
break
fi
echo "في الانتظار... ($i/10)"
sleep 3
done
# 7. تنظيف الصور القديمة
echo "جاري تنظيف الصور القديمة..."
docker image prune -f
# 8. معلومات النشر
echo ""
echo "=== اكتمل النشر ==="
echo "التطبيق: http://localhost:80"
echo "API: http://localhost:80/api/health"
echo "قاعدة البيانات: postgresql://taskflow@localhost:5432/taskflow"
echo "السجلات: docker compose logs -f app"
echo "إعادة التشغيل: docker compose restart app"
// src/app/api/health/route.ts
// ============================================
// API فحص الصحة: يستدعى من Docker HEALTHCHECK
// ============================================
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET() {
const checks = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
checks: {} as Record<string, boolean>
}
try {
// التحقق من اتصال قاعدة البيانات
await prisma.$queryRaw`SELECT 1`
checks.checks.database = true
} catch {
checks.checks.database = false
checks.status = 'degraded'
}
try {
// التحقق من Redis (إذا تم إعداده)
// await redis.ping()
checks.checks.redis = true
} catch {
checks.checks.redis = false
if (!checks.checks.database) {
checks.status = 'unhealthy'
}
}
const statusCode = checks.status === 'healthy' ? 200 : 503
return NextResponse.json(checks, { status: statusCode })
}
❓ أسئلة شائعة
output: 'standalone' و output: 'export'؟standalone ينتج حزمة مستقلة تتضمن خادم Node.js وتدعم جميع ميزات Next.js، مثل SSR و ISR و API Routes. أما export فينتج HTML ثابتا خالصا (مع تعطيل SSR)، وهو مناسب للاستضافة على CDN. للنشر الذاتي مع Docker، يجب استخدام وضع standalone.restart: unless-stopped في Docker يعالج انهيارات مستوى الحاوية (مثل OOM)، بينما يعالج PM2 انهيارات مستوى عملية Node.js (مثل الاستثناءات غير الملتقطة). كما يوفر PM2 ميزات لا يوفرها Docker نفسه، مثل تدوير السجلات ووضع الكتلة وإعادة التشغيل بدون توقف.NEXT_PUBLIC_* أثناء البناء؟NEXT_PUBLIC_* تضمن في حزمة JS أثناء البناء ولا يمكن تعديلها في وقت التشغيل. للنشر الذاتي، الحل هو: (1) تمرير قيمة NEXT_PUBLIC_* أثناء البناء؛ (2) أو وضع المتغيرات التي تتطلب إعدادا في وقت التشغيل في استجابة API route (مثل /api/config)، والتي تسترجعها الواجهة الأمامية عبر fetch.📖 ملخص
- إعداد
next.config.jsبـoutput: 'standalone'هو شرط أساسي للنشر الذاتي مع Docker وينشئ خادم Node.js مستقلا - البناء متعدد المراحل لـ Docker (deps ← build ← runner) يضغط الصورة إلى ~358MB، مما يقلل تكاليف النقل والتخزين
- يعمل Nginx كوكيل عكسي، ويوفر إنهاء SSL والتخزين المؤقت الثابت وحقن رؤوس الأمان، مما يجعله أساسيا لبيئات الإنتاج
- يوفر PM2 إدارة العمليات كخدمة ووضع الكتلة وإعادة التشغيل بدون توقف وإدارة السجلات لتعزيز موثوقية التطبيق
- ينسق Docker Compose ثلاث حاويات: App و Nginx و PostgreSQL؛
docker compose up -dتشغيل بنقرة واحدة - يتم حقن متغيرات البيئة في وقت التشغيل عبر
docker run -eأوenv_file، مما يتيح نشر صورة واحدة عبر بيئات متعددة.
📝 تمارين
-
مسألة أساسية (⭐): أنشئ
next.config.jsيحتوي علىoutput: 'standalone'، واكتب Dockerfile متعدد المراحل، وابنه وشغله بنجاح باستخدامdocker run، ثم تحقق منه باستخدامcurl localhost:3000. -
تمرين متقدم (⭐⭐): أضف حاوية Nginx كوكيل عكسي إلى Docker Compose: (1) أعد شهادة SSL موقعة ذاتيا؛ (2) أضف قواعد التخزين المؤقت للموارد الثابتة؛ (3) أعد
/_next/staticليتم تخزينها مؤقتا لمدة 365 يوما؛ (4) تحقق من أن الوصول عبر HTTPS يعمل بشكل صحيح. -
تحد (⭐⭐⭐): ابن خط تجميع CI/CD + Docker كامل للنشر الذاتي: (1) استخدم GitHub Actions لبناء صور Docker تلقائيا ودفعها إلى GHCR؛ (2) اسحب الصور الجديدة إلى الخادم الهدف عبر SSH؛ (3) استخدم Docker Compose للتحديث بدون توقف (
docker compose up -d --no-deps --build app)؛ (4) أعد وضع الكتلة في PM2 وتدوير السجلات.