Cách chạy nhiều website trên 1 VPS mà vẫn tách biệt an toàn

Hướng dẫn chạy nhiều website trên 1 VPS với nginx: mỗi site một server block, user Linux, pool PHP-FPM, database, SSL riêng và giới hạn tài nguyên.

Sơ đồ một VPS chia thành nhiều ô riêng, mỗi website có user, pool PHP và database riêng

Ý chính

  • nginx chọn website theo header Host, nên nhiều tên miền có thể dùng chung một IP và một VPS.
  • Mỗi site cần user Linux, pool PHP-FPM và user database riêng để một site bị hack không đọc hay sửa được site bên cạnh.
  • Thêm một server block default_server trả về 444 để tên miền lạ không rơi vào một website thật.
  • Mặc định mọi pool PHP-FPM chạy chung một service systemd, nên muốn giới hạn CPU, RAM theo từng site phải chạy PHP-FPM master riêng cho site đó.

Để chạy nhiều website trên 1 VPS, bạn trỏ bản ghi A của từng tên miền về IP của server, rồi tạo cho mỗi site một server block nginx riêng; nginx dựa vào header Host của request để biết phục vụ site nào. Muốn các site thật sự tách biệt, mỗi site còn cần user Linux riêng, pool PHP-FPM chạy bằng user đó, database và user database riêng, cùng chứng chỉ SSL riêng. Khi đó một site bị hack hay bị quá tải chỉ là chuyện của riêng site đó.

Bài này làm thủ công trên Ubuntu 24.04 với nginx, PHP 8.3 và MariaDB. Bài giả định bạn đã cài xong bộ LEMP; nếu chưa, hãy bắt đầu với hướng dẫn cài LEMP trên Ubuntu 24.04. Ở đây ta tập trung vào những gì thay đổi khi có site thứ hai, rồi site thứ hai mươi.

Vì sao phải tách biệt các website?

Cách nhanh nhất để thêm site thứ hai là bỏ code vào /var/www/site2 và cho chạy chung pool www mặc định. Cách này vẫn chạy, nhưng mọi site sẽ chạy PHP bằng cùng một user (www-data), nên code của site này đọc và ghi được file của mọi site khác, kể cả file wp-config.php chứa mật khẩu database.

Đây không phải lý thuyết. Công ty bảo mật Sucuri gọi hiện tượng này là lây nhiễm chéo (cross-site contamination): một site bảo mật kém trở thành cửa vào cho mọi site khác trên cùng tài khoản hoặc server. Trong đợt tấn công qua plugin MailPoet năm 2014, nhà nghiên cứu Daniel Cid ghi nhận khoảng 50.000 website WordPress bị cài backdoor, và một số site không hề dùng plugin lỗi đó: chỉ cần một site khác trong cùng tài khoản hosting có nó.

Tách biệt còn có mục đích thứ hai: tài nguyên. Không có giới hạn, một site bị bot tấn công hoặc dính plugin lỗi có thể ngốn hết CPU và RAM, kéo chậm tất cả các site còn lại.

Kế hoạch: mỗi site được gì?

Thành phần Dùng chung Riêng cho từng site
Địa chỉ IP, nginx, MariaDB server Có
Bản ghi DNS A example.com, www.example.com
Server block nginx và log /etc/nginx/sites-available/example.com
User Linux và thư mục home example, /home/example
Pool PHP-FPM và socket /run/php/php8.3-fpm-example.sock
Database và user database example_db, example_db@localhost
Chứng chỉ SSL Let's Encrypt qua Certbot
Giới hạn CPU, RAM, tiến trình (tùy chọn) systemd slice

Bài dùng ví dụ example.com với user example. Với mỗi site mới, bạn lặp lại các bước với tên khác.

Bước 1: Trỏ DNS về VPS

Tại nơi quản lý DNS (nhà đăng ký tên miền hoặc Cloudflare), tạo bản ghi A cho example.com và www trỏ về IPv4 của VPS (thêm bản ghi AAAA nếu server có IPv6). Kiểm tra trước khi làm tiếp:

dig +short example.com A
dig +short www.example.com A

Cả hai phải trả về đúng IP server, nếu không Certbot sẽ lỗi ở bước 7. Chi tiết xem bài trỏ tên miền về VPS.

Bước 2: Tạo user Linux riêng cho từng site

sudo useradd --create-home --home-dir /home/example --shell /usr/sbin/nologin example
sudo mkdir -p /home/example/{public_html,tmp,logs}
sudo chown -R example:example /home/example
sudo chown example:www-data /home/example
sudo chmod 750 /home/example
sudo chmod 700 /home/example/tmp /home/example/logs

