داکردواپس

مقیاس‌پذیر و ایمن کردن یک برنامه Django با Docker، Nginx و Let’s Encrypt

مقدمه

در زیرساخت‌های ابری، روش‌های مختلفی برای مقیاس‌پذیر (Scalable) و ایمن (Secure) کردن یک برنامه Django وجود دارد.

یکی از رایج‌ترین روش‌ها، مقیاس‌پذیری افقی (Horizontal Scaling) است. در این روش به‌جای افزایش منابع یک سرور، چندین نمونه (Instance) از برنامه Django اجرا می‌شود. این کار علاوه بر افزایش توان پردازش درخواست‌ها، باعث می‌شود در صورت از کار افتادن یکی از سرورها، سرویس همچنان در دسترس باقی بماند و تحمل خطای بالاتری داشته باشد.

برای پیاده‌سازی این معماری معمولاً چند سرور برنامه (Application Server) در نظر گرفته می‌شود که روی هرکدام برنامه Django به همراه یک WSGI Server مانند Gunicorn یا uWSGI اجرا می‌شود.

در جلوی این سرورها نیز یک Nginx قرار می‌گیرد که وظایف زیر را بر عهده دارد:

  • توزیع درخواست‌ها بین سرورهای Django (Load Balancing)
  • Reverse Proxy
  • مدیریت اتصال‌های HTTPS
  • پایان دادن به ارتباط TLS (TLS Termination)
  • ارسال درخواست‌ها به سرورهای برنامه

اجرای Django و Nginx داخل کانتینرهای Docker باعث می‌شود بدون توجه به محیط اجرا (توسعه، تست یا تولید)، رفتار برنامه کاملاً یکسان باشد. همچنین Docker فرایند استقرار، بروزرسانی و مدیریت برنامه را بسیار ساده‌تر می‌کند.

در این آموزش ابتدا یک برنامه Django که داخل Docker اجرا می‌شود را به صورت افقی مقیاس‌پذیر می‌کنیم؛ به این صورت که دو سرور برنامه خواهیم داشت و روی هرکدام یک کانتینر شامل Django و Gunicorn اجرا می‌شود.

سپس برای فعال کردن HTTPS، یک سرور سوم راه‌اندازی می‌کنیم که روی آن دو کانتینر اجرا خواهند شد:

  • Nginx به عنوان Reverse Proxy و Load Balancer
  • Certbot برای دریافت و تمدید خودکار گواهی SSL از Let’s Encrypt

گواهی‌های TLS توسط Certbot دریافت شده و در اختیار Nginx قرار می‌گیرند تا تمامی ارتباطات از طریق HTTPS انجام شوند. در این معماری، تمام درخواست‌های کاربران ابتدا وارد سرور Nginx شده و سپس بین دو سرور Django توزیع می‌شوند.

در انتها نیز با اعمال تنظیمات امنیتی مناسب، دسترسی مستقیم کاربران به سرورهای Django را مسدود می‌کنیم تا تنها Nginx امکان برقراری ارتباط با آن‌ها را داشته باشد.


پیش‌نیازها

برای انجام این آموزش به موارد زیر نیاز دارید:

۱. سه سرور Ubuntu

به سه سرور Ubuntu 24.04 LTS (یا نسخه‌های جدیدتر) نیاز دارید:

  • دو سرور برای اجرای برنامه Django و Gunicorn
  • یک سرور برای اجرای Nginx و Certbot

روی هر سه سرور باید:

  • یک کاربر غیر Root با دسترسی sudo ایجاد شده باشد.
  • فایروال فعال باشد.

۲. نصب Docker روی هر سه سرور

روی هر سه سرور Docker را نصب کنید.

اگر از پارمین کلود استفاده می‌کنید، برای نصب Docker از میرور رسمی پارمین کلود استفاده کنید.

اضافه کردن کلید GPG

curl -fsSL https://docker-ce.parmincloud.ir/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker.gpg

اضافه کردن مخزن Docker

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] https://docker-ce.parmincloud.ir/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

سپس:

sudo apt update

sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

۳. یک دامنه

به یک دامنه نیاز دارید؛ در این آموزش از دامنه زیر استفاده می‌کنیم:

your_domain.com

۴. تنظیم DNS

یک رکورد A ایجاد کنید تا دامنه شما به IP عمومی سرور Nginx اشاره کند.


۵. فضای ذخیره‌سازی برای فایل‌های Static

برای نگهداری فایل‌های Static برنامه Django به یک فضای Object Storage سازگار با S3 نیاز دارید.

در این آموزش می‌توانید از Object Storage پارمین کلود یا هر سرویس سازگار با S3 استفاده کنید.

همچنین باید اطلاعات زیر را در اختیار داشته باشید:

  • Access Key
  • Secret Key
  • Bucket Name
  • Endpoint URL

۶. پایگاه داده PostgreSQL

یک سرور PostgreSQL آماده کنید و یک دیتابیس برای پروژه ایجاد نمایید.

برای مثال:

  • Database Name
polls
  • Database User
sammy

همچنین اطلاعات زیر را یادداشت کنید:

  • نام دیتابیس
  • نام کاربری
  • رمز عبور
  • آدرس سرور
  • شماره پورت

در این آموزش از PostgreSQL استفاده می‌کنیم، اما با تغییرات جزئی می‌توانید از سایر پایگاه‌های داده پشتیبانی‌شده توسط Django نیز استفاده کنید.


مرحله اول — راه‌اندازی اولین سرور Django

در این بخش، اولین سرور برنامه (Application Server) را آماده می‌کنیم. در پایان این مرحله، یک کانتینر شامل Django و Gunicorn روی اولین سرور اجرا خواهد شد و از طریق مرورگر قابل دسترس خواهد بود.

