وب‌سرور

راه‌اندازی سایت با Nginx روی اوبونتو — از نصب تا HTTPS رایگان

راه‌اندازی سایت با Nginx روی اوبونتو: نمایی از ترمینال سرور اوبونتو در کنار بلوک پیکربندی server، فعال‌سازی گواهی رایگان SSL با Certbot، اتصال PHP-FPM و روشن‌کردن فشرده‌سازی و کش فایل‌های ایستا

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

پیش‌نیازها: قبل از شروع چه چیزی باید آماده باشد؟

  • یک سرور اوبونتو (۲۲.۰۴، ۲۴.۰۴ یا ۲۶.۰۴) با دسترسی root؛
  • دسترسی SSH با کاربری که sudo دارد (راهنمای کامل: اتصال به سرور با SSH
  • یک دامنه که رکورد A آن به آی‌پی سرور اشاره کند (نمی‌دانید چطور؟ آموزش رکوردهای DNS).

سومی را جدی بگیرید: بخش بزرگی از شکست‌های گواهی SSL از همین‌جاست که دامنه هنوز به آی‌پی سرور resolve نمی‌شود. از خودِ سرور تست کنید — dig روی نصب استاندارد نیست و با sudo apt install bind9-dnsutils -y می‌آید:

dig +short example.ir

203.0.113.45

اگر خروجی خالی بود یا آی‌پی دیگری برگشت، وقت ادامه نرسیده: رکورد A را اصلاح کنید و به اندازه‌ی TTL صبر کنید (در DNS مدیریت‌شده‌ی مهران هاست حداقل TTL شصت ثانیه است).

کدام نسخه‌ی اوبونتو؟ تفاوت‌هایی که روی پیکربندی اثر می‌گذارند

نسخه‌ای که انتخاب می‌کنید تعیین می‌کند چه Nginx و PHP‌ای می‌گیرید و — نکته‌ای که خیلی‌ها را غافلگیر می‌کند — با چه دستوری HTTP/2 را روشن می‌کنید:

نسخه‌ی اوبونتو Nginx در مخزن PHP پیش‌فرض روش فعال‌کردن HTTP/2 پایان پشتیبانی استاندارد
۲۲.۰۴ LTS1.18.08.1listen 443 ssl http2;می ۲۰۲۷
۲۴.۰۴ LTS1.24.08.3listen 443 ssl http2;می ۲۰۲۹
۲۶.۰۴ LTS1.28.38.5دستور مستقل http2 on;می ۲۰۳۱

ستون چهارم منبع یکی از رایج‌ترین سردرگمی‌هاست: Nginx از 1.25.1 پارامتر http2 روی listen را منسوخ و http2 on; را جایگزین کرد. یعنی روی ۲۶.۰۴ نوشتن listen 443 ssl http2; هشدار می‌دهد، و برعکس http2 on; روی دو نسخه‌ی قدیمی‌تر پیکربندی را می‌شکند.

درباره‌ی HTTP/3 نسخه تعیین‌کننده است. روی ۲۲.۰۴ و ۲۴.۰۴ در دسترس نیست: 1.18 و 1.24 قدیمی‌تر از 1.25.0 هستند که QUIC از آن شروع شد، و بسته‌ی ۲۴.۰۴ با پرچم --with-http_v3_module ساخته نشده. اما روی ۲۶.۰۴ ورق برگشته: دبیان این پرچم را از 1.26.0-2 فعال کرده و بسته‌ی 1.28.3 آن را دارد — پس برای h3 لازم نیست سراغ مخزن mainline خودِ nginx.org بروید. آپ‌استریم هنوز آن را آزمایشی می‌داند، پس بسته‌ی خودتان را بررسی کنید؛ خروجی خالی یعنی در دسترس نیست:

nginx -V 2>&1 | grep -o with-http_v3_module
⚠️ پیش از روشن‌کردن HTTP/3 برای مخاطب داخل ایران: گذرگاه بین‌الملل دست‌دادن‌های QUIC نسخه‌ی ۱ را بی‌صدا می‌اندازد، در حالی که همان آی‌پی و پورت با TCP بی‌مشکل پاسخ می‌دهد. مرورگر منتظر می‌ماند و به TCP برمی‌گردد — h3 برای این بازدیدکننده تأخیر می‌آورد، نه سرعت.

قدم ۱: نصب Nginx روی اوبونتو

sudo apt update && sudo apt install nginx -y
sudo systemctl enable --now nginx

خط دوم اطمینان‌خاطر است، نه ضرورت: بسته‌ی اوبونتو خودش سرویس را استارت و برای بوت فعال می‌کند. اما در ایمیج‌های ابری و کانتینرهایی که با policy-rc.d جلوی استارت خودکار را می‌گیرند، این اتفاق نمی‌افتد:

systemctl status nginx --no-pager | head -6

● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
     Active: active (running) since Mon 2026-09-07 10:12:41 UTC; 8s ago
       Docs: man:nginx(8)
   Main PID: 1841 (nginx)
      Tasks: 3 (limit: 4613)

(روی ۲۲.۰۴ مسیر یونیت /lib/systemd/system/ است.) آی‌پی سرور را در مرورگر باز کنید: صفحه‌ی «Welcome to nginx» یعنی وب‌سرور زنده است. اگر UFW فعال دارید:

sudo ufw allow 'Nginx Full'

Rule added
Rule added (v6)

sudo ufw status

Status: active
To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
Nginx Full                 ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)
Nginx Full (v6)            ALLOW       Anywhere (v6)

