خوادم الويب والوكلاء العكسيون

مشروع: واجهة Nginx أمامية للإنتاج

35 دقيقة الدرس 10 من 28

مشروع: واجهة Nginx أمامية للإنتاج

كل ما درسناه في هذه السلسلة كان يتجه نحو هذا الدرس. أنت تمتلك الآن المفردات اللازمة — إنهاء TLS، والبروكسي العكسي، ومجموعات الخوادم الخلفية، والتخزين المؤقت، وتحديد معدل الطلبات، ورؤوس الأمان، ومفاتيح الضبط. يربط هذا المشروع جميع هذه العناصر معاً في تهيئة Nginx واحدة مختبرة ميدانياً يمكنك نشرها أمام تطبيق حقيقي اليوم. التطبيق النموذجي هو عملية Node.js أو Python أو PHP تستمع على 127.0.0.1:8000، لكن النمط متطابق مع أي لغة أو إطار عمل.

سنبني التهيئة في طبقات متعمدة — نجعلها تعمل أولاً، ثم نجعلها صحيحة، ثم سريعة وصلبة. يعكس هذا التطور الطريقة الفعلية التي يكرر بها المهندسون الكبار على البنية التحتية للإنتاج.

معمارية المشروع

Production Nginx front-end architecture Internet :443 / :80 Nginx TLS Termination (Let\'s Encrypt) Rate Limiting (req/s zones) Security Headers proxy_cache (disk / RAM) proxy_pass → upstream gzip + brotli compression App Upstream 127.0.0.1:8000 keepalive 32 Static Files /var/www/app/public Cache Zone /var/cache/nginx Let\'s Encrypt ACME / certbot HTTPS dynamic static hit/miss
طبقات Nginx للإنتاج: إنهاء TLS، وتحديد المعدل، والتخزين المؤقت، وتمرير البروكسي إلى مجموعة التطبيقات الخلفية.

الخطوة الأولى — تخطيط المجلدات والمتطلبات الأساسية

قبل كتابة أي توجيه واحد، أنشئ تخطيط الملفات. تستخدم البيئات المؤسسية الكبرى نمط sites-available / sites-enabled مع الروابط الرمزية حتى يمكن إدارة كل مضيف ظاهري باستقلالية وتعطيله دون لمس الآخرين.

# Install Nginx and Certbot (Debian/Ubuntu) apt-get update && apt-get install -y nginx certbot python3-certbot-nginx # Create directory structure mkdir -p /etc/nginx/conf.d mkdir -p /var/cache/nginx/app_cache mkdir -p /var/log/nginx mkdir -p /var/www/app/public # Set cache directory ownership chown -R www-data:www-data /var/cache/nginx # Obtain a TLS certificate (DNS must already point to this server) certbot certonly --nginx -d example.com -d www.example.com \ --non-interactive --agree-tos -m ops@example.com

الخطوة الثانية — خط أساس nginx.conf العالمي

يضبط الملف العالمي ضبط العمال، وتنسيق التسجيل، ويُعلن منطقة التخزين المؤقت المشتركة. تعيش منطقة التخزين المؤقت هنا — وليس في كتلة الخادم — لأن المنطقة هي منطقة ذاكرة مشتركة، وليست خاصة بكل مضيف ظاهري.

# /etc/nginx/nginx.conf user www-data; worker_processes auto; worker_rlimit_nofile 65535; pid /run/nginx.pid; events { worker_connections 4096; use epoll; multi_accept on; } http { # --- MIME & basics --- include /etc/nginx/mime.types; default_type application/octet-stream; sendfile on; tcp_nopush on; tcp_nodelay on; # --- Keep-alive --- keepalive_timeout 65; keepalive_requests 1000; # --- Buffers (tune to match your app response sizes) --- client_body_buffer_size 16k; client_max_body_size 50m; proxy_buffer_size 16k; proxy_buffers 4 64k; proxy_busy_buffers_size 128k; # --- Gzip compression --- gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_min_length 1024; gzip_types text/plain text/css application/json application/javascript application/xml text/xml image/svg+xml font/woff2; # --- Rate-limiting zones (shared memory) --- limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s; limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m; # --- Proxy cache zone (disk-backed, 1GB max) --- proxy_cache_path /var/cache/nginx/app_cache levels=1:2 keys_zone=app_cache:20m max_size=1g inactive=60m use_temp_path=off; # --- Structured JSON log format --- log_format json_combined escape=json '{' '"time":"$time_iso8601",' '"ip":"$remote_addr",' '"method":"$request_method",' '"uri":"$uri",' '"status":"$status",' '"bytes":"$body_bytes_sent",' '"rt":"$request_time",' '"upstream_rt":"$upstream_response_time",' '"cache":"$upstream_cache_status",' '"ua":"$http_user_agent"' '}'; access_log /var/log/nginx/access.log json_combined; error_log /var/log/nginx/error.log warn; server_tokens off; include /etc/nginx/conf.d/*.conf; }
لماذا تنسيق سجل JSON؟ تتطلب السجلات العادية المدمجة محللات تعبير منتظم في كل مجمّع سجلات. يُرسَل سجل JSON المنظّم مباشرة إلى Elasticsearch أو Datadog أو Splunk دون أي تحويل. كل حقل — وقت استجابة الخادم الخلفي، حالة ضربة التخزين المؤقت، الأسلوب — يصبح بُعداً قابلاً للتصفية. على نطاق واسع، هذا هو الفرق بين حادث يستغرق 30 دقيقة وحادث يستغرق 3 دقائق.

الخطوة الثالثة — تهيئة المضيف الظاهري

هذا هو ملف /etc/nginx/conf.d/example.conf الكامل. اقرأ التعليقات الداخلية بعناية — لكل توجيه سبب سيُهم في الإنتاج.

# /etc/nginx/conf.d/example.conf # ── Upstream pool ────────────────────────────────────────────────────────── upstream app_backend { server 127.0.0.1:8000; keepalive 32; keepalive_requests 100; keepalive_timeout 60s; } # ── HTTP → HTTPS redirect ────────────────────────────────────────────────── server { listen 80; listen [::]:80; server_name example.com www.example.com; location /.well-known/acme-challenge/ { root /var/www/certbot; } location / { return 301 https://example.com$request_uri; } } # ── www → apex redirect ─────────────────────────────────────────────────── server { listen 443 ssl; listen [::]:443 ssl; http2 on; server_name www.example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; return 301 https://example.com$request_uri; } # ── Primary HTTPS vhost ──────────────────────────────────────────────────── server { listen 443 ssl; listen [::]:443 ssl; http2 on; server_name example.com; root /var/www/app/public; # ── TLS ─────────────────────────────────────────────────────────── ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on; resolver 1.1.1.1 8.8.8.8 valid=300s; resolver_timeout 5s; # ── Security headers ─────────────────────────────────────────────── add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always; add_header Content-Security-Policy "default-src \'self\'; script-src \'self\' \'unsafe-inline\'; style-src \'self\' \'unsafe-inline\'; img-src \'self\' data: https:; font-src \'self\'; connect-src \'self\'; frame-ancestors \'self\';" always; # ── Proxy cache defaults ─────────────────────────────────────────── proxy_cache app_cache; proxy_cache_valid 200 302 10m; proxy_cache_valid 404 1m; proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; proxy_cache_lock on; add_header X-Cache-Status $upstream_cache_status; # ── Common proxy headers ─────────────────────────────────────────── proxy_http_version 1.1; 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_set_header Connection ""; # ── Static assets ───────────────────────────────────────────────── location ~* \.(css|js|woff2|woff|ttf|ico|png|jpg|jpeg|gif|svg|webp|avif|pdf|zip)$ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; gzip_static on; } # ── Auth endpoints ──────────────────────────────────────────────── location ~* ^/(login|register|password|api/auth) { limit_req zone=auth burst=5 nodelay; limit_req_status 429; proxy_pass http://app_backend; proxy_cache off; } # ── API endpoints ───────────────────────────────────────────────── location /api/ { limit_req zone=api burst=60 nodelay; limit_req_status 429; proxy_pass http://app_backend; proxy_cache off; proxy_no_cache 1; proxy_cache_bypass 1; } # ── Default ─────────────────────────────────────────────────────── location / { limit_req zone=api burst=60 nodelay; try_files $uri $uri/ @proxy; } location @proxy { proxy_pass http://app_backend; proxy_cache_methods GET HEAD; proxy_cache_bypass $http_authorization $cookie_session; proxy_no_cache $http_authorization $cookie_session; } # ── Healthcheck ─────────────────────────────────────────────────── location = /healthz { access_log off; proxy_pass http://app_backend; proxy_cache off; } # ── Block attack paths ───────────────────────────────────────────── location ~ /\. { deny all; } location ~* \.(php|asp|aspx|jsp|cgi)$ { deny all; } error_page 429 /errors/429.html; error_page 502 503 504 /errors/50x.html; location ^~ /errors/ { internal; root /var/www/app; } access_log /var/log/nginx/example_access.log json_combined; error_log /var/log/nginx/example_error.log warn; }

الخطوة الرابعة — التحقق وإعادة التحميل واختبار الدخان

# 1. Syntax check — ALWAYS do this before reloading nginx -t # 2. Zero-downtime reload systemctl reload nginx # 3. Confirm TLS and HTTP/2 curl -I https://example.com # 4. Check HSTS header curl -sI https://example.com | grep -i strict # 5. Probe cache — first: MISS, second: HIT curl -sI https://example.com/about | grep X-Cache-Status curl -sI https://example.com/about | grep X-Cache-Status # 6. Verify rate limiting on auth (expect 429 after 5 requests) for i in $(seq 1 20); do curl -o /dev/null -sw "%{http_code}\n" -X POST https://example.com/login done
قائمة مراجعة اختبار الدخان للإنتاج بعد كل تغيير في تهيئة Nginx:
  1. يجتاز nginx -t دون تحذيرات.
  2. يُعيد curl -I الاستجابة HTTP/2 200 (لا حلقة 301، ولا 502).
  3. رأس HSTS موجود في استجابة HTTPS.
  4. الحصول على X-Cache-Status: HIT في الطلب الثاني لعنوان URL قابل للتخزين المؤقت.
  5. يُعيد تحديد المعدل 429 (وليس 500) على مسار المصادقة.
  6. تُعيد الأصول الثابتة Cache-Control: public, immutable.
  7. يُعيد curl https://example.com/.env الاستجابة 403.

الخطوة الخامسة — تجديد TLS التلقائي وإعادة تحميل Nginx

تنتهي صلاحية الشهادات كل 90 يوماً. يُثبّت Certbot مؤقت systemd يُشغّل التجديد مرتين يومياً، لكنه لا يُعيد تحميل Nginx تلقائياً بعد التجديد. يجب أن تتصل بخطوة ما بعد التجديد.

# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh #!/bin/bash set -e nginx -t && systemctl reload nginx chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh # Test the full renewal flow without actually renewing certbot renew --dry-run

أنماط الفشل الشائعة في الإنتاج

كل نمط في هذه التهيئة موجود لأن شيئاً ما انكسر في الإنتاج. إليك أكثر أنماط الفشل شيوعاً التي يواجهها المهندسون مع هذا الإعداد بالتحديد:

  • 502 Bad Gateway بعد النشر: أُعيد تشغيل عملية التطبيق لكن مجموعة keepalive للخادم الخلفي لا تزال تحتفظ باتصال بـ FD العملية القديمة. الإصلاح: اضبط proxy_next_upstream error timeout invalid_header http_502 حتى يُعيد Nginx المحاولة مع الخادم التالي عند الفشل.
  • تسميم التخزين المؤقت عبر حقن رأس Host: يُرسل المهاجم رأس Host مصنوعاً خصيصاً؛ يُخزّن Nginx استجابة مفهرسة لنطاق مختلف. الإصلاح: اضبط صراحةً proxy_cache_key "$scheme$host$request_uri" ولا تُضمّن رؤوس يوفرها المستخدم في مفتاح التخزين المؤقت.
  • إقفال HSTS لنطاق ما: إذا ضبطت max-age طويلاً ثم احتجت للعودة إلى HTTP، سترفض المتصفحات الاتصال طوال مدة HSTS بأكملها. ابدأ بـ max-age=300 في بيئة التجريب؛ اضبط 63072000 (سنتان) في الإنتاج فقط عند الاستقرار التام.
  • تحديد المعدل يحجب المستخدمين الشرعيين خلف NAT: بروكسي مؤسسي يعني مئات المستخدمين يتشاركون عنوان IP واحداً. حد معدل 30 طلباً/ثانية لكل IP يصبح 0.3 طلب/ثانية لكل مستخدم. تخفيف: استخدم $http_x_forwarded_for إن كان بروكسي موثوق يضبطه، أو زد قيم burst للمسارات التي تستخدمها تطبيقات سطح المكتب.
  • فشل تجديد الشهادة بصمت: يُجدّد Certbot الشهادة لكن خطاف النشر لم يُجعل قابلاً للتنفيذ. النتيجة: يستمر Nginx في تقديم الشهادة القديمة حتى تنتهي صلاحيتها. نفّذ دائماً certbot renew --dry-run فور إضافة خطافات التجديد.
لا تضبط add_header داخل كتلة location وفي مستوى كتلة server في آنٍ معاً — فهما لا تُدمجان. يستبدل add_header داخل كتلة location جميع الرؤوس الموروثة من كتلة server الأصل. إذا ضبطت HSTS على مستوى server ثم أضفت أي add_header داخل كتلة location /api/، سيُسقط رأس HSTS بصمت من جميع استجابات API. الإصلاح هو تكرار جميع رؤوس الأمان المطلوبة في كل كتلة location، أو ضبطها فقط على مستوى server وعدم استخدامها أبداً داخل كتل location.

ما الذي بنيته

تُنفّذ هذه التهيئة كل طبقة من طبقات الواجهة الأمامية للإنتاج: TLS 1.2/1.3 مع تدبيس OCSP واستئناف الجلسة، وHTTP/2، وتجديد الشهادات التلقائي، وتسجيل HSTS المسبق، ومجموعة رؤوس أمان كاملة، وتخزين مؤقت للبروكسي مدعوم بالقرص مع دلالات stale-while-revalidate، وتحديد معدل لكل مسار مع هامش burst، وتجميع اتصالات keepalive للخادم الخلفي، وسجلات وصول JSON منظّمة، وفصل نظيف بين خدمة الملفات الثابتة (دون أي تورط لعملية التطبيق) والبروكسي الديناميكي. هذا هو النمط المستخدم بالضبط عبر البيئات السحابية الأصلية في شركات ذات حركة مرور عالية — بمعلمة كتلة upstream مختلفة ونطاق مختلف، يصبح الواجهة الأمامية لأي تطبيق في مكدسك.