Nếu bạn đang chạy WordPress trên VPS bằng Nginx mà cảm thấy site chậm, TTFB cao, hay bị quá tải lúc traffic tăng đột biến — thì bài này là cho bạn. Mình sẽ hướng dẫn từng bước tuning Nginx + PHP-FPM + FastCGI Cache từ A đến Z, áp dụng được ngay trên Ubuntu 22.04/24.04.
\n\n
Trước khi bắt đầu, mình cần nói rõ: bài viết này dành cho ai đang tự quản lý VPS (không dùng shared hosting). Nếu bạn dùng Control Panel như HestiaCP, CyberPanel hay CloudPanel thì các config tương tự nhưng đường dẫn file có thể khác.
\n\n
Tối ưu Nginx cho WordPress VPS là làm những gì?
\n\n
Tối ưu Nginx cho WordPress bao gồm 5 nhóm chính: tuning worker processes/connections, bật compression (gzip/Brotli), cấu hình FastCGI Cache (microcaching), tuning PHP-FPM pool, và set cache headers cho static assets. Mục tiêu cuối cùng là giảm TTFB xuống duoi 200ms và giữ site ổn định khi traffic tăng gấp 5-10 lần. Sau khi áp dụng đầy đủ, mình đã thấy TTFB giảm 60-80% trên VPS 2GB RAM.
\n\n
Nguyên nhân server WordPress chậm là gì?
\n\n
Đa số “server chậm” không phải do CPU yếu. Nguyên nhân phổ biến nhất là: PHP-FPM thiếu worker (requestxếp hàng chờ), không có cache layer (mỗi request đều đánh vào PHP + MySQL), Nginx chưa tuning worker connections, và static files không có cache headers. Khi mình check log các VPS khách hàng, 80% trường hợp chậm nằm ở chỗ PHP-FPM bị bão hòa — không phải Nginx bản thân nó chậm.
\n\n
Bước 1: Đo lường trước khi tối ưu — baseline TTFB
\n\n
Đây là bước nhiều bạn bỏ qua nhất. Nếu không có số liệu trước khi tối ưu, bạn sẽ không biết mình cải thiện được bao nhiêu. Mình luôn làm 3 việc trước khi đụng vào config:
\n\n
1. Đo TTFB từ máy tính:
\n\n
curl -o /dev/null -s -w "DNS:%{time_namelookup} Connect:%{time_connect} TLS:%{time_appconnect} TTFB:%{time_starttransfer} Total:%{time_total}\\n" https://example.com/\n\n
Chạy 5-10 lần để xem độ ổn định. Nếu TTFB nhảy lên xuống 200ms – 800ms thì PHP đang bị queue. Nếu ổn định ở mức cao thì network hoặc config cần chỉnh.
\n\n
2. Snapshot tình trạng server:
\n\n
nginx -v\nphp-fpm8.3 -v\nfree -h\ndf -h\nuptime\ntop -o %CPU\n\n
3. Bật log format kèm timing:
\n\n
# /etc/nginx/nginx.conf -- thêm vào trong block http {}\nlog_format timed '$remote_addr - $host [$time_local] "$request" '\n'$status $body_bytes_sent '\n'rt=$request_time urt=$upstream_response_time '\n'cache=$upstream_cache_status';\n\n
Sau đó reload Nginx: sudo nginx -t && sudo systemctl reload nginx
\n\n
Khi đó access log sẽ hiện thêm rt=0.234 urt=0.198 cache=MISS. Nếu urt cao mà rt thấp thì PHP chậm. Nếu rt cao mà urt thấp thì Nginx đang wait I/O.
\n\n
Bước 2: Tuning worker processes và connections
\n\n
Đây là config nền tảng quyết định Nginx xử lý được bao nhiêu request cùng lúc. Mở file /etc/nginx/nginx.conf:
\n\n
worker_processes auto;\nworker_rlimit_nofile 200000;\n\nevents {\n worker_connections 4096;\n multi_accept on;\n use epoll;\n}\n\n
worker_processes auto cho phép Nginx tự chọn số worker bằng số core CPU. Đừng hard-code số cụ thể trừ khi bạn biết chính xác.
\n\n
Tiếp theo, tăng giới hạn file descriptor cho Nginx service:
\n\n
sudo systemctl edit nginx\n\n# Thêm vào:\n[Service]\nLimitNOFILE=200000\n\nsudo systemctl daemon-reload\nsudo nginx -t && sudo systemctl restart nginx\n\n
Lưu ý: nếu bạn không restart mà chỉ reload, LimitNOFILE sẽ không có hiệu lực. Cần restart lần đầu tiên.
\n\n
Bước 3: Bật sendfile và tối ưu TCP
\n\n
Vẫn trong block http {} của /etc/nginx/nginx.conf:
\n\n
sendfile on;\ntcp_nopush on;\ntcp_nodelay on;\nkeepalive_timeout 15;\nkeepalive_requests 1000;\n\n
sendfile giúp Nginx gửi file tĩnh trực tiếp từ kernel space, bỏ qua user space — giảm CPU overhead đáng kể khi phục vụ images, CSS, JS. tcp_nopush đi kèm với sendfile để gói TCP gửi hiệu quả hơn.
\n\n
Thêm cache cho file tĩnh để giảm filesystem lookup:
\n\n
open_file_cache max=50000 inactive=60s;\nopen_file_cache_valid 120s;\nopen_file_cache_min_uses 2;\nopen_file_cache_errors on;\n\n
Bước 4: Bật compression Gzip và Brotli
\n\n
Compression giảm kích thước response, đặc biệt hiệu quả trên mobile. Thêm vào http {}:
\n\n
# Gzip (có sẵn mặc định)\ngzip on;\ngzip_comp_level 5;\ngzip_min_length 1024;\ngzip_vary on;\ngzip_proxied any;\ngzip_types\n text/plain\n text/css\n text/xml\n application/json\n application/javascript\n application/xml\n application/xml+rss\n image/svg+xml;\n\n
Nếu Nginx build của bạn hỗ trợ Brotli (kiểm tra bằng nginx -V 2>&1 | grep brotli), thì bật thêm:
\n\n
brotli on;\nbrotli_comp_level 5;\nbrotli_types text/plain text/css application/json application/javascript image/svg+xml;\n\n
Brotli nén tốt hơn gzip 15-20% cho text. Nếu không có Brotli, gzip level 5 là đủ — đừng đẩy lên level 9 vì tốn CPU mà_gain không đáng kể.
\n\n
Bước 5: FastCGI Cache — cong cụ giảm tải PHP mạnh nhất
\n\n
Đây là bước mang lại impact lớn nhất. FastCGI Cache lưu response PHP trong bộ nhớ mấy giây — đủ để hấp thụ traffic spike mà không đánh vào PHP hay MySQL.
\n\n
Tạo cache zone trong http {}:
\n\n
fastcgi_cache_path /var/cache/nginx/fastcgi\n levels=1:2\n keys_zone=phpcache:100m\n inactive=10m\n max_size=2g;\n\nfastcgi_cache_key "$scheme$request_method$host$request_uri";\n\n
Tạo thư mục cache:
\n\n
sudo mkdir -p /var/cache/nginx/fastcgi\nsudo chown -R www-data:www-data /var/cache/nginx/fastcgi\n\n
Thêm cache logic vào server block WordPress (thường ở /etc/nginx/sites-available/example.com):
\n\n
set $skip_cache 0;\n\n# Không cache cho user đã đăng nhập hoặc giỏ hàng\nif ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_logged_in") {\n set $skip_cache 1;\n}\nif ($request_method != GET) { set $skip_cache 1; }\nif ($query_string != "") { set $skip_cache 1; }\n\nlocation ~ \\.php$ {\n fastcgi_pass unix:/run/php/php8.3-fpm.sock;\n fastcgi_index index.php;\n include fastcgi_params;\n fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n\n fastcgi_cache phpcache;\n fastcgi_cache_valid 200 301 302 10s;\n fastcgi_cache_use_stale updating error timeout invalid_header http_500 http_503;\n fastcgi_cache_bypass $skip_cache;\n fastcgi_no_cache $skip_cache;\n add_header X-FastCGI-Cache $upstream_cache_status always;\n}\n\n
10 giây cache nghe có vẻ ngắn, nhưng trên site traffic cao, trong 10s đó có thể có hàng trăm request trùng lặp — tất cả đều được phục vụ từ cache thay vì PHP.
\n\n
Kiểm tra cache hoạt động:
\n\n
curl -I https://example.com/ | grep X-FastCGI-Cache\n\n
Lần đầu: X-FastCGI-Cache: MISS. Lần hai: X-FastCGI-Cache: HIT. Nếu vẫn MISS thì cache chưa hoạt động.
\n\n
Lưu ý quan trọng: nếu bạn chạy WooCommerce, membership plugin, hay bất kỳ tính năng nào cá nhân hóa theo user, hãy cẩn thận với cache rules. Test kỹ checkout, login, account page sau khi bật cache.
\n\n
Bước 6: Tuning PHP-FPM pool
\n\n
PHP-FPM là chỗ 80% WordPress chậm xảy ra. Mở file pool config — thường ở /etc/php/8.3/fpm/pool.d/www.conf:
\n\n
Đo memory usage trung bình per worker trước:
\n\n
ps -ylC php-fpm8.3 --sort:rss | awk 'NR==1{print} NR<=10{print}'\n\n
WordPress với plugin thường dùng 80-150MB per worker. Page builder như Elementor có thể lên 200MB.
\n\n
Config cho VPS 4GB RAM (giả sử PHP worker trung bình 120MB):
\n\n
pm = dynamic\npm.max_children = 16\npm.start_servers = 4\npm.min_spare_servers = 4\npm.max_spare_servers = 8\npm.max_requests = 500\n\n
Công tính: 16 x 120MB = 1.9GB cho PHP. Còn lại cho Nginx, OS page cache, MySQL. Nếu VPS 2GB RAM thì giảm pm.max_children xuống 8-10.
\n\n
pm.max_requests = 500 quan trọng — nó recycle worker sau 500 request để tránh memory leak. Đừng bỏ qua thông số này.
\n\n
Restart PHP-FPM:
\n\n
sudo systemctl restart php8.3-fpm\nsudo tail -n 50 /var/log/php8.3-fpm.log\n\n
Nếu thấy server reached pm.max_children trong log, nghĩa là pool đang bão hòa. Nếu RAM còn dư thì tăng pm.max_children lên. Nếu RAM không cho phép thì cần tối ưu cache (bước 5) để giảm request đến PHP.
\n\n
Bước 7: Cache headers cho static assets
\n\n
Thêm vào server block:
\n\n
location ~* \\.(css|js|jpg|jpeg|png|gif|ico|svg|webp|avif|ttf|woff|woff2)$ {\n expires 30d;\n add_header Cache-Control "public, max-age=2592000, immutable";\n access_log off;\n}\n\n
immutable báo cho trình duyệt biết file không thay đổi trong 30 ngày — trình duyệt sẽ không gửi conditional request (304 check), tiết kiệm 1 round-trip cho mỗi asset khi user quay lại.
\n\n
Không áp dụng cache dài cho HTML — chỉ cache static assets có version hash trong tên file.
\n\n
Bước 8: HTTP/2 cho TLS sites
\n\n
Nếu site đã có SSL certificate (Let’s Encrypt), đảm bảo HTTP/2 đang bật:
\n\n
listen 443 ssl http2;\n\n
HTTP/2 multiplexing cho phép trình duyệt tải nhiều asset song song trên 1 connection — giảm overhead và improve page load đáng kể. Kiểm tra bằng:
\n\n
curl -I --http2 https://example.com/\n\n
Nếu response header có HTTP/2 200 là OK. Nếu vẫn HTTP/1.1 thì cần check lại config.
\n\n
Bước 9: Rate limiting cho endpoint tốn CPU
\n\n
WordPress login, XML-RPC, search là những endpoint tốn CPU nhất. Rate limiting vừa bảo vệ hiệu suất vừa tăng cường bảo mật:
\n\n
# Trong http {}\nlimit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;\nlimit_req_zone $binary_remote_addr zone=search:10m rate=30r/m;\n\n# Trong server block\nlocation = /wp-login.php {\n limit_req zone=login burst=5 nodelay;\n fastcgi_pass unix:/run/php/php8.3-fpm.sock;\n include fastcgi_params;\n # ... rest config\n}\n\nlocation / {\n if ($arg_s != "") {\n limit_req zone=search burst=10 nodelay;\n }\n try_files $uri $uri/ /index.php?$args;\n}\n\n
10 request/phút cho login là đủ cho user thật, nhưng chặn brute force hiệu quả. Search 30 request/phút chống bot crawl taxonomy archive.
\n\n
Verify kết quả: so sánh trước và sau
\n\n
Sau khi áp dụng tất cả các bước, chạy lại baseline TTFB:
\n\n
for i in {1..10}; do\n curl -o /dev/null -s -w "TTFB:%{time_starttransfer}\\n" https://example.com/\ndone\n\n
Trên VPS 2-4GB RAM, kết quả mong đợi:
\n\n
- \n
- TTFB giảm từ 400-800ms xuống 100-200ms (cache HIT)
- Throughput tăng 10-20x trên cache HIT
- PHP-FPM queue biến mất (log sạch)
- Server ổn định khi traffic tăng gấp 5
\n
\n
\n
\n
\n\n
Đừng quên kiểm tra X-FastCGI-Cache: HIT xuất hiện thường xuyên trong response header. Tỷ lệ HIT/TOTAL càng cao thì càng tốt.
\n\n
Troubleshooting: lỗi thường gặp
\n\n
Lỗi 1: Site trắng sau khi thay config
\n\n
Ngay lập tức revert: sudo nginx -t trước mỗi lần reload. Nếu nginx -t báo syntax error, config cũ vẫn chạy — đừng panic. Fix lỗi rồi reload lại.
\n\n
Lỗi 2: WooCommerce checkout bị lỗi sau khi bật cache
\n\n
Thêm /cart/, /checkout/, /my-account/ vào danh sách bypass:
\n\n
if ($request_uri ~* "/(cart|checkout|my-account)") {\n set $skip_cache 1;\n}\n\n
Lỗi 3: PHP-FPM log báo “max_children reached”
\n\n
Kiểm tra RAM: free -h. Nếu còn RAM, tăng pm.max_children. Nếu hết RAM, cần thêm swap hoặc upgrade VPS. Đồng thời bật FastCGI Cache (bước 5) để giảm tải PHP.
\n\n
Lỗi 4: HTTP/2 không hoạt động
\n\n
Đảm bảo tất cả server block đều dùng listen 443 ssl http2;. Nếu chỉ 1 block thiếu http2, Nginx có thể fallback toàn bộ về HTTP/1.1.
\n\n
Lỗi 5: Cache HIT tỷ lệ thấp
\n\n
Kiểm tra $skip_cache rules có quá rộng không. Query string rỗng ($query_string != "") sẽ skip cache cho mọi URL có tham số — bao gồm UTM tags. Nếu analytics quan trọng, cân nhắc loại bỏ rule này hoặc strip UTM params trước cache.
\n\n
Checklist triển khai nhanh
\n\n
Mình tóm tắt lại 9 bước theo thứ tự ưu tiên, bạn nào bận thì làm theo thứ tự này:
\n\n
- \n
- Đo baseline TTFB (2 phút)
- Bật FastCGI Cache (15 phút) — impact lớn nhất
- Tuning PHP-FPM pool (10 phút)
- Gzip compression (2 phút)
- Worker processes/connections (5 phút)
- Static asset cache headers (2 phút)
- HTTP/2 (2 phút nếu đã có SSL)
- Sendfile + TCP options (2 phút)
- Rate limiting login/search (10 phút)
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n\n
Nếu chỉ có thời gian làm 1 thứ hôm nay, hãy bật FastCGI Cache. Đó là thay đổi cho impact lớn nhất với rủi ro thấp nhất.
\n\n
Mình sẽ viết tiếp bài về Redis Object Cache và MariaDB optimization cho WordPress trong các bài sau. Nếu bạn gặp khó khăn ở bước nào, để lại comment mình sẽ hỗ trợ.