پروفایل Nginx Full هر دو پورت 80 و 443 را باز می‌کند (Nginx HTTP فقط 80، Nginx HTTPS فقط 443). وسوسه‌ی بستن پورت 80 به‌نام «امن‌تر بودن» را کنار بگذارید؛ صدور گواهی رایگان به آن نیاز دارد.

⚠️ اگر UFW را روی سروری که با SSH به آن وصل هستید تازه فعال می‌کنید، پیش از هر چیز sudo ufw allow OpenSSH را بزنید. فایروال با سیاست پیش‌فرضِ بستن ورودی، بدون این قاعده ارتباط شما را همان لحظه قطع می‌کند و بازگشت فقط از کنسول ممکن است.

قدم ۲: پوشه‌ی سایت و مجوزهایی که بعداً دردسر نسازند

sudo mkdir -p /var/www/example.ir/html
sudo tee /var/www/example.ir/html/index.html >/dev/null <<'EOF'
<!doctype html>
<meta charset="utf-8">
<h1>سلام دنیا! 🚀</h1>
EOF

به‌جای example.ir دامنه‌ی خودتان را بگذارید. ساختار /var/www/domain/html اجباری نیست اما قرارداد خوبی است: پوشه‌های جانبی مثل لاگ اپ کنارِ html می‌نشینند، بیرون از وب.

حالا مالکیت و مجوزها — بخشی که خیلی از آموزش‌ها با یک chmod 777 از رویش رد می‌شوند؛ مجوزی که دیگر پیکربندی نیست، دعوت‌نامه است. قاعده‌ی درست: کاربر وب‌سرور فقط بخوانَد و مالکِ فایل‌ها کسِ دیگری باشد:

sudo chown -R $USER:www-data /var/www/example.ir
sudo find /var/www/example.ir -type d -exec chmod 750 {} \;
sudo find /var/www/example.ir -type f -exec chmod 640 {} \;

شما مالک هستید و می‌نویسید، و گروه www-data — کاربری که کارگرهای Nginx و PHP-FPM با آن اجرا می‌شوند — فقط می‌خواند. آموزش‌هایی که کل سایت را به www-data می‌سپارند همین محافظ را از بین می‌برند: وقتی وب‌سرور مالک است، هر اسکریپت PHP — از جمله وب‌شل آپلودشده — کد سایت را بازنویسی می‌کند. اگر اپ به نوشتن نیاز دارد فقط همان پوشه را استثنا کنید:

sudo chown -R www-data:www-data /var/www/example.ir/html/wp-content/uploads

قدم ۳: تعریف سایت در Nginx با بلوک server

