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.

Ý 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/examplecó quyền750, chủ sở hữuexample, nhómwww-data. User của site có toàn quyền, nginx (chạy bằngwww-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.tmpvàlogscó quyền700: chỉ PHP của chính site đó đọc được.- Shell là
nologinvì 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:
uservàgroupkhiến PHP của site chạy bằng userexample, nên chỉ đụng được vào file màexamplecó quyền.- Socket thuộc về
www-datavới quyền0660, nên nginx kết nối được còn user của site khác thì không. pm = ondemandkhô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_childrengiới hạn số request PHP site này được chạy cùng lúc.- Giá trị đặt bằng
php_admin_valuekhông thể bị code của site ghi đè bằngini_set(). Session và file upload nằm trongtmpriêng của site thay vì/tmpdù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.
Đọc tiếp

Checklist bảo mật VPS hosting: 40 mục cần kiểm tra
Checklist bảo mật VPS chạy website với 40 mục: cập nhật, SSH, tường lửa, control panel, cách ly tài khoản, lớp web và backup.

CyberPanel có an toàn không? Lịch sử lỗ hổng, cách kiểm tra và gia cố năm 2026
CyberPanel có an toàn không năm 2026? Các CVE từ đợt ransomware 2024 đến 2026, cách kiểm tra server bị xâm nhập và cách gia cố nếu tiếp tục dùng.

Cách kiểm tra VPS Linux có bị hack không
Kiểm tra VPS bị hack: đăng nhập, user, SSH key, tiến trình, kết nối mạng, cron, systemd, file hệ thống bị sửa và rootkit, kèm lệnh và dấu hiệu cần tìm.