رفع مشروع Django أو Flask على سيرفر VPS باستخدام Gunicorn وNginx

لرفع مشروع Django على سيرفر VPS للإنتاج: شغّله عبر Gunicorn كخدمة systemd، وضع Nginx أمامه كـ reverse proxy يخدم الملفات الثابتة أيضًا، واحفظ الأسرار في ملف متغيرات بيئة، واربطه بقاعدة PostgreSQL محلية، ثم فعّل HTTPS بـ Certbot. ولا تستخدم manage.py runserver في الإنتاج أبدًا؛ فهو خادم تطوير بخيط واحد، بلا مراقبة للعملية، وغير مصمم لتحمّل زيارات حقيقية أو هجمات. الخطوات التالية تنقل مشروعًا يعمل من مستودع Git إلى موقع حي على HTTPS فوق Ubuntu 22.04 أو 24.04، وفي النهاية نوضح الفروق مع Flask.
الأسماء المستخدمة في الأمثلة: حزمة المشروع myproject، ومستخدم النظام والخدمة myapp، والدومين example.com (استبدله بدومينك، ووجّه سجل A الخاص به إلى السيرفر أولًا). ونفّذ قبل البدء خطوات التأمين الأساسية من دليل تأمين سيرفر لينكس VPS بمفاتيح SSH وUFW وFail2ban.
كيف تتكامل الأجزاء؟
Nginx يستقبل الاتصالات على المنفذين 80 و443، ويتولى شهادة SSL، ويخدم
/static/و/media/مباشرة من القرص، ويمرر باقي الطلبات.Gunicorn يشغّل عدة عمليات Python (workers) تنفذ كود Django وتستمع على Unix socket.
systemd يشغّل Gunicorn مع إقلاع السيرفر، ويعيد تشغيله لو توقف، ويرسل مخرجاته إلى السجل.
PostgreSQL يستمع على localhost فقط.
1. تثبيت حزم النظام
sudo apt update
sudo apt install -y python3-venv python3-dev nginx postgresql gitبايثون موجودة مع Ubuntu (الإصدار 3.10 على 22.04 و3.12 على 24.04). لو مشروعك يحتاج إصدارًا مختلفًا، تأكد من ذلك قبل أن تكمل.
2. مستخدم خاص بالتطبيق، وجلب الكود، وبيئة افتراضية
المستخدم المخصص بلا صلاحية دخول يحدّ مما يمكن أن تصل إليه أي ثغرة في التطبيق.
sudo useradd --system --create-home --home-dir /srv/myapp --shell /usr/sbin/nologin myapp
sudo -u myapp git clone https://git.example.com/myproject.git /srv/myapp/src
sudo -u myapp python3 -m venv /srv/myapp/venv
sudo -u myapp /srv/myapp/venv/bin/pip install --upgrade pip
sudo -u myapp /srv/myapp/venv/bin/pip install -r /srv/myapp/src/requirements.txt gunicorn "psycopg[binary]"ثبّت أرقام الإصدارات في requirements.txt (ومنها Gunicorn بعد أن يعمل) حتى تحصل على نفس الحزم لو أعدت البناء بعد شهر. ويدعم Django 4.2 وما بعده مكتبة psycopg 3، أما المشاريع الأقدم فتستخدم psycopg2-binary. والبيئة الافتراضية venv تتجنب أيضًا خطأ "externally managed environment" الذي يظهر مع pip على مستوى النظام في Ubuntu 24.04.
3. إنشاء قاعدة بيانات PostgreSQL
sudo -u postgres psql
CREATE ROLE myapp WITH LOGIN PASSWORD 'change-this-long-password';
CREATE DATABASE myapp OWNER myapp;
\qجعل myapp مالكًا لقاعدة البيانات يتجنب أخطاء صلاحيات الـ schema التي تظهر مع إصدارات PostgreSQL الأحدث عند تنفيذ migrate. ولضبط الذاكرة والنسخ الاحتياطي وقواعد الوصول، راجع دليل PostgreSQL على VPS: الضبط والنسخ الاحتياطي والأمان.
4. الأسرار في ملف متغيرات بيئة
sudo mkdir -p /etc/myapp
sudo nano /etc/myapp/myapp.envDJANGO_SETTINGS_MODULE=myproject.settings
DJANGO_SECRET_KEY=paste-a-long-random-value-here
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=example.com,www.example.com
DB_NAME=myapp
DB_USER=myapp
DB_PASSWORD=change-this-long-password
DB_HOST=127.0.0.1
DB_PORT=5432python3 -c "import secrets; print(secrets.token_urlsafe(50))"
sudo chown root:myapp /etc/myapp/myapp.env
sudo chmod 640 /etc/myapp/myapp.envالكتلة الأولى تنشئ الملف، والأخيرة تولّد مفتاحًا سريًا عشوائيًا وتقفل الملف بحيث لا يقرؤه إلا root ومجموعة التطبيق. اجعل القيم بلا مسافات أو ضعها بين علامات تنصيص، لأن systemd والـ shell كليهما يقرآن هذا الملف. ولا ترفعه إلى Git أبدًا.
بعدها اجعل settings.py يقرأ القيم من البيئة:
import os
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DJANGO_DEBUG", "False") == "True"
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")
CSRF_TRUSTED_ORIGINS = ["https://example.com", "https://www.example.com"]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ["DB_NAME"],
"USER": os.environ["DB_USER"],
"PASSWORD": os.environ["DB_PASSWORD"],
"HOST": os.environ.get("DB_HOST", "127.0.0.1"),
"PORT": os.environ.get("DB_PORT", "5432"),
"CONN_MAX_AGE": 60,
}
}
STATIC_URL = "/static/"
STATIC_ROOT = "/var/www/myapp/static"
MEDIA_URL = "/media/"
MEDIA_ROOT = "/var/www/myapp/media"
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = Trueالإعداد SECURE_PROXY_SSL_HEADER يخبر Django أن يثق بالـ header المسمى X-Forwarded-Proto الذي يضيفه Nginx، فيعرف أن الطلب الأصلي كان HTTPS. لا تضعه إلا إذا كان Nginx أمام التطبيق ويكتب هذا الـ header بنفسه كما في الإعداد بالأسفل.
5. تنفيذ migrate وجمع الملفات الثابتة وفحص الإنتاج
sudo mkdir -p /var/www/myapp/static /var/www/myapp/media
sudo chown myapp:myapp /var/www/myapp/static /var/www/myapp/media
sudo -u myapp bash -c 'set -a; . /etc/myapp/myapp.env; set +a; cd /srv/myapp/src; /srv/myapp/venv/bin/python manage.py migrate'
sudo -u myapp bash -c 'set -a; . /etc/myapp/myapp.env; set +a; cd /srv/myapp/src; /srv/myapp/venv/bin/python manage.py collectstatic --noinput'
sudo -u myapp bash -c 'set -a; . /etc/myapp/myapp.env; set +a; cd /srv/myapp/src; /srv/myapp/venv/bin/python manage.py check --deploy'وضعنا الملفات الثابتة في /var/www/myapp بدل مجلد المستخدم، لأن Ubuntu ينشئ مجلدات المستخدمين بصلاحية 750 فلا يستطيع Nginx (المستخدم www-data) قراءتها. والأمر check --deploy يسرد إعدادات الإنتاج الناقصة؛ أصلح كل تحذير تفهمه وابحث عن الباقي.
6. تشغيل Gunicorn كخدمة systemd
أنشئ الملف /etc/systemd/system/myapp.service:
[Unit]
Description=myapp (Gunicorn)
After=network.target postgresql.service
[Service]
Type=notify
NotifyAccess=main
User=myapp
Group=myapp
WorkingDirectory=/srv/myapp/src
EnvironmentFile=/etc/myapp/myapp.env
RuntimeDirectory=myapp
ExecStart=/srv/myapp/venv/bin/gunicorn --workers 3 --timeout 60 --bind unix:/run/myapp/gunicorn.sock --access-logfile - myproject.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=30
Restart=on-failure
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myappلماذا هذه الاختيارات؟
عدد الـ workers: نقطة البداية المعتادة هي (2 × عدد الأنوية) + 1. كل worker يحمل نسخة كاملة من التطبيق في الذاكرة، غالبًا بين 50 و150 ميجابايت لمشروع Django عادي، فراجع الذاكرة بـ
free -hبعد التشغيل.RuntimeDirectory يجعل systemd ينشئ المجلد
/run/myappللـ socket مع كل تشغيل، ومالكه مستخدم التطبيق.ExecReload يرسل إشارة HUP، فيشغّل Gunicorn عمليات جديدة بالكود الجديد ويوقف القديمة بهدوء. لذلك ينشر الأمر
systemctl reload myappالتحديث دون قطع الطلبات، بشرط ألا تستخدم--preload.--timeout 60 يُنهي أي worker عالق 60 ثانية. المهام الطويلة مثل التقارير والاستيراد مكانها طابور مهام في الخلفية لا طلب ويب.
7. إعداد Nginx كـ reverse proxy
أنشئ الملف /etc/nginx/sites-available/myapp:
upstream myapp_app {
server unix:/run/myapp/gunicorn.sock fail_timeout=0;
}
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
client_max_body_size 20M;
location /static/ {
alias /var/www/myapp/static/;
expires 30d;
access_log off;
}
location /media/ {
alias /var/www/myapp/media/;
}
location / {
proxy_pass http://myapp_app;
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_redirect off;
proxy_read_timeout 60s;
}
}sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginxانتبه للشرطة المائلة في الآخر: location /static/ وalias /var/www/myapp/static/; لا بد أن ينتهي كلاهما بـ /، وإلا ستظهر أخطاء 404 للملفات. أما client_max_body_size فيحدد أقصى حجم للرفع، والافتراضي في Nginx ميجابايت واحد فقط.
8. تفعيل HTTPS بشهادة مجانية عبر Certbot
sudo ufw allow 'Nginx Full'
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-runيعدّل Certbot إعداد Nginx ليضيف المنفذ 443 وتحويلًا من HTTP، ويضبط مؤقتًا يجدد الشهادة تلقائيًا. بعدها يجب أن يفتح الموقع على https:// وأن يعمل دخول لوحة الإدارة دون أخطاء CSRF.
9. السجلات والأخطاء التي ستقابلها غالبًا
sudo journalctl -u myapp -f
sudo journalctl -u myapp --since "1 hour ago"
sudo tail -f /var/log/nginx/error.log502 Bad Gateway: Gunicorn لا يعمل، أو مسار الـ socket مختلف بين ملف الخدمة وإعداد Nginx. ابدأ بـ
systemctl status myapp.400 Bad Request: الدومين غير موجود في
DJANGO_ALLOWED_HOSTS.فشل CSRF بعد تفعيل HTTPS: أضف عنوان https إلى
CSRF_TRUSTED_ORIGINSوتأكد أن Nginx يرسلX-Forwarded-Proto.الصفحة تظهر بلا تنسيق: لم يُنفَّذ
collectstaticأو مسارaliasخطأ.تحويل لا ينتهي (redirect loop): تفعيل
SECURE_SSL_REDIRECT = Trueدون إعداد الـ proxy header.
10. نشر التحديثات تقريبًا بلا توقف
احفظ هذا السكربت باسم /usr/local/bin/deploy-myapp واجعله قابلًا للتنفيذ:
#!/usr/bin/env bash
set -euo pipefail
APP=/srv/myapp
run() { sudo -u myapp bash -c "set -a; . /etc/myapp/myapp.env; set +a; cd $APP/src; $*"; }
run "git pull --ff-only"
run "$APP/venv/bin/pip install -r requirements.txt"
run "$APP/venv/bin/python manage.py migrate --noinput"
run "$APP/venv/bin/python manage.py collectstatic --noinput"
sudo systemctl reload myappفي الـ migrations التي تحذف أعمدة أو تغير أسماءها، انشر على مرحلتين (كود يعمل مع الشكلين أولًا، ثم الـ migration) حتى لا تنهار العمليات القديمة أثناء إعادة التحميل. ونفس النمط، أي عملية تحت إشراف خلف Nginx، ينطبق على لغات أخرى؛ قارنه بدليل استضافة Node.js على VPS باستخدام PM2 وNginx.
لو مشروعك Flask
كل ما سبق ينطبق مع ثلاثة فروق: لا يوجد migrate ولا collectstatic (استخدم flask db upgrade لو تعتمد على Flask-Migrate)، وتحتاج نقطة دخول WSGI تثق بـ headers الـ proxy، ثم توجّه Gunicorn إليها:
# /srv/myapp/src/wsgi.py
from werkzeug.middleware.proxy_fix import ProxyFix
from app import create_app
app = create_app()
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)ExecStart=/srv/myapp/venv/bin/gunicorn --workers 3 --bind unix:/run/myapp/gunicorn.sock --access-logfile - wsgi:appوللملفات الثابتة، انسخ مجلد static الخاص بالتطبيق إلى /var/www/myapp/static أثناء النشر، أو اجعل alias يشير إلى أي مجلد يستطيع Nginx قراءته.
اختيار السيرفر المناسب
كقاعدة عامة، تطبيق Django أو Flask صغير مع PostgreSQL على نفس الجهاز يعمل براحة على نواتين و2 إلى 4 جيجابايت RAM، ويزيد الاحتياج مع عدد الـ workers وCelery وحجم قاعدة البيانات. سيرفر Linux VPS من IM HOST يعطيك root كاملًا على KVM مع Ubuntu أو Debian ويبدأ من $5 شهريًا. وعندما تحتاج الطلبات الثقيلة أو قاعدة البيانات المزدحمة أداءً ثابتًا، فإن Cloud VPS بأنوية وذاكرة مخصصة يتيح لك الانتقال بين الباقات مع نمو الزيارات.
أسئلة شائعة
لماذا Gunicorn وNginx بدل runserver؟
runserver خادم للتطوير فقط. Gunicorn يشغّل عدة عمليات تحت إشراف، وNginx يتعامل مع SSL والاتصالات البطيئة والملفات الثابتة بكفاءة أعلى بكثير من بايثون.
كم عدد Gunicorn workers المناسب؟
ابدأ بـ (2 × عدد الأنوية) + 1 ثم عدّل حسب الذاكرة المتاحة. لو الذاكرة محدودة، فعدد أقل من الـ workers مع --threads 2 بديل معقول للتطبيقات التي تنتظر قاعدة البيانات والشبكة أكثر مما تستهلك المعالج.
Unix socket أم 127.0.0.1:8000؟
الاثنان يعملان. الـ socket يتجنب حمل TCP ولا يمكن الوصول إليه من الشبكة، والمنفذ المحلي أسهل في الاختبار بـ curl. اختر واحدًا واستخدم نفس القيمة في ملف systemd وفي Nginx.
هل يمكن استخدام Docker بدلًا من ذلك؟
نعم، والطبقات نفسها تنطبق داخل الحاويات: Gunicorn في حاوية التطبيق، وNginx أمامه، وPostgreSQL مع volume دائم. لكن طريقة systemd أبسط لتطبيق واحد على سيرفر واحد.
إذا أردت سيرفرًا نظيفًا تجرب عليه هذه الخطوات، ابدأ بـ Linux VPS من IM HOST. لا رسوم إعداد، والتجديد بنفس السعر، ومهندسونا متاحون 24/7 لو لم تعمل أي خطوة كما هو موضح.
المزيد من مدونتنا
اكتشف المزيد من الأدلة العملية ورؤى الخدمات من فريق IM Host.
عرض كل المقالات