Nginx روی اوبونتو قرارداد دوپوشه‌ای دارد: پیکربندی سایت‌ها در /etc/nginx/sites-available/ نوشته و با یک لینک نمادین در /etc/nginx/sites-enabled/ فعال می‌شود — یعنی غیرفعال‌کردن موقت یک سایت فقط حذف یک لینک است. پس بلوک زیر را با sudo nano /etc/nginx/sites-available/example.ir بنویسید — هم‌نام‌کردن فایل با دامنه قرارداد است، نه اجبار:

server {
    listen 80;
    listen [::]:80;

    server_name example.ir www.example.ir;
    root /var/www/example.ir/html;
    index index.html index.php;
    charset utf-8;

    access_log /var/log/nginx/example.ir.access.log;
    error_log  /var/log/nginx/example.ir.error.log;

    location / {
        try_files $uri $uri/ =404;
    }
}

سه نکته. خط listen [::]:80; همان پورت را روی IPv6 هم باز می‌کند؛ بدون آن بازدیدکننده‌ی IPv6 به بلوک شما نمی‌رسد: تا وقتی سایت default فعال است همان «Welcome to nginx» را می‌گیرد، وگرنه مرورگر بعد از مکثی به IPv4 برمی‌گردد — یعنی سایت اشتباه یا کندی، نه خطای واضح. server_name تعیین می‌کند بلوک به کدام نام‌ها پاسخ دهد — برای همین هر دو صورتِ با و بدون www آمده. و charset utf-8; را جدی بگیرید: Nginx پیش‌فرض هیچ charset در هدر Content-Type اعلام نمی‌کند. صفحه‌ی HTML با تگ meta charset سالم می‌ماند، اما فایل .txt، خروجی مستقیم PHP یا پاسخ JSON بدون هدر صریح با حدسِ مرورگر خوانده می‌شود و فارسی به‌هم می‌ریزد.

حالا با یک لینک نمادین فعالش کنید و پیکربندی را تست بگیرید:

sudo ln -s /etc/nginx/sites-available/example.ir /etc/nginx/sites-enabled/
sudo nginx -t

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

sudo systemctl reload nginx

nginx -t را به عادت تبدیل کنید؛ پیکربندی خراب را قبل از اعمال لو می‌دهد. بین reload و restart هم تفاوت هست: اولی پیکربندی جدید را بدون قطع اتصال‌های در جریان به کارگرها می‌دهد، دومی سرویس را پایین و بالا می‌کند.

💡 سایت پیش‌فرض را فراموش نکنید: اوبونتو فایلی به نام default در sites-enabled دارد که با پرچم default_server به هر درخواستی که با هیچ server_name نخورَد پاسخ می‌دهد؛ یعنی هر دامنه‌ی اشتباهی که به سرورتان اشاره کند «Welcome to nginx» می‌گیرد. یا حذفش کنید، یا بلوک خودتان را default_server اعلام کنید.

قدم ۴: HTTPS رایگان با Let's Encrypt

Let's Encrypt گواهی مورد اعتماد همه‌ی مرورگرها را رایگان صادر می‌کند و ابزار رسمی خودکارسازی‌اش Certbot نام دارد. دو راه نصب دارید:

# راه ساده — از مخزن خود اوبونتو
sudo apt install certbot python3-certbot-nginx -y

# راه توصیه‌شده‌ی خود پروژه — نسخه‌ی همیشه به‌روز
# اگر قبلاً نسخه‌ی apt را نصب کرده‌اید، طبق دستور رسمی Certbot اول حذفش کنید
sudo apt-get remove certbot -y
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/local/bin/certbot

تفاوت در تازگی نسخه است: بسته‌ی اوبونتو ۲۴.۰۴ روی 2.9.0 قفل است، اما snap خودش به‌روز می‌شود و تیم Certbot رسماً همان را توصیه می‌کند. آن خط حذف هم تشریفاتی نیست: با هر دو نصب، زمان‌بندِ apt جداگانه روی همان /etc/letsencrypt اجرا می‌شود و دو نسخه‌ی Certbot سراغ یک گواهی می‌روند. سپس:

sudo certbot --nginx -d example.ir -d www.example.ir