نکته: اگر قبلاً مقاله «راه‌اندازی Django و Gunicorn با Docker» را انجام داده‌اید، می‌توانید این مرحله را رد کرده و مستقیماً به مرحله دوم بروید.


دریافت سورس پروژه

ابتدا وارد اولین سرور شوید و مخزن نمونه پروژه Polls را دریافت کنید:

git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git

سپس وارد پوشه پروژه شوید:

cd django-polls

این پروژه نسخه Dockerized برنامه نمونه Polls در مستندات Django است که برای اجرا داخل کانتینر آماده شده است.


بررسی Dockerfile

فایل Dockerfile را مشاهده کنید:

cat Dockerfile

محتوای آن مشابه زیر خواهد بود:

FROM python:3.7.4-alpine3.10

ADD django-polls/requirements.txt /app/requirements.txt

RUN set -ex \
    && apk add --no-cache --virtual .build-deps postgresql-dev build-base \
    && python -m venv /env \
    && /env/bin/pip install --upgrade pip \
    && /env/bin/pip install --no-cache-dir -r /app/requirements.txt \
    && runDeps="$(scanelf --needed --nobanner --recursive /env \
        | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \
        | sort -u \
        | xargs -r apk info --installed \
        | sort -u)" \
    && apk add --virtual rundeps $runDeps \
    && apk del .build-deps

ADD django-polls /app
WORKDIR /app

ENV VIRTUAL_ENV /env
ENV PATH /env/bin:$PATH

EXPOSE 8000

CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "mysite.wsgi"]

این Dockerfile مراحل زیر را انجام می‌دهد:

  • از ایمیج رسمی Python به عنوان Base Image استفاده می‌کند.
  • وابستگی‌های موردنیاز پروژه را نصب می‌کند.
  • محیط مجازی Python ایجاد می‌کند.
  • کتابخانه‌های موردنیاز Django و Gunicorn را نصب می‌کند.
  • فایل‌های پروژه را داخل ایمیج کپی می‌کند.
  • پورت 8000 را برای دریافت درخواست‌ها باز می‌کند.
  • در نهایت Gunicorn را با سه Worker اجرا می‌کند تا درخواست‌های ورودی را پردازش کند.

ساخت ایمیج Docker

اکنون ایمیج پروژه را بسازید:

docker build -t polls .

پارامتر -t نام ایمیج را مشخص می‌کند. در این آموزش نام ایمیج را polls قرار داده‌ایم.

پس از پایان Build، ایمیج‌های موجود را بررسی کنید:

docker images

خروجی مشابه زیر خواهد بود:

REPOSITORY          TAG       SIZE
polls               latest    197MB
python              3.7.4     98MB

اگر ایمیج polls را مشاهده می‌کنید، یعنی ساخت ایمیج با موفقیت انجام شده است.


پیکربندی فایل env

قبل از اجرای کانتینر، باید متغیرهای محیطی پروژه را تنظیم کنیم.

فایل env را باز کنید:

nano env

محتوای آن به شکل زیر است:

DJANGO_SECRET_KEY=
DEBUG=True
DJANGO_ALLOWED_HOSTS=
DATABASE_ENGINE=postgresql_psycopg2
DATABASE_NAME=polls
DATABASE_USERNAME=
DATABASE_PASSWORD=
DATABASE_HOST=
DATABASE_PORT=
STATIC_ACCESS_KEY_ID=
STATIC_SECRET_KEY=
STATIC_BUCKET_NAME=
STATIC_ENDPOINT_URL=
DJANGO_LOGLEVEL=info

مقادیر زیر را تکمیل کنید:

متغیر توضیح
DJANGO_SECRET_KEY یک کلید امنیتی تصادفی و غیرقابل حدس برای Django
DJANGO_ALLOWED_HOSTS در محیط آزمایشی می‌توانید * قرار دهید. در محیط Production دامنه سایت را وارد کنید.
DATABASE_NAME نام دیتابیس PostgreSQL
DATABASE_USERNAME نام کاربر دیتابیس
DATABASE_PASSWORD رمز عبور دیتابیس
DATABASE_HOST آدرس یا IP سرور PostgreSQL
DATABASE_PORT پورت PostgreSQL (معمولاً 5432)
STATIC_ACCESS_KEY_ID Access Key مربوط به فضای ذخیره‌سازی S3
STATIC_SECRET_KEY Secret Key فضای ذخیره‌سازی
STATIC_BUCKET_NAME نام Bucket
STATIC_ENDPOINT_URL آدرس Endpoint سرویس Object Storage

پس از تکمیل اطلاعات، فایل را ذخیره و خارج شوید.


ایجاد ساختار پایگاه داده

بعد از تکمیل فایل env، باید جداول موردنیاز پروژه را در پایگاه داده ایجاد کنیم.

برای این کار دستور زیر را اجرا کنید:

docker run --env-file env polls sh -c "python manage.py makemigrations && python manage.py migrate"

در این دستور:

  • فایل env به کانتینر منتقل می‌شود.
  • دستور پیش‌فرض Dockerfile به‌طور موقت با یک Shell جایگزین می‌شود.
  • ابتدا makemigrations اجرا شده و در صورت وجود تغییرات، Migrationها ساخته می‌شوند.
  • سپس migrate اجرا شده و جداول موردنیاز در PostgreSQL ایجاد می‌شوند.

اگر همه‌چیز به‌درستی انجام شده باشد، خروجی مشابه زیر را مشاهده خواهید کرد:

No changes detected
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, polls, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  ...
  Applying polls.0001_initial... OK
  Applying sessions.0001_initial... OK

این خروجی نشان می‌دهد که ساختار پایگاه داده با موفقیت ایجاد شده است.

نکته: اگر این دستور را مجدداً اجرا کنید و تغییری در مدل‌های Django ایجاد نشده باشد، عملیات جدیدی انجام نخواهد شد.


ایجاد کاربر مدیریتی (Superuser)

