PHP

Cách chạy nhiều phiên bản PHP trên cùng VPS với Nginx và PHP-FPM

Hướng dẫn tách PHP-FPM pool và socket cho nhiều phiên bản PHP trên cùng VPS, sau đó cấu hình Nginx theo từng domain với quy trình verify và rollback.

NHT
· 8 phút đọc
Kiến trúc nhiều PHP-FPM worker pool phục vụ các website qua Nginx

Chạy nhiều phiên bản PHP trên cùng một VPS là nhu cầu phổ biến khi một website cũ chưa thể nâng cấp, trong khi dự án mới cần PHP 8.2 hoặc 8.3. Cách an toàn với Nginx là để mỗi phiên bản PHP chạy qua một dịch vụ PHP-FPM riêng, sau đó trỏ từng server block tới đúng socket hoặc port của phiên bản đó.

Bài hướng dẫn này dùng mô hình Ubuntu/Debian với Nginx và PHP-FPM. Các tên package có thể khác tùy phiên bản hệ điều hành và repository. Hãy kiểm tra package khả dụng bằng apt-cache policy trước khi cài, không copy mù các lệnh từ một Ubuntu release khác vào production.

Nguyên lý: Nginx không tự chạy PHP

Nginx nhận HTTP request và chuyển request PHP sang FastCGI server bằng module ngx_http_fastcgi_module. PHP-FPM là process manager đứng phía sau, quản lý worker PHP và pool. Directive fastcgi_pass quyết định request đi tới socket/port nào; vì vậy nếu mỗi phiên bản PHP có một endpoint riêng, từng domain có thể dùng runtime khác nhau [1] [2].

WebsiteRuntimePHP-FPM endpoint ví dụ
legacy.example.comPHP 8.2/run/php/php8.2-fpm.sock
app.example.comPHP 8.3/run/php/php8.3-fpm.sock

Điểm quan trọng là phiên bản CLI mặc định của lệnh php -v không quyết định runtime của website. Website dùng phiên bản nào là do fastcgi_pass trỏ tới FPM socket nào.

Kiểm tra hệ thống trước khi cài

lsb_release -a
nginx -v
apt-cache policy php8.2-fpm php8.3-fpm
systemctl list-unit-files 'php*-fpm.service'
df -h
free -h

Không nên cài thêm runtime khi VPS thiếu RAM hoặc đang có nhiều pool dư thừa. Mỗi phiên bản FPM có master process, worker và OPcache riêng; nhiều phiên bản làm tăng lượng service, log và bộ nhớ cần theo dõi. Chọn đúng một phiên bản cho dự án mới và chỉ giữ phiên bản cũ trong thời gian cần migration.

Cài các phiên bản PHP-FPM

Ví dụ dưới đây minh họa package PHP 8.2 và 8.3. Hãy thay phiên bản theo package thật sự có trong repository của máy.

sudo apt update
sudo apt install -y nginx php8.2-fpm php8.2-cli php8.2-mysql \
    php8.3-fpm php8.3-cli php8.3-mysql

sudo systemctl enable --now php8.2-fpm
sudo systemctl enable --now php8.3-fpm
systemctl --type=service --state=running | grep php

Ứng dụng Laravel hoặc CMS có thể cần thêm extension như mbstring, xml, curl, zipgd. Chỉ cài extension mà ứng dụng yêu cầu, sau đó kiểm tra bằng php8.2 -m hoặc php8.3 -m. Không dùng php -m để kết luận cho cả hai runtime.

Kiểm tra và tạo pool PHP-FPM riêng

Mỗi FPM instance đã có pool mặc định, nhưng production nhiều website nên dùng pool có tên rõ ràng để tách user, socket và giới hạn worker. PHP Manual cho biết mỗi pool có thể có cài đặt riêng, bao gồm listen, user/group và process manager [2].

; /etc/php/8.2/fpm/pool.d/legacy.conf
[legacy]
user = legacy
group = legacy
listen = /run/php/legacy-82.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = ondemand
pm.max_children = 8
pm.process_idle_timeout = 10s
pm.max_requests = 500
catch_workers_output = yes
security.limit_extensions = .php .phar

; /etc/php/8.3/fpm/pool.d/app.conf
[app]
user = app
group = app
listen = /run/php/app-83.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 12
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.max_requests = 500
catch_workers_output = yes
security.limit_extensions = .php .phar

Pool chạy bằng user riêng giúp giảm phạm vi ảnh hưởng nếu ứng dụng bị lỗi hoặc bị khai thác. Đảm bảo user đó có quyền đọc source và ghi đúng các thư mục cần thiết như storage/cache; không cấp quyền ghi toàn bộ document root nếu không cần.

Chọn pm dynamic hay ondemand?

dynamic giữ một số worker sẵn sàng và phù hợp website có traffic đều. ondemand tạo worker khi có request, phù hợp website ít truy cập hoặc pool legacy chỉ cần bật thỉnh thoảng. pm.max_children là giới hạn số request PHP đồng thời của pool, không phải con số có thể đặt tùy ý. Nếu mỗi worker dùng 80 MB và VPS chỉ còn khoảng 1 GB cho PHP, đặt tổng max children quá cao sẽ dẫn tới swap hoặc OOM.

Hãy đo memory thực tế bằng process list và log trước khi tăng worker. Nếu cần tuning sâu, theo dõi thời gian response, số request chờ, CPU, RAM và slow log thay vì chỉ tăng pm.max_children.

Trỏ từng Nginx server block tới đúng socket

server {
    listen 80;
    server_name legacy.example.com;
    root /var/www/legacy/public;
    index index.php index.html;

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

    location ~ \.php$ {
        try_files $uri =404;
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/legacy-82.sock;
    }
}