Certbot ایمیل و پذیرش شرایط را می‌پرسد، بعد اعتبارسنجی HTTP-01 را اجرا می‌کند: فایلی موقت زیر /.well-known/acme-challenge/ می‌گذارد که سرورهای Let's Encrypt از پورت 80 می‌خوانند. سپس گواهی را نصب، بلوک را برای پورت 443 بازنویسی و ریدایرکت HTTP به HTTPS را ایجاد می‌کند:

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/example.ir/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/example.ir/privkey.pem
This certificate expires on 2026-12-06.
These files will be updated when the certificate renews.

Deploying certificate
Successfully deployed certificate for example.ir to /etc/nginx/sites-enabled/example.ir
Successfully deployed certificate for www.example.ir to /etc/nginx/sites-enabled/example.ir

خط‌های پایانی را بشمارید: به‌ازای هر نامی که با -d داده‌اید باید یک خط deploy ببینید. اگر یکی کم بود، همان نام اعتبارسنجی نشده است.

💡 نکته: اگر سایت پشت CDN است، مسیر /.well-known/ هم باید بدون مانع به سرور مبدأ برسد، وگرنه اعتبارسنجی شکست می‌خورد.

Let's Encrypt سقف‌های نرخی دارد که در آزمون‌وخطا زود پر می‌شوند: ۵۰ گواهی برای هر دامنه‌ی ثبت‌شده در هفت روز، ۵ گواهی برای مجموعه‌ی دقیقاً یکسانی از نام‌ها در هفت روز، و ۵ شکست اعتبارسنجی برای هر نام در هر ساعت. پس هنگام آزمایش --dry-run بزنید که سهمیه را نمی‌سوزاند. وایلدکارت و اعتبارسنجی DNS در راهنمای کامل نصب گواهی رایگان SSL با Certbot آمده است.

کاری که Certbot برایتان انجام نمی‌دهد: روشن‌کردن HTTP/2

Certbot بلوک 443 را می‌سازد اما HTTP/2 را فعال نمی‌کند؛ باید دستی اضافه کنید، به شکلی که در جدول بالا دیدید: روی ۲۶.۰۴ خط http2 on; داخل بلوک 443، و روی نسخه‌های قدیمی‌تر پارامتر چسبیده به listen. بعد nginx -t، ری‌لود و تأیید:

curl -sI --http2 https://example.ir | head -1

HTTP/2 200

تمدید خودکار گواهی کجا بی‌صدا می‌شکند؟

گواهی‌های Let's Encrypt تا ۱۰ فوریه ۲۰۲۷ نود روز اعتبار دارند و بعد از آن ۶۴ روز، و قرار است بدون دخالت شما تمدید شوند. Certbot هنگام نصب زمان‌بندی می‌سازد که روزی دو بار اجرا می‌شود و هر گواهی را با ماندن یک‌سوم عمرش تمدید می‌کند:

systemctl list-timers | grep certbot

Mon 2026-09-07 21:47:00 UTC  11h left  Mon 2026-09-07 09:12:03 UTC  1h ago  snap.certbot.renew.timer  snap.certbot.renew.service

sudo certbot renew --dry-run

Congratulations, all simulated renewals succeeded:
  /etc/letsencrypt/live/example.ir/fullchain.pem (success)

نام یونیت به روش نصب بستگی دارد: با snap زمان‌بند snap.certbot.renew.timer است و با apt نامش certbot.timer می‌شود. اگر هیچ‌کدام را ندیدید، تمدید خودکار وجود ندارد.

حالا بخش مهم‌تر: چرا این سازوکار گاهی بی‌صدا می‌شکند. Let's Encrypt سرویس ایمیل هشدار انقضا را در ژوئن ۲۰۲۵ تعطیل کرد؛ تور ایمنی قدیمی دیگر نیست: اگر تمدید یک شب شکست بخورد هیچ‌کس نمی‌گوید — تا وقتی بازدیدکننده‌ها هشدار قرمز مرورگر را ببینند.

دلایلش قابل‌پیشگیری‌اند: پورت 80 که بعداً روی فایروال بسته شده؛ رکورد A که با جابه‌جایی سرور عوض شده اما گواهی هنوز روی سرور قبلی تمدید می‌شود؛ یا بلوکی که دستی ویرایش شده و /.well-known/ را به ریدایرکتی سراسری فرستاده. پایش انقضا را به مانیتورینگ سرور اضافه کنید:

echo | openssl s_client -connect example.ir:443 -servername example.ir 2>/dev/null \
  | openssl x509 -noout -enddate

notAfter=Dec  6 08:14:22 2026 GMT
⚠️ Let's Encrypt عمر گواهی‌ها را پله‌پله کوتاه می‌کند و دو پله‌اش نزدیک است: پروفایل اختیاری tlsserver از ۱۳ مه ۲۰۲۶ گواهی ۴۵ روزه می‌دهد؛ از ۱۰ فوریه ۲۰۲۷ پیش‌فرضِ classic از ۹۰ به ۶۴ روز می‌رسد و از ۱۶ فوریه ۲۰۲۸ به ۴۵ روز؛ پروفایل shortlived هم گواهی شش‌روزه می‌دهد. Certbot که با «یک‌سوم عمر باقی‌مانده» حساب می‌کند وفق می‌دهد؛ تمدید دستی محکوم به شکست است.

قدم ۵ (اختیاری): PHP برای سایت‌های داینامیک

تا اینجا سایت فقط فایل ایستا سرو می‌کند. برخلاف آپاچی، Nginx خودش PHP را اجرا نمی‌کند؛ درخواست را با FastCGI به سرویس جداگانه‌ای به نام PHP-FPM می‌سپارد:

sudo apt install php8.3-fpm php8.3-mysql php8.3-mbstring php8.3-xml php8.3-curl -y
systemctl status php8.3-fpm --no-pager | head -3

● php8.3-fpm.service - The PHP 8.3 FastCGI Process Manager
     Loaded: loaded (/usr/lib/systemd/system/php8.3-fpm.service; enabled)
     Active: active (running)

نسخه را متناسب با اوبونتوی خودتان انتخاب کنید (جدول بالا) و داخل بلوک server این را اضافه کنید:

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

اسنیپت آماده‌ی snippets/fastcgi-php.conf بی‌ارزش نیست: علاوه بر پارامترهای استاندارد FastCGI، خط try_files $fastcgi_script_name =404; را دارد که پیش از فرستادن درخواست به مفسر مطمئن می‌شود فایل PHP وجود دارد — همین یک خط خانواده‌ای از حمله‌های قدیمی را می‌بندد که با مسیرهای دستکاری‌شده اسکریپت دلخواه اجرا می‌کردند.

سه تنظیمی که بعداً به آن‌ها برمی‌گردید

دو پیش‌فرض محافظه‌کارانه‌ی Nginx با اپ واقعی خودشان را نشان می‌دهند: سقف یک‌مگابایتی بدنه‌ی درخواست — آپلود بزرگ‌تر با HTTP 413 رد می‌شود — و مهلت شصت‌ثانیه‌ای پاسخ FastCGI. اما این سقف دوم رقیب دارد. max_execution_time در PHP پیش‌فرض سی ثانیه است، ولی روی لینوکس فقط زمان اجرای خودِ اسکریپت را می‌شمارد؛ کوئری دیتابیس، فراخوانی API و خواندن از سوکت در آن حساب نمی‌شوند. یعنی حلقه‌ی سنگینِ CPU اول به سقف PHP می‌خورد و HTTP 500 می‌دهد، اما کوئری کند — علت واقعی بیشتر ۵۰۴ها — تایمر PHP را جلو نمی‌برد و بعد از شصت ثانیه 504 از Nginx می‌گیرید.

client_max_body_size 32m;

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    fastcgi_read_timeout 120s;
}

همین قاعده برای آپلود هم برقرار است: سقفش دو جا تعریف شده — client_max_body_size در Nginx و upload_max_filesize و post_max_size در /etc/php/8.3/fpm/php.ini — و سخت‌گیرترینشان برنده است. برای مهلت ۱۲۰ ثانیه هم max_execution_time = 120 را همان‌جا بگذارید و PHP-FPM را جداگانه ری‌لود کنید؛ ری‌لود Nginx کافی نیست.

وردپرس و اپ‌های تک‌ورودی: تنظیم try_files