اکنون باید یک کاربر مدیریتی برای ورود به پنل مدیریت Django ایجاد کنیم.

ابتدا یک کانتینر تعاملی اجرا کنید:

docker run -it --env-file env polls sh

پس از ورود به محیط کانتینر، دستور زیر را اجرا کنید:

python manage.py createsuperuser

سپس اطلاعات موردنیاز را وارد کنید:

  • نام کاربری (Username)
  • ایمیل
  • رمز عبور

پس از ایجاد کاربر، با فشردن کلیدهای CTRL + D از محیط کانتینر خارج شوید.


تولید و بارگذاری فایل‌های Static

در مرحله بعد باید فایل‌های Static پروژه را تولید کرده و در فضای ذخیره‌سازی Object Storage بارگذاری کنیم.

دستور زیر را اجرا کنید:

docker run --env-file env polls sh -c "python manage.py collectstatic --noinput"

بسته به سرعت اینترنت و فضای ذخیره‌سازی، این عملیات ممکن است چند دقیقه زمان ببرد.

در پایان خروجی مشابه زیر را مشاهده خواهید کرد:

121 static files copied.

این پیام نشان می‌دهد که فایل‌های Static با موفقیت تولید و در Bucket مربوطه بارگذاری شده‌اند.


اجرای برنامه

اکنون همه‌چیز آماده است و می‌توانیم کانتینر Django را اجرا کنیم:

docker run --env-file env -p 80:8000 polls

خروجی مشابه زیر خواهد بود:

[INFO] Starting gunicorn
[INFO] Listening at: http://0.0.0.0:8000
[INFO] Using worker: sync
[INFO] Booting worker with pid: ...

در این دستور:

  • پورت ۸۰ سرور به پورت ۸۰۰۰ داخل کانتینر متصل می‌شود.
  • Gunicorn با سه Worker اجرا شده و آماده پاسخ‌گویی به درخواست‌ها خواهد بود.

بررسی عملکرد برنامه

اکنون مرورگر خود را باز کرده و آدرس زیر را وارد کنید:

http://IP-سرور

به احتمال زیاد با خطای 404 Page Not Found روبه‌رو خواهید شد.

این موضوع کاملاً طبیعی است، زیرا هنوز مسیری برای آدرس / در پروژه تعریف نشده است.

برای مشاهده برنامه نمونه Polls کافی است وارد آدرس زیر شوید:

http://IP-سرور/polls

اگر همه مراحل را به‌درستی انجام داده باشید، صفحه اصلی برنامه Polls نمایش داده خواهد شد.


ورود به پنل مدیریت Django

برای دسترسی به پنل مدیریت، آدرس زیر را در مرورگر باز کنید:

http://IP-سرور/admin

صفحه ورود پنل مدیریت Django نمایش داده می‌شود.

در این صفحه، نام کاربری و رمز عبوری که با دستور createsuperuser ایجاد کردید را وارد کنید تا وارد پنل مدیریت شوید.

پس از ورود، داشبورد مدیریتی Django را مشاهده خواهید کرد و می‌توانید داده‌های برنامه Polls را مدیریت کنید.

همچنین فایل‌های Static مربوط به پنل مدیریت و برنامه Polls مستقیماً از Object Storage بارگذاری می‌شوند؛ بنابراین فشار کمتری به سرور برنامه وارد خواهد شد.


توقف کانتینر

پس از اطمینان از اینکه برنامه به‌درستی اجرا می‌شود، با فشردن کلیدهای زیر اجرای کانتینر را متوقف کنید:

CTRL + C

اجرای برنامه در حالت Detached

در محیط Production معمولاً نمی‌خواهیم با بستن ترمینال، کانتینر نیز متوقف شود. بنابراین آن را در حالت Detached اجرا می‌کنیم:

docker run -d --rm --name polls --env-file env -p 80:8000 polls

در این دستور:

  • -d باعث می‌شود کانتینر در پس‌زمینه اجرا شود.
  • --rm پس از توقف کانتینر، منابع آن را به‌صورت خودکار پاک می‌کند.
  • --name polls نام مشخصی برای کانتینر تعیین می‌کند.

پس از اجرای این دستور، حتی اگر از سرور خارج شوید، برنامه همچنان در حال اجرا خواهد بود.

برای اطمینان، یک بار دیگر آدرس زیر را در مرورگر باز کنید:

http://IP-سرور/polls

اگر صفحه Polls نمایش داده شد، اولین سرور برنامه با موفقیت راه‌اندازی شده است.


مرحله دوم — راه‌اندازی دومین سرور Django

اکنون باید یک نسخه دیگر از برنامه را روی سرور دوم اجرا کنیم تا بعداً Nginx بتواند درخواست‌ها را بین هر دو سرور توزیع کند (Load Balancing).

از آنجایی که مراحل این بخش تقریباً مشابه مرحله قبل هستند، فقط دستورات اصلی را مرور می‌کنیم.

ابتدا وارد سرور دوم شوید و مخزن پروژه را دریافت کنید:

git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git

سپس وارد پوشه پروژه شوید:

cd django-polls

ساخت ایمیج Docker

ایمیج پروژه را بسازید:

docker build -t polls .

پیکربندی فایل env

فایل تنظیمات را باز کنید:

nano env

مانند سرور اول، مقادیر زیر را تکمیل کنید:

DJANGO_SECRET_KEY=
DEBUG=True
DJANGO_ALLOWED_HOSTS=
DATABASE_ENGINE=postgresql_psycopg2
DATABASE_NAME=polls
DATABASE_USERNAME=
DATABASE_PASSWORD=
DATABASE_HOST=
DATABASE_PORT=
STATIC_ACCESS_KEY_ID=
STATIC_SECRET_KEY=
STATIC_BUCKET_NAME=
STATIC_ENDPOINT_URL=
DJANGO_LOGLEVEL=info