server {
    listen 80;
    server_name app.example.com;
    root /var/www/app/public;
    index index.php index.html;

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

    location ~ \.php$ {
        try_files $uri =404;
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/app-83.sock;
    }
}

NGINX documentation minh họa việc truyền SCRIPT_FILENAME, query string và các request parameter tới FastCGI [1]. Trên Ubuntu, file snippets/fastcgi-php.conf thường đã chứa nhiều thiết lập chuẩn; hãy đọc nội dung file trước khi thêm directive trùng lặp. Không dùng fastcgi_pass tới socket của PHP 8.3 trong server block legacy chỉ vì đó là phiên bản mặc định trên máy.

Kiểm tra socket, quyền và syntax

sudo systemctl status php8.2-fpm --no-pager
sudo systemctl status php8.3-fpm --no-pager
ls -l /run/php/*.sock
sudo nginx -t
sudo systemctl reload nginx

Nếu Nginx trả 502, bắt đầu từ socket: file có tồn tại không, Nginx có quyền kết nối không, pool có đang listen đúng path không. Sau đó đọc log:

sudo tail -n 80 /var/log/nginx/error.log
sudo journalctl -u php8.2-fpm -n 80 --no-pager
sudo journalctl -u php8.3-fpm -n 80 --no-pager

Lỗi “connect() to unix socket failed” thường liên quan socket sai path, service FPM dừng, permission hoặc pool không load do syntax. Lỗi “Primary script unknown” thường liên quan root, try_files hoặc SCRIPT_FILENAME không trỏ tới file thật.

Xác nhận PHP version từ chính website

Tạo file kiểm tra tạm thời trong document root:

<?php
header('Content-Type: text/plain; charset=utf-8');
echo PHP_VERSION, PHP_EOL;

Mở URL kiểm tra bằng domain tương ứng rồi xóa file ngay sau khi xác nhận. Không để phpinfo() công khai vì nó có thể tiết lộ đường dẫn, module, biến môi trường và thông tin server. Với ứng dụng đã có route health check, ưu tiên trả về một giá trị runtime tối thiểu thay vì dùng file debug.

Cài CLI version riêng cho từng dự án

CLI và FPM là hai đường chạy khác nhau. Khi chạy Composer hoặc Artisan cho dự án legacy, gọi rõ binary:

/usr/bin/php8.2 -v
/usr/bin/php8.2 /usr/local/bin/composer install --no-dev --optimize-autoloader
/usr/bin/php8.3 -v
/usr/bin/php8.3 artisan migrate --force

Tùy hệ thống, binary có thể nằm ở path khác. Kiểm tra bằng command -v php8.2, command -v php8.3readlink -f trước khi đưa vào cron hoặc CI/CD. Nếu dùng Composer platform config, hãy bảo đảm dependency thực sự tương thích với PHP version của FPM mà website đang chạy.

Bảo mật và vận hành

Hạng mụcKhuyến nghị
SocketDùng Unix socket riêng, mode 0660 và group phù hợp; không mở TCP ra Internet nếu không cần
User poolTách user theo ứng dụng; chỉ cấp quyền ghi vào thư mục cần thiết
PHP extensionsChỉ cài module cần dùng; kiểm kê trước khi gỡ phiên bản cũ
Worker limitTính theo RAM thực tế, theo dõi OOM/swap và slow log
DebugTắt display_errors trên production, không để phpinfo hoặc file test công khai
Nâng cấpBackup database/source, test trên staging và có kế hoạch đổi socket rollback

Rollback khi đổi PHP version

Cách rollback ít rủi ro nhất là không thay đổi mọi website cùng lúc. Giữ nguyên socket cũ, tạo socket mới cho version thử nghiệm, chuyển một server block sang socket mới, kiểm tra log và chức năng, rồi mới nhân rộng. Nếu có lỗi, sửa fastcgi_pass về socket cũ, chạy nginx -t và reload. Không gỡ PHP version cũ trước khi xác nhận toàn bộ domain, cron và queue đã chạy ổn trên version mới.

sudo cp /etc/nginx/sites-available/app /etc/nginx/sites-available/app.bak
sudo nginx -t
sudo systemctl reload nginx
# rollback: khôi phục app.bak, sau đó kiểm tra lại
sudo cp /etc/nginx/sites-available/app.bak /etc/nginx/sites-available/app
sudo nginx -t && sudo systemctl reload nginx

Checklist trước khi đưa vào production

  • Đã xác nhận package, binary và service của từng PHP version trên đúng Ubuntu/Debian release.
  • Mỗi website đã có server block, document root, user pool và FPM socket riêng.
  • Đã kiểm tra nginx -t, service status, socket permission và log 502.
  • Đã xác nhận runtime bằng endpoint kiểm tra tạm thời rồi xóa endpoint đó.
  • Đã test Composer, Artisan, cron và queue bằng binary PHP đúng version.
  • Đã lưu bản cấu hình và biết cách rollback về socket cũ.

Kết luận

Muốn chạy nhiều phiên bản PHP trên cùng VPS, hãy nghĩ theo đơn vị FPM service/pool + socket + server block, không chỉ theo lệnh php -v. Nginx chọn runtime thông qua fastcgi_pass; FPM kiểm soát worker, user, permission và giới hạn tài nguyên. Khi từng domain được tách rõ, việc nâng cấp dần dần và rollback sẽ an toàn hơn. Nếu bạn cần nền tảng reverse proxy nhiều website trước khi phân tuyến PHP, xem thêm bài Cấu hình Nginx reverse proxy cho nhiều website trên cùng một VPS.

Tham khảo

  1. NGINX Documentation — Module ngx_http_fastcgi_module.
  2. PHP Manual — FPM Configuration.