اگر وردپرس یا هر فریم‌ورک PHP مدرنی نصب کنید، احتمالاً صفحه‌ی اصلی درست کار می‌کند اما هر لینک داخلی خطای 404 می‌دهد. پیکربندی قدم سوم می‌گفت «اگر فایل یا پوشه‌ای با این آدرس نبود، 404 بده»، و آدرسی مثل /blog/nginx-setup/ فایلی روی دیسک ندارد؛ وردپرس باید آن را از index.php بسازد. آپاچی این کار را با .htaccess می‌کند که Nginx نمی‌خواندش:

location / {
    try_files $uri $uri/ /index.php?$args;
}

ترجمه‌اش: اول دنبال فایلی با همین نام بگرد، بعد پوشه‌ای با همین نام، و اگر نبود درخواست را با همان پارامترهای کوئری به index.php بسپار. برای لاراول همین الگو با ریشه‌ی public جواب می‌دهد. برای نصب کامل، راهنمای نصب وردپرس روی سرور را دنبال کنید.

سه بهینه‌سازی کوچک با اثر بزرگ

فشرده‌سازی gzip

در /etc/nginx/nginx.conf معمولاً فعال است؛ مطمئن شوید این خط‌ها از کامنت درآمده‌اند:

gzip on;
gzip_vary on;
gzip_min_length 256;
gzip_types text/css application/javascript application/json image/svg+xml;

حجم فایل‌های متنی به‌شکل چشمگیری کم می‌شود — هم سرعت، هم ترافیک خروجی که جداگانه بر اساس گیگابایت حساب می‌شود. ماژول ngx_brotli معمولاً یک قدم جلوتر می‌رود. دو نکته: text/html لازم نیست چون Nginx همیشه آن را فشرده می‌کند، و gzip_vary on; هدر Vary: Accept-Encoding را می‌فرستد تا کش‌های میانی دو نسخه را قاتی نکنند. تصویر و ویدیو را نگذارید؛ از قبل فشرده‌اند.

کش فایل‌های استاتیک

location ~* \.(css|js|png|jpg|jpeg|gif|svg|webp|woff2)$ {
    add_header Cache-Control "public, max-age=2592000, immutable";
    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options SAMEORIGIN;
    add_header Referrer-Policy strict-origin-when-cross-origin;
    add_header Strict-Transport-Security "max-age=300" always;
    access_log off;
}

این بلوک به مرورگر می‌گوید سی روز سراغ سرور نیاید و immutable یک قدم جلوتر می‌رود: در این بازه حتی درخواست اعتبارسنجی هم نفرست. شرطش نسخه‌دار بودن آدرس فایل‌هاست؛ اگر همیشه همان style.css را بازنویسی می‌کنید، کاربران قدیمی تا یک ماه نسخه‌ی کهنه را می‌بینند.

دو نکته درباره‌ی بلوک. expires 30d; را عمداً ننوشتیم: خودش هدر Cache-Control می‌سازد و با add_header پاسخ دو هدر متناقض می‌گیرد. و چهار هدر امنیتی عیناً تکرار شده‌اند — به دلیلِ تله‌ای که چند خط پایین‌تر می‌آید.

برای بردن همین کش تا نزدیک کاربر، اگر نِیم‌سرورهای دامنه را به مهران هاست سپرده باشید می‌توانید CDN مهران هاست را با یک کلید کنار همان رکورد A و بدون هزینه‌ی جداگانه روشن کنید؛ نودهای لبه در ایران و اروپا فایل ایستا را نگه می‌دارند و سرور شما فقط بار دینامیک را می‌کشد.

هدرهای امنیتی

add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header Referrer-Policy strict-origin-when-cross-origin;
add_header Strict-Transport-Security "max-age=300" always;

سه‌تای اول به‌ترتیب حدس‌زدن نوع محتوا، جاسازی سایت شما در فریمِ سایت دیگر و نشت اطلاعات هدر Referer را محدود می‌کنند. چهارمی — HSTS — به مرورگر می‌گوید این دامنه را فقط با HTTPS باز کن.