مقادیر این فایل باید دقیقاً مشابه سرور اول باشند؛ زیرا هر دو سرور به یک پایگاه داده و یک فضای ذخیره‌سازی مشترک متصل می‌شوند.

پس از ذخیره فایل، کانتینر را در حالت Detached اجرا کنید:

docker run -d --rm --name polls --env-file env -p 80:8000 polls

اکنون آدرس زیر را در مرورگر باز کنید:

http://IP-سرور-دوم/polls

اگر صفحه Polls نمایش داده شد، سرور دوم نیز با موفقیت راه‌اندازی شده است.

در این مرحله، دو Application Server کاملاً آماده هستند و در ادامه، سرور Nginx Reverse Proxy را راه‌اندازی می‌کنیم تا درخواست‌ها را بین این دو سرور توزیع کند.


مرحله سوم — راه‌اندازی کانتینر Nginx

تا اینجا دو سرور Django آماده شده‌اند. در این مرحله، یک سرور مجزا برای Nginx راه‌اندازی می‌کنیم تا به عنوان Reverse Proxy و Load Balancer عمل کند.

Nginx درخواست‌های کاربران را دریافت کرده و آن‌ها را بین دو سرور Django توزیع می‌کند. همچنین در ادامه، مسئول مدیریت HTTPS و گواهی SSL نیز خواهد بود.


چرا از Nginx استفاده می‌کنیم؟

Nginx امکانات متعددی در اختیار ما قرار می‌دهد، از جمله:

  • Reverse Proxy
  • Load Balancing
  • مدیریت HTTPS
  • کش کردن محتوا
  • افزایش امنیت

در این آموزش، فایل‌های Static مستقیماً از Object Storage بارگذاری می‌شوند؛ بنابراین از قابلیت Cache در Nginx استفاده نمی‌کنیم. در عوض، Nginx مسئول انجام این وظایف خواهد بود:

  • توزیع درخواست‌ها بین دو سرور Django
  • مدیریت ارتباط HTTPS
  • دریافت گواهی SSL از Let’s Encrypt
  • هدایت خودکار درخواست‌های HTTP به HTTPS

به این ترتیب، تمام درخواست‌های کاربران ابتدا وارد Nginx شده و سپس به یکی از سرورهای Django ارسال می‌شوند.


معماری این آموزش

در این سناریو، Nginx روی یک سرور مستقل اجرا می‌شود و دو سرور Django در پشت آن قرار می‌گیرند.

این معماری چند مزیت مهم دارد:

  • افزایش مقیاس‌پذیری (Scalability)
  • جداسازی لایه وب از لایه برنامه
  • امکان افزایش تعداد سرورهای Django بدون تغییر در ساختار کلی
  • امکان اضافه کردن چندین Nginx در آینده و استفاده از Load Balancerهای سطح بالاتر مانند HAProxy

البته بسته به نیاز پروژه، می‌توان Nginx را روی یکی از سرورهای Django نیز اجرا کرد یا از معماری‌های متفاوت استفاده نمود.


ایجاد پوشه تنظیمات Nginx

وارد سرور Proxy شوید و پوشه تنظیمات را ایجاد کنید:

mkdir conf

سپس فایل تنظیمات Nginx را ایجاد کنید:

nano conf/nginx.conf

تنظیمات Nginx

محتوای زیر را داخل فایل قرار دهید:

upstream django {
    server APP_SERVER_1_IP;
    server APP_SERVER_2_IP;
}

server {
    listen 80 default_server;
    return 444;
}

server {
    listen 80;
    listen [::]:80;
    server_name your_domain.com;

    return 301 https://$server_name$request_uri;
}

server {

    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name your_domain.com;

    ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;

    ssl_session_cache shared:le_nginx_SSL:10m;
    ssl_session_timeout 1440m;
    ssl_session_tickets off;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";

    client_max_body_size 4G;
    keepalive_timeout 5;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_pass http://django;
    }

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/html;
    }

}

این فایل چه کاری انجام می‌دهد؟

این فایل چهار بخش اصلی دارد:

۱. تعریف سرورهای Backend

در ابتدای فایل، دو سرور Django معرفی می‌شوند:

upstream django {
    server APP_SERVER_1_IP;
    server APP_SERVER_2_IP;
}

هر زمان که Nginx درخواستی را دریافت کند، آن را به یکی از این دو سرور ارسال خواهد کرد.

اگر از شبکه خصوصی Parmin Cloud VPC استفاده می‌کنید، بهتر است در این قسمت از IP خصوصی سرورها استفاده کنید تا ارتباط بین سرورها از طریق شبکه داخلی انجام شود.


۲. مسدود کردن درخواست‌هایی که دامنه صحیح ندارند

server {
    listen 80 default_server;
    return 444;
}

اگر کاربری مستقیماً IP سرور را وارد کند یا دامنه‌ای غیر از دامنه اصلی سایت را درخواست دهد، Nginx بدون ارسال پاسخ، اتصال را قطع می‌کند.

کد وضعیت 444 یک کد اختصاصی Nginx است و باعث می‌شود ارتباط فوراً بسته شود.


۳. هدایت HTTP به HTTPS

server {
    listen 80;

    server_name your_domain.com;

    return 301 https://$server_name$request_uri;
}

در این بخش، تمام درخواست‌های HTTP با کد 301 به نسخه HTTPS هدایت می‌شوند.

به این ترتیب کاربران همیشه از نسخه امن سایت استفاده خواهند کرد.


۴. پیکربندی HTTPS

در آخرین بخش فایل، تنظیمات SSL و Reverse Proxy قرار گرفته است.