Ý nghĩa của phân quyền:

  • /home/example có quyền 750, chủ sở hữu example, nhóm www-data. User của site có toàn quyền, nginx (chạy bằng www-data) vào được để phục vụ file tĩnh, còn mọi user khác bị chặn, kể cả user PHP của các site khác.
  • tmp và logs có quyền 700: chỉ PHP của chính site đó đọc được.
  • Shell là nologin vì user này chỉ để sở hữu file và chạy PHP, không dùng để đăng nhập.

Bước 3: Mỗi site một pool PHP-FPM

Tạo file /etc/php/8.3/fpm/pool.d/example.conf:

[example]
user = example
group = example

listen = /run/php/php8.3-fpm-example.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = ondemand
pm.max_children = 5
pm.process_idle_timeout = 10s
pm.max_requests = 500

php_admin_value[open_basedir] = /home/example/public_html:/home/example/tmp
php_admin_value[upload_tmp_dir] = /home/example/tmp
php_admin_value[session.save_path] = /home/example/tmp
php_admin_value[sys_temp_dir] = /home/example/tmp
php_admin_flag[log_errors] = on
php_admin_value[error_log] = /home/example/logs/php-error.log

Các dòng quan trọng:

  • user và group khiến PHP của site chạy bằng user example, nên chỉ đụng được vào file mà example có quyền.
  • Socket thuộc về www-data với quyền 0660, nên nginx kết nối được còn user của site khác thì không.
  • pm = ondemand không giữ worker nào khi site không có truy cập, tiết kiệm RAM khi bạn chạy nhiều site nhỏ. pm.max_children giới hạn số request PHP site này được chạy cùng lúc.
  • Giá trị đặt bằng php_admin_value không thể bị code của site ghi đè bằng ini_set(). Session và file upload nằm trong tmp riêng của site thay vì /tmp dùng chung.

Về open_basedir: tài liệu PHP nói rõ đây "chỉ là một lớp an toàn bổ sung", "không thể dựa vào khi cần bảo mật", và nó còn tắt realpath cache. Bức tường thật sự là user Linux riêng và thư mục home 750. Hãy coi open_basedir là lớp thứ hai.

Kiểm tra cú pháp rồi reload:

sudo php-fpm8.3 -t && sudo systemctl reload php8.3-fpm
ls -l /run/php/

Khi mọi site đã có pool riêng, bạn có thể bỏ pool www.conf mặc định nếu không còn site nào dùng. Cần mỗi site một phiên bản PHP khác nhau? Cách tạo pool y hệt, chỉ khác service FPM của từng phiên bản; xem bài chạy nhiều phiên bản PHP trên một server.

Bước 4: Mỗi site một server block nginx

Tạo file /etc/nginx/sites-available/example.com:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    root /home/example/public_html;
    index index.php index.html;

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

    disable_symlinks if_not_owner;

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

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

    location ~ /\.(?!well-known) {
        deny all;
    }
}

Mỗi site có access log và error log riêng, nên bạn biết ngay site nào đang bị dội request. File trong /var/log/nginx/ được logrotate của gói nginx xoay vòng sẵn. disable_symlinks if_not_owner ngăn nginx đi theo symlink trỏ tới file của người khác, một mẹo kinh điển để đọc file cấu hình của site bên cạnh.

Bật site:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Bước 5: Thêm server mặc định "hứng" mọi tên miền lạ

Khi request đến với tên miền nginx không biết, hoặc chỉ gõ IP, nginx chuyển nó cho server mặc định của cổng đó. Nếu bạn chưa chỉ định, đó là server block được nạp đầu tiên, tức là một website thật bất kỳ sẽ trả lời cho mọi tên miền ai đó trỏ về IP của bạn. Hãy khai báo server mặc định rõ ràng và để trống. Tạo file /etc/nginx/sites-available/000-default:

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 444;
}

server {
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name _;
    ssl_reject_handshake on;
}

444 là mã riêng của nginx: đóng kết nối mà không gửi phản hồi nào. ssl_reject_handshake (có từ nginx 1.19.4; Ubuntu 24.04 dùng 1.24) từ chối bắt tay HTTPS với tên miền lạ, nên bạn không cần chứng chỉ giả. Xóa site mặc định của Ubuntu (nó cũng khai báo default_server) rồi bật file mới:

sudo rm /etc/nginx/sites-enabled/default
sudo ln -s /etc/nginx/sites-available/000-default /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Bước 6: Mỗi site một database và user database