⚠️ دو تله در همین چند خط. اول: HSTS برگشت‌پذیر نیست؛ با max-age یک‌ساله، اگر HTTPS مشکل پیدا کند مرورگرهایی که هدر را دیده‌اند تا یک سال سایت را روی HTTP باز نمی‌کنند. با مقدار کوتاه شروع کنید. دوم: add_header ارث‌بری غافلگیرکننده‌ای دارد — اگر در یک بلوک location داخلی حتی یک add_header بنویسید، همه‌ی هدرهای سطح بالاتر آنجا کنار گذاشته می‌شوند. یعنی بلوک کش استاتیکِ بالا هر چهار هدر را خاموش می‌کند — برای همین عیناً تکرارشان کردیم. هدر تازه‌ای هم که بعداً به server اضافه کنید باید آنجا تکرار شود.

عیب‌یابی سریع: علامت، علت، اولین اقدام

پیش از هر حدسی sudo nginx -t بزنید و لاگ خطا را با sudo tail -f /var/log/nginx/error.log زنده تماشا کنید:

علامتعلت محتملاولین اقدام
502 Bad GatewayPHP-FPM خاموش است یا مسیر سوکت با نسخه‌ی PHP نمی‌خواندsystemctl status php8.3-fpm و مقایسه‌ی fastcgi_pass با محتوای /run/php/
403 Forbiddenمالکیت یا مجوز پوشه، یا نبودن فایل ایندکسبازبینی با ls -l و اطمینان از اینکه گروه www-data حق خواندن دارد
404 روی همه‌ی صفحه‌های داخلیقاعده‌ی بازنویسی برای اپ تک‌ورودی نوشته نشدهافزودن try_files با ارجاع نهایی به index.php در location /
فایل PHP دانلود می‌شود به‌جای اجرابلوک location ~ \.php$ وجود نداردافزودن بلوک FastCGI و ری‌لود Nginx
صفحه‌ی پیش‌فرض Nginxلینک sites-enabled ساخته نشده یا server_name نمی‌خوردls -l /etc/nginx/sites-enabled/ و حذف فایل default
413 Request Entity Too Largeسقف پیش‌فرض یک‌مگابایتی بدنه‌ی درخواستافزایش client_max_body_size و هم‌زمان upload_max_filesize در php.ini
504 Gateway Time-outعبور از مهلت شصت‌ثانیه‌ای Nginx؛ اگر به‌جایش خطای 500 می‌گیرید، به سقف سی‌ثانیه‌ای خود PHP خورده‌ایدافزایش هم‌زمان fastcgi_read_timeout و max_execution_time
Certbot در مرحله‌ی challenge شکست می‌خوردپورت 80 بسته، رکورد A غلط، یا ریدایرکت سراسریsudo ufw status و dig +short example.ir
هدرهای امنیتی روی بعضی مسیرها نیستارث‌بری add_header در بلوک داخلی قطع شدهتکرار هدرها در همان بلوک location

خطای 502 آن‌قدر پرتکرار است که برایش راهنمای جداگانه نوشته‌ایم: ریشه‌یابی گام‌به‌گام خطای 502 Bad Gateway.

تمام شد — سایت بالاست

الان یک وب‌سرور واقعی با HTTPS خودکار دارید: بلوک server تمیز، تمدید پایش‌شده، PHP-FPM متصل، و فشرده‌سازی و کش روشن.

سه کار می‌ماند برای همین روز اول: نصب unattended-upgrades برای وصله‌های امنیتی خودکار؛ بردن پشتیبانِ /var/www و /etc/nginx و دیتابیس به بیرون از سرور با یک بازیابی آزمایشی — پشتیبانی که کنار خودِ داده نشسته پشتیبان نیست؛ و ثبت وضعیت فعلی با آزمون سرعت سایت مهران هاست. این ابزار از سرور پنل در خارج از ایران درخواست می‌فرستد، نه از شبکه‌ی خانگی ایران؛ پس عدد را برای مقایسه‌ی قبل و بعدِ خودتان بخوانید، نه تجربه‌ی بازدیدکننده‌ی ایرانی.