این قسمت:

  • از گواهی‌های صادر شده توسط Let’s Encrypt استفاده می‌کند.
  • نسخه‌های امن TLS را فعال می‌کند.
  • درخواست‌ها را به دو سرور Django ارسال می‌کند.
  • مسیر موردنیاز برای اعتبارسنجی Certbot را در اختیار Let’s Encrypt قرار می‌دهد.

بررسی بخش‌های مهم فایل پیکربندی Nginx

در بخش قبل فایل کامل nginx.conf را ایجاد کردیم. حالا بهتر است بدانیم هر قسمت از این فایل دقیقاً چه کاری انجام می‌دهد.


معرفی گواهی SSL

این دو دستور مسیر فایل‌های گواهی SSL و کلید خصوصی را مشخص می‌کنند:

ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;

این فایل‌ها در مرحله بعد توسط Certbot از Let’s Encrypt دریافت می‌شوند و هنگام اجرای کانتینر Nginx داخل آن Mount خواهند شد.


تنظیمات امنیتی TLS

تنظیمات زیر مربوط به امنیت ارتباط HTTPS هستند:

ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_session_tickets off;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;

ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";

این مقادیر توسط پروژه Certbot پیشنهاد شده‌اند و باعث می‌شوند:

  • فقط نسخه‌های امن TLS فعال باشند.
  • Cipherهای ضعیف غیرفعال شوند.
  • امنیت ارتباط HTTPS افزایش پیدا کند.

در اکثر پروژه‌ها نیازی به تغییر این بخش نیست.


محدودیت حجم درخواست‌ها

client_max_body_size 4G;
keepalive_timeout 5;

این دو تنظیم عملکرد زیر را دارند:

  • client_max_body_size حداکثر اندازه فایل ارسالی کاربران را مشخص می‌کند.
  • keepalive_timeout مدت زمان باز ماندن اتصال بین کاربر و Nginx را تعیین می‌کند.

در صورت نیاز می‌توانید مقدار client_max_body_size را متناسب با نیاز پروژه خود تغییر دهید.


ارسال درخواست‌ها به Django

مهم‌ترین بخش فایل، قسمت زیر است:

location / {

    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $http_host;

    proxy_redirect off;

    proxy_pass http://django;

}

هر درخواستی که به سایت ارسال شود، از این بخش عبور می‌کند.

در این قسمت:

  • IP واقعی کاربر برای Django ارسال می‌شود.
  • پروتکل مورد استفاده (HTTP یا HTTPS) حفظ می‌شود.
  • مقدار Host اصلی تغییر نمی‌کند.
  • درخواست به گروه سرورهای django که در ابتدای فایل تعریف کردیم ارسال می‌شود.

به این ترتیب، کاربران مستقیماً به سرورهای Django متصل نمی‌شوند و تمام درخواست‌ها ابتدا توسط Nginx پردازش خواهند شد.


مسیر موردنیاز برای Let’s Encrypt

در انتهای فایل، این بخش قرار دارد:

location ^~ /.well-known/acme-challenge/ {
    root /var/www/html;
}

این مسیر هنگام صدور یا تمدید گواهی SSL توسط Let’s Encrypt استفاده می‌شود.

در فرآیند اعتبارسنجی دامنه، Certbot فایل‌هایی را داخل این مسیر قرار می‌دهد و سرورهای Let’s Encrypt با بررسی آن‌ها مالکیت دامنه را تأیید می‌کنند.


اجرای اولین کانتینر Nginx

اکنون می‌توانیم کانتینر Nginx را اجرا کنیم.

توجه: در اولین اجرا، کانتینر با خطا متوقف خواهد شد؛ زیرا هنوز گواهی SSL ایجاد نشده است. این موضوع کاملاً طبیعی است.

دستور زیر را اجرا کنید:

docker run --rm --name nginx \
    -p 80:80 \
    -p 443:443 \
    -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \
    -v /var/www/html:/var/www/html \
    nginx:1.19.0

در این دستور:

  • پورت‌های 80 و 443 سرور به کانتینر متصل می‌شوند.
  • فایل nginx.conf داخل کانتینر Mount می‌شود.
  • پوشه /var/www/html نیز برای استفاده Certbot در اختیار کانتینر قرار می‌گیرد.
  • از ایمیج رسمی Nginx نسخه 1.19.0 استفاده می‌شود.

Docker ابتدا ایمیج Nginx را دانلود می‌کند و سپس سعی می‌کند سرویس را اجرا کند.

اما چون هنوز فایل‌های زیر وجود ندارند:

/etc/letsencrypt/live/your_domain.com/fullchain.pem
/etc/letsencrypt/live/your_domain.com/privkey.pem

Nginx با خطا متوقف خواهد شد.

این خطا کاملاً طبیعی است، زیرا در مرحله بعد با استفاده از Certbot گواهی SSL را دریافت کرده و سپس Nginx را مجدداً اجرا خواهیم کرد.


مرحله چهارم — دریافت گواهی SSL با Certbot و تنظیم تمدید خودکار

اکنون که Nginx آماده شده است، باید گواهی SSL را از Let’s Encrypt دریافت کنیم.

برای این کار از Certbot استفاده می‌کنیم؛ ابزاری که توسط بنیاد Electronic Frontier Foundation (EFF) توسعه داده شده و امکان دریافت و تمدید رایگان گواهی‌های TLS/SSL را فراهم می‌کند.

از آنجا که Docker روی سرور Proxy نصب است، از نسخه Dockerized برنامه Certbot استفاده خواهیم کرد.


بررسی رکورد DNS

قبل از هر چیز مطمئن شوید که دامنه شما به IP عمومی سرور Proxy اشاره می‌کند.

برای مثال:

your_domain.com  --->  Proxy Server Public IP

تا زمانی که این رکورد به‌درستی تنظیم نشده باشد، Let’s Encrypt قادر به تأیید مالکیت دامنه نخواهد بود.


دریافت گواهی آزمایشی (Staging)