sudo mariadb -e "CREATE DATABASE example_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'example_db'@'localhost' IDENTIFIED BY 'mat-khau-dai-ngau-nhien';
GRANT ALL PRIVILEGES ON example_db.* TO 'example_db'@'localhost';
ALTER USER 'example_db'@'localhost' WITH MAX_USER_CONNECTIONS 20;"

Quyền chỉ cấp cho example_db.*, nên lộ thông tin đăng nhập của một site chỉ mở được dữ liệu của site đó. MAX_USER_CONNECTIONS ngăn một site đông khách chiếm hết kết nối của MariaDB. Đừng bao giờ dùng chung một user database cho nhiều site.

Bước 7: Mỗi site một chứng chỉ SSL

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

Certbot tự thêm cấu hình HTTPS vào server block của site đó, và gói Certbot trên Ubuntu cài sẵn timer tự gia hạn chứng chỉ. Chạy sudo certbot renew --dry-run một lần để kiểm tra. Tìm hiểu thêm: SSL miễn phí với Let's Encrypt.

Bước 8 (tùy chọn): Giới hạn CPU và RAM cho từng site

Các bước trên tách biệt file. Chúng không ngăn một site ngốn hết CPU. Vấn đề là mọi pool trong pool.d chạy dưới một tiến trình master duy nhất của php8.3-fpm.service, nên PHP của mọi site nằm chung một cgroup và bạn không thể giới hạn riêng một site ở đó. Bài giới hạn tài nguyên tài khoản hosting giải thích chi tiết điều này.

Cách khắc phục là cho site một PHP-FPM master riêng nằm trong systemd slice riêng. Chuyển pool ra khỏi pool.d sang file /etc/php/8.3/fpm/example-fpm.conf, thêm phần [global] ở đầu:

[global]
pid = /run/php-fpm-example/php-fpm.pid
error_log = /home/example/logs/php-fpm.log

[example]
; giữ nguyên cấu hình pool ở bước 3, chỉ đổi:
listen = /run/php-fpm-example/php.sock

Tạo slice /etc/systemd/system/site-example.slice:

[Slice]
CPUQuota=100%
MemoryMax=1G
TasksMax=100

và service /etc/systemd/system/php-fpm-example.service:

[Unit]
Description=PHP-FPM for example.com
After=network.target

[Service]
Type=notify
Slice=site-example.slice
RuntimeDirectory=php-fpm-example
ExecStart=/usr/sbin/php-fpm8.3 --nodaemonize --fpm-config /etc/php/8.3/fpm/example-fpm.conf
ExecReload=/bin/kill -USR2 $MAINPID

[Install]
WantedBy=multi-user.target
sudo php-fpm8.3 -t -y /etc/php/8.3/fpm/example-fpm.conf
sudo systemctl daemon-reload
sudo systemctl enable --now php-fpm-example

Đổi fastcgi_pass của site sang unix:/run/php-fpm-example/php.sock rồi reload nginx. CPUQuota=100% cho phép dùng tối đa một core, MemoryMax= là trần cứng và TasksMax= giới hạn số tiến trình, theo tài liệu systemd.resource-control. Theo dõi bằng systemd-cgtop. Cron job của site cũng nên chạy trong cùng slice (systemd-run --slice=site-example.slice --uid=example ...), nếu không chúng sẽ thoát khỏi giới hạn.

Bước 9: Backup riêng từng site

Khi đã tách site, backup trở nên đơn giản: mỗi site một thư mục và một database.

sudo tar -czf /root/backups/example-$(date +%F).tar.gz -C /home example
sudo mariadb-dump --single-transaction example_db | gzip > /root/backups/example_db-$(date +%F).sql.gz

Chạy bằng cron và chép bản backup ra ngoài server; backup nằm trên chính VPS sẽ mất cùng VPS. Xem thêm chiến lược backup website và backup VPS lên S3.

Cách dùng panel: cùng mức tách biệt trong ZoPanel

ZoPanel dựng sẵn cấu trúc này cho bạn. Mỗi tài khoản hosting là một user Linux riêng; website nằm trong /home/<tài-khoản>/domains/<tên-miền>/, và PHP chạy trong pool PHP-FPM riêng của tài khoản, mỗi phiên bản PHP một pool. Mỗi tài khoản chạy trong systemd slice riêng với giới hạn CPU, RAM, số tiến trình và tốc độ đĩa theo gói hosting, và pool PHP, ứng dụng, cron job, phiên SSH của tài khoản đều nằm trong slice đó. Dung lượng đĩa và số inode được giới hạn bằng quota hệ thống file, số kết nối database được giới hạn theo từng user database. nginx từ chối symlink trỏ tới file của người khác, còn SFTP bị chroot trong thư mục home (bảo mật, gói hosting và giới hạn).