قدم بعدی سفت‌کردن خودِ سرور است: کلید SSH به‌جای رمز، بستن ورود مستقیم root و فایروالی که فقط پورت‌های لازم را باز بگذارد — همه در چک‌لیست امنیت سرور لینوکس. اگر این مسیر برایتان زیاد است، روی هاست اشتراکی با cPanel یا دایرکت‌ادمین این لایه‌ها از پیش پیکربندی شده‌اند.

سؤالات پرتکرار

برای راه‌اندازی سایت با Nginx روی اوبونتو چه پیش‌نیازهایی لازم است؟

سه چیز: یک سرور اوبونتو نسخه‌ی ۲۲.۰۴ یا بالاتر، دسترسی SSH با کاربری که اختیار sudo دارد، و دامنه‌ای که رکورد A آن به آی‌پی سرور اشاره کند. با دستور dig مطمئن شوید دامنه واقعاً به همان آی‌پی resolve می‌شود؛ وگرنه مرحله‌ی گرفتن گواهی SSL شکست می‌خورد. کل فرایند از نصب تا فعال‌شدن HTTPS، اگر رکورد A درست باشد، حدود ده دقیقه است.

تفاوت Nginx و آپاچی برای یک سایت ساده چیست؟

مهم‌ترین تفاوت عملی این است که Nginx فایل htaccess را نمی‌خواند؛ هر قاعده‌ی بازنویسی آدرس باید در بلوک server نوشته و با ری‌لود اعمال شود. Nginx همچنین PHP را خودش اجرا نمی‌کند و درخواست را با FastCGI به PHP-FPM می‌سپارد، در حالی که آپاچی از قدیم می‌توانست ماژول PHP را درون خودش اجرا کند — هرچند روی اوبونتوی امروزی MPM پیش‌فرضِ event با mod_php کار نمی‌کند و توصیه‌ی رایج آنجا هم PHP-FPM است.

چرا Certbot خطا می‌دهد و گواهی SSL صادر نمی‌شود؟

سه علت بیشترین سهم را دارند: دامنه هنوز به آی‌پی سرور اشاره نمی‌کند، پورت 80 روی فایروال بسته است، یا ریدایرکتی سراسری مسیر اعتبارسنجی را منحرف کرده. اعتبارسنجی HTTP-01 لازم دارد سرورهای Let's Encrypt فایلی را زیر مسیر well-known از روی پورت 80 بخوانند. هر نام در هر ساعت فقط پنج شکست مجاز دارد، پس برای آزمایش از dry-run استفاده کنید.

گواهی Let's Encrypt چند وقت یک‌بار تمدید می‌شود و اگر تمدید نشود چه می‌شود؟

گواهی‌ها تا فوریه ۲۰۲۷ نود روز اعتبار دارند و بعد از آن پیش‌فرض Let's Encrypt ۶۴ روز می‌شود، و Certbot با یک زمان‌بند خودکار هر گواهی را وقتی حدود یک‌سوم عمرش باقی مانده تمدید می‌کند. اگر این فرایند بشکند هیچ هشداری نمی‌گیرید، چون Let's Encrypt سرویس ایمیل اطلاع‌رسانی انقضا را در ژوئن ۲۰۲۵ تعطیل کرد. نتیجه، هشدار قرمز مرورگر برای همه‌ی بازدیدکنندگان است.

بعد از نصب وردپرس روی Nginx چرا صفحه‌های داخلی خطای 404 می‌دهند؟

چون قاعده‌ی بازنویسی آدرس تعریف نشده است. وردپرس این قاعده را روی آپاچی از فایل htaccess می‌گیرد و Nginx آن را نمی‌خواند، پس آدرس مقاله‌ای که روی دیسک فایلی ندارد به 404 می‌رسد. راه‌حل، نوشتن try_files با ارجاع نهایی به index.php در بلوک اصلی location و ری‌لود Nginx است. همین الگو برای لاراول و اپ‌های تک‌صفحه‌ای هم پاسخ می‌دهد.

آموزش‌های مرتبط

آماده‌ی تمرین عملی هستید؟

سرور ابری ساعتی مهران هاست در ۶۰ ثانیه تحویل می‌شود — تمرین کنید و فقط بابت همان ساعت‌ها پرداخت کنید. هزینه را پیش از ثبت‌نام با محاسبه‌گر برآورد کنید.