ابتدا یک گواهی آزمایشی دریافت می‌کنیم تا مطمئن شویم همه‌چیز به‌درستی پیکربندی شده است.

دستور زیر را اجرا کنید:

docker run -it --rm -p 80:80 --name certbot \
    -v "/etc/letsencrypt:/etc/letsencrypt" \
    -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
    certbot/certbot certonly --standalone --staging \
    -d your_domain.com

در این دستور:

  • پورت 80 در اختیار Certbot قرار می‌گیرد.
  • پوشه‌های مربوط به گواهی‌ها روی سیستم Mount می‌شوند.
  • گزینه --standalone باعث می‌شود Certbot بدون نیاز به Nginx اجرا شود.
  • گزینه --staging از سرور آزمایشی Let’s Encrypt استفاده می‌کند و محدودیت‌های صدور گواهی واقعی را شامل نمی‌شود.

تکمیل مراحل Certbot

در اولین اجرا، Certbot چند سؤال از شما خواهد پرسید:

  • آدرس ایمیل
  • موافقت با قوانین استفاده (Terms of Service)

پس از تأیید دامنه، خروجی مشابه زیر نمایش داده می‌شود:

Congratulations!

Your certificate and chain have been saved at:

/etc/letsencrypt/live/your_domain.com/fullchain.pem

Your key file has been saved at:

/etc/letsencrypt/live/your_domain.com/privkey.pem

این پیام نشان می‌دهد که گواهی آزمایشی با موفقیت ایجاد شده است.


بررسی فایل گواهی

برای اطمینان از ایجاد شدن فایل‌ها می‌توانید دستور زیر را اجرا کنید:

sudo cat /etc/letsencrypt/live/your_domain.com/fullchain.pem

در صورت مشاهده محتوای گواهی، همه‌چیز به‌درستی انجام شده است.


اجرای مجدد Nginx

اکنون که فایل‌های SSL ایجاد شده‌اند، دوباره کانتینر Nginx را اجرا می‌کنیم:

docker run --rm --name nginx \
    -p 80:80 \
    -p 443:443 \
    -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \
    -v /etc/letsencrypt:/etc/letsencrypt \
    -v /var/lib/letsencrypt:/var/lib/letsencrypt \
    -v /var/www/html:/var/www/html \
    nginx:1.19.0

برخلاف اجرای قبلی، این بار Nginx بدون خطا اجرا خواهد شد؛ زیرا فایل‌های SSL در مسیرهای موردنیاز وجود دارند.


بررسی HTTPS

اکنون مرورگر را باز کرده و وارد آدرس زیر شوید:

http://your_domain.com

مرورگر شما احتمالاً هشدار امنیتی نمایش خواهد داد.

این موضوع طبیعی است، زیرا هنوز از گواهی آزمایشی (Staging Certificate) استفاده می‌کنیم که توسط مرورگرها معتبر شناخته نمی‌شود.

همچنین مشاهده خواهید کرد که درخواست HTTP به‌صورت خودکار به HTTPS هدایت شده است.


دریافت گواهی اصلی (Production)

اکنون باید گواهی واقعی را دریافت کنیم.

ابتدا اجرای Nginx را با فشردن کلیدهای زیر متوقف کنید:

CTRL + C

سپس همین دستور را اجرا کنید، با این تفاوت که گزینه --staging را حذف می‌کنیم:

docker run -it --rm -p 80:80 --name certbot \
    -v "/etc/letsencrypt:/etc/letsencrypt" \
    -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
    certbot/certbot certonly --standalone \
    -d your_domain.com

اگر Certbot از شما پرسید که گواهی قبلی حفظ شود یا جایگزین گردد، گزینه 2 را انتخاب کرده و Enter بزنید.

در پایان، گواهی معتبر Let’s Encrypt روی سرور شما نصب خواهد شد.


اجرای Nginx با گواهی اصلی

پس از دریافت گواهی معتبر، دوباره Nginx را اجرا کنید:

docker run --rm --name nginx \
    -p 80:80 \
    -p 443:443 \
    -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \
    -v /etc/letsencrypt:/etc/letsencrypt \
    -v /var/lib/letsencrypt:/var/lib/letsencrypt \
    -v /var/www/html:/var/www/html \
    nginx:1.19.0

اکنون مرورگر را باز کرده و وارد آدرس زیر شوید:

http://your_domain.com

در نوار آدرس مرورگر مشاهده خواهید کرد که درخواست HTTP به‌صورت خودکار به HTTPS هدایت می‌شود.

از آنجایی که برنامه Polls برای مسیر / صفحه‌ای تعریف نکرده است، احتمالاً با خطای 404 Page Not Found از Django مواجه خواهید شد که کاملاً طبیعی است.

اکنون مسیر زیر را باز کنید:

https://your_domain.com/polls

در صورتی که همه مراحل را به‌درستی انجام داده باشید، صفحه برنامه Polls نمایش داده خواهد شد.

در این مرحله:

  • ارتباط سایت با HTTPS برقرار شده است.
  • گواهی معتبر Let’s Encrypt نصب شده است.
  • Nginx درخواست‌ها را بین دو سرور Django توزیع می‌کند.

تمدید خودکار گواهی SSL

گواهی‌های صادر شده توسط Let’s Encrypt تنها ۹۰ روز اعتبار دارند؛ بنابراین باید پیش از پایان اعتبار، آن‌ها را تمدید کنید.

از آنجا که اکنون Nginx در حال اجراست، دیگر نباید Certbot را با حالت standalone اجرا کنید. در این مرحله از حالت Webroot استفاده می‌کنیم.

در این روش، Certbot فایل تأیید دامنه را در مسیر زیر ایجاد می‌کند:

/var/www/html/.well-known/acme-challenge/

و همان‌طور که در فایل nginx.conf تعریف کردیم، Nginx این درخواست‌ها را مستقیماً در اختیار Let’s Encrypt قرار می‌دهد.