Thêm website chỉ là điền một form: tên miền, loại website (PHP, WordPress, Laravel, HTML, Node/proxy), phiên bản PHP từ 7.4 đến 8.4, và SSL miễn phí được cấp ngay khi DNS trỏ về server (website và PHP). Mỗi tên miền chỉ dùng được một lần trên server, và subdomain của tên miền thuộc tài khoản khác sẽ bị từ chối.

Một khác biệt cần nói thẳng: trong ZoPanel, tách biệt theo tài khoản, không phải theo từng website. Các website trong cùng một tài khoản dùng chung user và pool của tài khoản đó, giống addon domain trong cPanel. Nếu hai site thuộc hai người khác nhau, hoặc một site dùng plugin rủi ro, hãy đặt chúng ở hai tài khoản riêng. ZoPanel cũng cần server mới cài Ubuntu 22.04/24.04/26.04 hoặc Debian 12/13, nên không cài chồng lên server bạn đã tự dựng.

Tự làm hay dùng control panel?

Tự làm (theo bài này) ZoPanel
Chi phí Miễn phí Gói Free (3 tài khoản, 10 website); Pro 49 USD/năm mỗi server
Thời gian thêm một site 15–30 phút sửa file cẩn thận Khoảng một phút
User, pool, database riêng Có, nếu không bỏ sót bước nào Tự động theo tài khoản
Giới hạn CPU/RAM Thêm FPM master + slice cho từng site Theo gói hosting
Quota đĩa Tự cấu hình quota Theo gói hosting
Gia hạn SSL Timer của Certbot Tự động
Backup Script tự viết Có sẵn, kèm bản sao lên S3
Quyền kiểm soát, hiểu hệ thống Toàn quyền, biết từng file Ít hơn, panel quản lý cấu hình
Phù hợp Vài site của chính bạn Nhiều site, khách hàng, agency

Với ba bốn site của riêng bạn, tự làm hoàn toàn hợp lý. Rủi ro tăng theo số lượng: mỗi site mới là một lần nữa có thể quên một lệnh chmod hoặc dùng nhầm pool.

Kết luận

Chạy nhiều website trên 1 VPS thì dễ; giữ chúng tách biệt mới là phần việc thật. Công thức giống nhau ở mọi quy mô: mỗi site một server block, một user Linux với thư mục home 750, một pool PHP-FPM, một user database và một chứng chỉ SSL, thêm một server mặc định hứng tên miền lạ, và nếu cần thì một slice giới hạn CPU, RAM. Nếu bạn không muốn lặp lại từng ấy bước cho mỗi site, gói Free của ZoPanel cho 3 tài khoản hosting tách biệt, 10 website và 10 database, không giới hạn thời gian. Cài trên một VPS mới bằng lệnh curl -fsSL https://get.zopanel.net | sudo bash.

Câu hỏi thường gặp

Một VPS chỉ có một IP có chạy được nhiều website không?

Được. nginx đọc header Host và, với HTTPS, tên miền SNI trong lúc bắt tay, rồi chọn server block khớp. Hàng nghìn tên miền có thể dùng chung một địa chỉ IPv4, mỗi tên miền có chứng chỉ Let's Encrypt riêng.

Chạy nhiều website trên một VPS có an toàn không?

An toàn nếu các site được tách biệt: user Linux riêng, pool PHP-FPM riêng, thư mục home khóa quyền và user database riêng. Nếu mọi site đều chạy bằng www-data, một site bị hack có thể đọc và sửa mọi site khác, đó chính là cách lây nhiễm chéo lan ra.

Có cần mỗi website một pool PHP-FPM không?

Với những site bạn quan tâm thì có. Pool quyết định user Linux nào chạy code, nên mỗi site một pool là điều giữ file của các site tách nhau. Với pm = ondemand, pool không có truy cập gần như không tốn RAM, nên nhiều pool cũng không tốn kém.

Vì sao tên miền của tôi lại hiện ra website khác?

nginx không có server block nào khớp tên miền đó nên chuyển sang server mặc định, tức server block được nạp đầu tiên nếu bạn chưa đặt default_server. Hãy thêm server mặc định trả về 444 và kiểm tra tên miền đã có trong server_name của đúng server block chưa.

VPS 4 GB chạy được bao nhiêu website?

Phụ thuộc vào lượng truy cập, plugin và cache nhiều hơn là số tên miền. Trong thử nghiệm của chúng tôi, VPS 4 GB Ubuntu 24.04 chạy được 100 website WordPress khi không có page cache và 250 website khi bật page cache, đồng thời vẫn chạy các ứng dụng khác; xem VPS 4 GB chạy được bao nhiêu website WordPress.