اجرای Nginx در پس‌زمینه

ابتدا اجرای فعلی Nginx را با فشردن کلیدهای زیر متوقف کنید:

CTRL + C

سپس آن را در حالت Detached اجرا کنید:

docker run --rm --name nginx -d \
    -p 80:80 \
    -p 443:443 \
    -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \
    -v /etc/letsencrypt:/etc/letsencrypt \
    -v /var/lib/letsencrypt:/var/lib/letsencrypt \
    -v /var/www/html:/var/www/html \
    nginx:1.19.0

اکنون Nginx در پس‌زمینه اجرا می‌شود و با خروج از SSH نیز متوقف نخواهد شد.


تست فرآیند تمدید گواهی

برای اطمینان از صحت تنظیمات، ابتدا یک تست آزمایشی انجام دهید:

docker run -it --rm --name certbot \
    -v "/etc/letsencrypt:/etc/letsencrypt" \
    -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
    -v "/var/www/html:/var/www/html" \
    certbot/certbot renew \
    --webroot \
    -w /var/www/html \
    --dry-run

در این دستور:

  • از افزونه webroot استفاده شده است.
  • مسیر فایل‌های اعتبارسنجی مشخص شده است.
  • گزینه --dry-run فقط فرآیند تمدید را شبیه‌سازی می‌کند و تغییری در گواهی واقعی ایجاد نخواهد کرد.

اگر همه‌چیز به‌درستی پیکربندی شده باشد، خروجی مشابه زیر را مشاهده خواهید کرد:

Congratulations, all renewals succeeded.

این پیام نشان می‌دهد که تمدید گواهی در زمان مقرر بدون مشکل انجام خواهد شد.


بارگذاری مجدد تنظیمات Nginx

پس از تمدید گواهی، لازم است Nginx فایل‌های جدید را بارگذاری کند.

برای این کار کافی است دستور زیر را اجرا کنید:

docker kill -s HUP nginx

این دستور سیگنال HUP را به پردازش Nginx ارسال می‌کند.

در نتیجه، بدون نیاز به توقف یا راه‌اندازی مجدد کانتینر، Nginx فایل‌های پیکربندی و گواهی SSL جدید را بارگذاری خواهد کرد و سرویس بدون قطعی به کار خود ادامه می‌دهد.


مرحله پنجم — جلوگیری از دسترسی مستقیم به سرورهای Django

در معماری این آموزش، تمام ارتباط HTTPS در سرور Nginx خاتمه پیدا می‌کند (SSL Termination).

به عبارت دیگر:

  • کاربر از طریق HTTPS به Nginx متصل می‌شود.
  • Nginx ترافیک رمزنگاری‌شده را رمزگشایی می‌کند.
  • سپس درخواست را از طریق شبکه داخلی به سرورهای Django ارسال می‌کند.

برای بسیاری از پروژه‌ها، این روش کاملاً مناسب و ایمن است.

اگر در حال توسعه سامانه‌هایی با داده‌های بسیار حساس مانند اطلاعات بانکی یا پزشکی هستید، می‌توانید از End-to-End Encryption استفاده کنید تا ارتباط بین Nginx و سرورهای Django نیز رمزنگاری شود. البته پیاده‌سازی این معماری خارج از محدوده این آموزش است.


چرا باید دسترسی مستقیم به سرورهای Django را مسدود کنیم؟

در این معماری، کاربران نباید بتوانند مستقیماً به سرورهای Django متصل شوند.

مسیر صحیح درخواست‌ها باید به شکل زیر باشد:

Client
    │
 HTTPS
    │
Nginx Reverse Proxy
    │
───────────────
│             │
Django 1   Django 2

اگر کاربر بتواند مستقیماً وارد یکی از سرورهای Django شود:

  • Load Balancer دور زده می‌شود.
  • HTTPS دور زده می‌شود.
  • امنیت معماری کاهش پیدا می‌کند.

مشکل Docker و UFW

در ابتدای آموزش اشاره کردیم که Docker به‌صورت پیش‌فرض برخی از قوانین UFW را نادیده می‌گیرد.

برای مثال، هنگام اجرای دستور زیر:

docker run -p 80:8000 polls

پورت 80 سرور مستقیماً در اینترنت باز می‌شود؛ حتی اگر در UFW قانونی برای آن تعریف نکرده باشید.

به همین دلیل توصیه می‌شود:

  • از فایروال ابری Parmin Cloud استفاده کنید.
  • یا قوانین iptables را مستقیماً مدیریت کنید.
  • یا تنظیمات UFW را اصلاح کنید.

در این آموزش از روش سوم استفاده می‌کنیم.


ویرایش فایل after.rules

ابتدا وارد اولین سرور Django شوید.

سپس فایل زیر را باز کنید:

sudo nano /etc/ufw/after.rules

در انتهای فایل، قبل از خط COMMIT یا بعد از قوانین موجود، بلوک زیر را اضافه کنید:

# BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:DOCKER-USER - [0:0]

-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16

-A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN

-A DOCKER-USER -j ufw-user-forward

-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12

-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12

-A DOCKER-USER -j RETURN
COMMIT
# END UFW AND DOCKER

این قوانین چه کاری انجام می‌دهند؟

این مجموعه قوانین باعث می‌شود:

  • ارتباطات داخلی شبکه خصوصی مجاز باشند.
  • ارتباط بین سرور Proxy و سرورهای Django برقرار بماند.
  • کاربران اینترنت نتوانند مستقیماً به پورت‌هایی که Docker باز کرده است دسترسی داشته باشند.

در نتیجه تنها Nginx قادر خواهد بود با سرورهای Django ارتباط برقرار کند.

اگر از شبکه خصوصی Parmin Cloud (VPC) استفاده می‌کنید، این تنظیمات بهترین گزینه هستند؛ زیرا ارتباط داخلی همچنان برقرار خواهد بود.


اگر از IP عمومی استفاده می‌کنید

اگر در فایل nginx.conf به‌جای IP خصوصی، IP عمومی سرورهای Django را در بخش upstream قرار داده‌اید، باید علاوه بر تنظیمات بالا، یک قانون در UFW ایجاد کنید تا فقط سرور Nginx بتواند به پورت 80 سرورهای Django متصل شود.

به این ترتیب، دسترسی کاربران عادی همچنان مسدود خواهد بود.


اعمال تنظیمات

پس از ذخیره فایل، سرویس UFW را مجدداً راه‌اندازی کنید:

sudo systemctl restart ufw

بررسی نتیجه

اکنون در مرورگر خود آدرس زیر را باز کنید:

http://APP_SERVER_1_IP

در صورتی که تنظیمات به‌درستی انجام شده باشد، دیگر نباید بتوانید مستقیماً به سرور Django متصل شوید.


تکرار مراحل برای سرور دوم

تمام مراحل بالا را روی دومین سرور Django نیز انجام دهید:

sudo nano /etc/ufw/after.rules

بلوک قوانین را اضافه کنید و سپس:

sudo systemctl restart ufw

تست نهایی

در پایان، آدرس دامنه خود را باز کنید:

https://your_domain.com/polls

اگر صفحه برنامه Polls بدون مشکل نمایش داده شد، یعنی:

  • کاربران دیگر دسترسی مستقیمی به سرورهای Django ندارند.
  • تمام درخواست‌ها از طریق Nginx عبور می‌کنند.
  • ارتباط HTTPS برقرار است.
  • درخواست‌ها بین دو سرور Django به‌صورت Load Balanced توزیع می‌شوند.

جمع‌بندی

در این آموزش یاد گرفتیم چگونه یک برنامه Django را به‌صورت مقیاس‌پذیر و ایمن، با استفاده از Docker، Nginx و Let’s Encrypt راه‌اندازی کنیم.

در طول مسیر:

  • دو سرور مجزا برای اجرای Django و Gunicorn راه‌اندازی کردیم.
  • یک سرور Proxy با Nginx ایجاد کردیم تا درخواست‌ها را بین سرورها توزیع کند.
  • با استفاده از Certbot گواهی SSL رایگان از Let’s Encrypt دریافت کردیم.
  • ارتباط HTTPS را فعال کردیم و امکان تمدید خودکار گواهی‌ها را فراهم ساختیم.
  • در نهایت، دسترسی مستقیم کاربران به سرورهای Django را مسدود کردیم تا تمام درخواست‌ها تنها از طریق Nginx پردازش شوند.

این معماری چگونه مقیاس‌پذیر می‌شود؟

یکی از مزیت‌های این ساختار، امکان توسعه مستقل هر بخش از سیستم است.

با افزایش تعداد کاربران، می‌توانید هر لایه را جداگانه مقیاس‌دهی کنید:

  • اضافه کردن سرورهای بیشتر برای اجرای Django
  • افزایش تعداد سرورهای Nginx یا استفاده از Load Balancer در لایه Proxy
  • ارتقاء یا توزیع پایگاه داده PostgreSQL

به این ترتیب بدون ایجاد تغییر اساسی در معماری، ظرفیت سرویس به‌مرور افزایش پیدا خواهد کرد.


بهبود مدیریت کانتینرها

در این آموزش کانتینرها را به‌صورت دستی اجرا کردیم، اما در محیط Production بهتر است از مکانیزم‌هایی استفاده کنید که در صورت ریبوت شدن سرور یا توقف کانتینر، سرویس به‌صورت خودکار مجدداً اجرا شود.

برای این منظور می‌توانید از یکی از گزینه‌های زیر استفاده کنید:

  • قابلیت Restart Policy در Docker
  • سرویس‌های systemd
  • ابزارهای Orchestration مانند Docker Compose یا Kubernetes

خودکارسازی استقرار

اگر قرار باشد همین زیرساخت را روی چندین سرور راه‌اندازی کنید، اجرای دستی تمام مراحل زمان‌بر و مستعد خطاست.

در چنین شرایطی استفاده از ابزارهای مدیریت پیکربندی مانند موارد زیر توصیه می‌شود:

  • Ansible
  • Chef
  • Puppet

این ابزارها امکان پیاده‌سازی یکسان تنظیمات روی تعداد زیادی سرور را فراهم می‌کنند.


استفاده از رجیستری خصوصی ایمیج‌ها

در این آموزش ایمیج Docker روی هر سرور به‌صورت جداگانه ساخته شد.

در پروژه‌های واقعی، معمولاً ایمیج یک‌بار ساخته شده و داخل یک Registry ذخیره می‌شود تا تمام سرورها همان نسخه را دریافت کنند.

در زیرساخت Parmin Cloud می‌توانید از Mirrorهای رجیستری استفاده کنید تا دانلود ایمیج‌ها با سرعت بیشتر و پایداری بالاتری انجام شود.

نمونه‌هایی از این Mirrorها عبارت‌اند از:

hub.cr.parmincloud.ir
ghcr.cr.parmincloud.ir
quay.cr.parmincloud.ir
gcr.cr.parmincloud.ir
k8s.cr.parmincloud.ir
gitlab.cr.parmincloud.ir

همچنین برای افزایش سرعت نصب وابستگی‌ها می‌توانید از Mirrorهای اختصاصی پارمین کلود استفاده کنید:

Python (PyPI)

https://pypi.parmincloud.ir/simple/

Node.js (NPM)

https://npm.parmincloud.ir

Composer

https://composer.parmincloud.ir
از همراهی شما با پارمین کلود سپاسگزاریم.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

همچنین ببینید
بستن
دکمه بازگشت به بالا