Bài 01

LEMP Stack: dựng web service đầu tiên từ con số 0

11 tháng 5, 2026 · 120 phút đọc

Bài 01 — LEMP Stack: dựng web service đầu tiên từ con số 0

Sau khi học Bash, đây là chặng đầu tiên bạn dựng được một thứ gì đó nhìn thấy được — một website thật, có domain, có HTTPS, có database. Không còn là script in ra terminal.

LEMP là combo phổ biến nhất để chạy web động trên Linux. WordPress, Magento, Laravel, Drupal — tất cả chạy được trên stack này. Học LEMP không phải để chạy WordPress, mà để hiểu mọi web stack đều có 4 thành phần: OS, web server, runtime, database. Đổi thành phần thì thành stack khác. Hiểu LEMP rồi nhìn MEAN/MERN/LAMP/Django stack đều dễ.

Bài này dài hơn các bài khác — vì 4 component liên kết với nhau, không tách được. Đọc 1 lần xong là dựng được production WordPress.


LEMP là gì

LEMP là viết tắt của 4 thành phần ghép lại thành một stack chạy web động:

  • Linux — hệ điều hành (CentOS, Ubuntu, RHEL...)
  • Enginx ("engine-x") — web server hiệu năng cao
  • MariaDB / MySQL — database
  • PHP-FPM — runtime xử lý code PHP

Khác với LAMP (dùng Apache), LEMP dùng Nginx — nhẹ hơn, hiệu suất tốt hơn cho high concurrency.

Mental model — flow 1 request đi qua đâu

[User browser]
     ↓ HTTPS request
[Nginx :443]
     ↓ nếu là .php → forward qua FastCGI
[PHP-FPM :9000 hoặc socket]thực thi PHP code, cần data
[MariaDB :3306]trả data
[PHP-FPM] → render HTML[Nginx]trả HTTP response
     ↓
[User browser]

4 hop. Mỗi hop có config riêng. Sai 1 chỗ là cả site đứng. Hiểu flow này = debug nhanh khi gặp lỗi.


Setup hệ thống — CentOS 9

Cài CentOS 9 trên VMware/VirtualBox/server thật.

Sau khi có server, việc đầu tiên — tắt SELinux (hoặc set permissive). Lý do: SELinux mặc định block nhiều thứ giữa nginx ↔ php-fpm ↔ filesystem, mới học sẽ không debug được:

# Tắt tạm thời
sudo setenforce 0

# Tắt vĩnh viễn — sửa file
sudo vim /etc/selinux/config
# Đổi: SELINUX=enforcing → SELINUX=disabled
sudo reboot

⚠️ Production thật KHÔNG nên tắt SELinux — phải config policy đúng. Nhưng cho lab và mới học, tắt để focus vào LEMP đã.

Kiểm tra:

getenforce
# Disabled hoặc Permissive

Phần 1 — Nginx

Nginx là gì

Nginx là web server hiệu năng cao, có kiến trúc bất đồng bộ và event-driven. Có thể đóng nhiều vai trò khác nhau:

  • Web Server: phục vụ các tài nguyên tĩnh như HTML, CSS, JavaScript, hình ảnh, video. Đặc biệt hiệu quả nhờ vào kiến trúc bất đồng bộ và mô hình event-driven.
  • Reverse Proxy: Nginx có thể đóng vai trò là reverse proxy — nhận request từ client và chuyển tiếp đến một hoặc nhiều máy chủ backend (ví dụ: ứng dụng web PHP, Python, hoặc các service khác).
  • Load Balancer: phân phối request đến nhiều backend để cân bằng tải và cải thiện độ ổn định. Hỗ trợ nhiều thuật toán: round-robin, least connections, IP hash.
  • Cache: cache cho tài nguyên tĩnh và kết quả động → giảm tải cho backend, tăng tốc độ phản hồi.
  • SSL/TLS Termination: nhận và giải mã SSL/TLS, giảm tải cho backend và cung cấp một điểm quản lý duy nhất cho các kết nối HTTPS.

Trong LEMP, nginx vừa làm web server (serve static files của WordPress) vừa làm reverse proxy (forward PHP request đến PHP-FPM).

Cài đặt

sudo dnf install -y nginx
nginx -v
# nginx version: nginx/1.20.1

Mặc định nginx chạy trên port 80 (http://<your_IP>).

Bật service + mở firewall

sudo systemctl enable nginx --now
sudo systemctl status nginx

# Mở firewall cho HTTP và HTTPS
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

Test: mở trình duyệt vào http://<IP-server> — thấy welcome page của nginx.

Virtual Host là gì

Virtual Host (vhost) cho phép chạy nhiều website trên cùng một máy chủ. Đây là một trong những chức năng quan trọng nhất.

Ví dụ: bạn có thể có một website chạy tại www.site1.com và một website khác chạy tại www.site2.com, tất cả trên cùng một máy chủ và cùng một địa chỉ IP. Virtual Host giúp máy chủ phân biệt và xử lý các yêu cầu từ các tên miền khác nhau, và phục vụ nội dung tương ứng.

Trong nginx, virtual host được cấu hình qua server block — một khối lệnh server { ... } trong file config.

File config nginx nằm trong: /etc/nginx/conf.d/

Tạo Virtual Host HTTP

Giả sử bạn muốn tạo vhost cho domain test.com, web root là /home/www/test.com:

Tạo 1 file cấu hình trong /etc/nginx/conf.d/:

sudo vim /etc/nginx/conf.d/test.com.conf

Thêm cấu hình server block:

server {
    listen 80;                              # lắng nghe request trên port 80 (HTTP)
    server_name test.com;

    root /home/www/test.com;                # thư mục gốc chứa file tĩnh (HTML, CSS, JS, ảnh...)
    index index.html index.htm index.php;   # thứ tự ưu tiên đọc file

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

    location / {
        # location / trong nginx là một chỉ thị xử lý mọi request có đường dẫn bắt đầu bằng /
        try_files $uri $uri/ =404;
        # try_files kiểm tra và trả về file yêu cầu nếu có, không thì trả lỗi 404
    }
}

Tạo thư mục web root và file index:

sudo mkdir -p /home/www/test.com
echo "<h1>Hello from test.com</h1>" | sudo tee /home/www/test.com/index.html

Phân quyền cho user nginx (nginx chạy dưới user này, cần quyền đọc):

sudo chown -R nginx:nginx /home/www/test.com
sudo chmod 755 /home/www/test.com

Test config và reload:

sudo nginx -t        # test syntax — phải OK trước khi reload
sudo systemctl reload nginx

Add tạm vào /etc/hosts máy client (chưa có DNS):

192.168.x.x  test.com

Vào http://test.com → thấy "Hello from test.com".

Virtual Host HTTPS — SSL self-signed

SSL self-signed là chứng chỉ TLS do chính bạn tự tạo và tự ký, không qua một CA (Certificate Authority) chính thức. Browser sẽ warning "not secure" nhưng vẫn vào được. Dùng cho lab. Production phải dùng cert từ Let's Encrypt hoặc CA thương mại.

Cài tool tạo cert:

sudo dnf install -y mod_ssl openssl

Tạo thư mục lưu chứng chỉ và khóa (private key + public key):

sudo mkdir /etc/nginx/ssl

Tạo chứng chỉ SSL và khóa riêng self-signed (valid 365 ngày):

sudo openssl req -x509 -newkey rsa:4096 \
  -keyout /etc/nginx/ssl/nginx.key \
  -out /etc/nginx/ssl/nginx.crt \
  -days 365
  • Điền PEM passphrase (tạm).
  • Điền các thông tin (Country, State, CN...). CN nên trùng domain (test.com).

Sau đó bỏ passphrase khỏi key (để nginx start không hỏi password mỗi lần):

sudo openssl rsa -in /etc/nginx/ssl/nginx.key -out /etc/nginx/ssl/nginx.key

Sửa file config vhost đã cấu hình HTTP:

sudo vim /etc/nginx/conf.d/test.com.conf

Thay nội dung:

server {
    listen 443 ssl;
    server_name test.com www.test.com;

    ssl_certificate     /etc/nginx/ssl/nginx.crt;
    ssl_certificate_key /etc/nginx/ssl/nginx.key;

    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         'HIGH:!aNULL:!MD5';
    ssl_prefer_server_ciphers on;

    access_log /var/log/nginx/test.com.access.log;
    error_log  /var/log/nginx/test.com.error.log warn;

    root  /home/www/test.com;
    index index.html index.htm;

    location / {
        # khối location này xử lý các yêu cầu đến thư mục gốc
        # kiểm tra xem file có tồn tại không, nếu không trả về lỗi 404
        try_files $uri $uri/ =404;
    }

    # Xử lý lỗi 497 — chuyển hướng user từ HTTP sang HTTPS
    error_page 497 https://$host$request_uri;
}

server {
    # Block thứ 2 — chuyển hướng vĩnh viễn (301 redirect) từ HTTP sang HTTPS
    listen 80;
    server_name test.com www.test.com;
    return 301 https://$host$request_uri;
    # $host = tên miền user yêu cầu (vd: test.com)
    # $request_uri = phần đường dẫn yêu cầu (vd: /index.html)
}

Test + reload:

sudo nginx -t
sudo systemctl reload nginx

Vào https://test.com → browser warning self-signed (bỏ qua) → thấy trang.

⚠️ Lưu ý quan trọng: tắt SELinux không sẽ không truy cập được.


Phần 2 — MariaDB

MariaDB là gì

MariaDB là database fork từ MySQL (sau khi Oracle mua MySQL năm 2009, community lo ngại nên fork ra). Tương thích 99% với MySQL, command y chang. CentOS modern default dùng MariaDB thay MySQL.

Thành phần kiến trúc của MariaDB

  • MariaDB Server: thành phần chính, bao gồm server cơ sở dữ liệu và các công cụ điều khiển cơ bản.
  • Client Utilities: các tool khách như mysql (CLI client để kết nối với MariaDB) và mysqladmin (tool quản lý server).
  • MariaDB Libraries: thư viện phần mềm dùng để phát triển ứng dụng tương tác với MariaDB.
  • Storage Engines: các engine như InnoDB, MyISAM, Aria... mỗi loại optimize cho kiểu lưu trữ dữ liệu khác nhau. InnoDB là default, hỗ trợ transaction.
  • MariaDB Configuration Files: file config như my.cnf.
  • Logs: file log như error.log, slow-query.log, general-log để ghi lại hoạt động của server.

Các thư mục trong MariaDB

Thư mục Nội dung
/usr/bin/ Công cụ client và các lệnh MariaDB
/usr/libexec/mysql/ File thực thi và script hỗ trợ MariaDB
/etc/my.cnf hoặc /etc/my.cnf.d/ File cấu hình chính của MariaDB
/var/lib/mysql/ Dữ liệu cơ sở dữ liệu — backup thư mục này
/var/log/mariadb/ Các file log của MariaDB
/etc/init.d/ hoặc /usr/lib/systemd/system/ Script và service để quản lý MariaDB
/var/run/mysqld/ PID và socket của MariaDB
/usr/share/mysql/ File hỗ trợ, tài liệu của MariaDB
/var/cache/mysql/ Cache của MariaDB

Biết các thư mục này để debug — log ở đâu, data ở đâu, config ở đâu.

Cài đặt MariaDB trên CentOS 9

sudo dnf install -y mariadb-server
sudo systemctl enable mariadb --now
sudo systemctl status mariadb

Đặt password cho database:

sudo mysql_secure_installation

Trả lời:

  • Enter current password for root → Enter (chưa có).
  • Switch to unix_socket authentication → N.
  • Change the root password? → Y, đặt password.
  • Remove anonymous users? → Y.
  • Disallow root login remotely? → Y.
  • Remove test database? → Y.
  • Reload privilege tables now? → Y.

Truy cập vào database:

mariadb -u root -p

Tạo database và user

Tạo database:

CREATE DATABASE wordpress;

Tạo user:

CREATE USER 'wordpress'@'localhost' IDENTIFIED BY 'strong_password_here';

Cấp quyền cho user vào database:

GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpress'@'localhost';

Xác nhận (reload privilege):

FLUSH PRIVILEGES;

Kiểm tra người dùng đã tạo:

SELECT user, host FROM mysql.user;

Kiểm tra quyền người dùng:

SHOW GRANTS FOR 'wordpress'@'localhost';

Thoát:

EXIT;

⚠️ 'wordpress'@'localhost' khác 'wordpress'@'%'.

  • localhost chỉ cho connect từ chính máy này.
  • % cho phép từ mọi IP.

WordPress chạy cùng server → dùng localhost an toàn hơn (chặn remote login).


Phần 3 — PHP-FPM

PHP-FPM là gì

PHP-FPM (FastCGI Process Manager) là một FastCGI (giao thức giữa web server và các ứng dụng động) dành riêng cho PHP. Trong mô hình web server WordPress, PHP-FPM có nhiệm vụ xử lý các yêu cầu PHP từ nginx (hoặc Apache, tùy cấu hình) và trả lại kết quả cho web server, giúp website hoạt động chính xác.

Khác với mô hình truyền thống (Apache + mod_php), PHP-FPM tách hoàn toàn:

  • Nginx chỉ làm web server / reverse proxy.
  • PHP-FPM là process độc lập, listen trên socket/port.
  • Hai bên giao tiếp qua FastCGI protocol.

Lợi: scale PHP-FPM độc lập, không restart nginx khi update PHP, performance tốt hơn.

FastCGI là gì

FastCGI là giao thức (protocol) giao tiếp giữa web server và các ứng dụng động (như PHP, Python, Ruby). Cải tiến từ CGI cũ — thay vì spawn process mới cho mỗi request, FastCGI giữ process sống và reuse.

Trong LEMP: nginx nhận HTTP request → đóng gói thành FastCGI request → gửi đến PHP-FPM → PHP-FPM xử lý → trả FastCGI response → nginx unpack thành HTTP response → trả browser.

Cài đặt kho EPEL và Remi

CentOS 9 default có PHP version cũ. Để cài PHP 8.2 (WordPress hiện đại yêu cầu), dùng Remi repository.

sudo dnf install -y epel-release
sudo dnf install -y https://rpms.remirepo.net/enterprise/remi-release-9.rpm

Bật kho Remi và cài đặt PHP-FPM

sudo dnf module list php
sudo dnf module enable php:remi-8.2 -y
sudo dnf install -y php-fpm php-cli php-mysqlnd php-gd php-mbstring php-xml php-zip

Kiểm tra và cấu hình PHP-FPM

File config chính: /etc/php-fpm.d/www.conf

sudo vim /etc/php-fpm.d/www.conf

Thay đổi file cấu hình:

[www]
listen = /var/run/php-fpm/www.sock
listen.allowed_clients = 127.0.0.1
listen.owner = nginx
listen.group = nginx
listen.mode = 0660
user = nginx
group = nginx
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
slowlog = /var/log/php-fpm/www-slow.log
php_admin_value[error_log] = /var/log/php-fpm/www-error.log
php_admin_flag[log_errors] = on
php_value[session.save_handler] = files
php_value[session.save_path] = /var/lib/php/session
security.limit_extensions = .php .php3 .php4 .php5 .php7

Listen qua socket hay TCP port?

⚠️ listen sẽ lắng nghe theo port hoặc theo Unix socket.

  • Đường dẫn socket Unix: /var/run/php-fpm/www.sock.
  • Nếu PHP-FPM giao tiếp với nginx trên cùng 1 máy → dùng socket.
  • Nếu khác máy → dùng cổng TCP, vd: fastcgi_pass 127.0.0.1:9000;

LEMP standard = cùng máy → dùng socket. Bài này dùng /var/run/php-fpm/www.sock.

⚠️ listen.owner = nginx cực quan trọng — owner của socket file phải là user nginx, không thì nginx không có quyền connect vào socket → site trả 502 Bad Gateway.

Khởi động PHP-FPM

sudo systemctl enable php-fpm --now
sudo systemctl status php-fpm

Verify socket được tạo:

ls -la /var/run/php-fpm/www.sock
# srw-rw---- 1 nginx nginx ...

Sửa nginx vhost để chạy PHP qua PHP-FPM

sudo vim /etc/nginx/conf.d/test.com.conf

Trong block server { ... }, thêm location ~ \.php$:

location ~ \.php$ {
    include fastcgi_params;
    fastcgi_pass unix:/var/run/php-fpm/www.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_index index.php;
}

Giải thích từng dòng config FastCGI

location ~ \.php$ { ... }

  • location: chỉ thị xác định cách nginx xử lý request theo đường dẫn.
  • ~: ký tự chỉ ra rằng biểu thức chính quy (regex) sẽ được sử dụng.
  • \.php$: regex match mọi URL kết thúc bằng .php. $ chỉ ra .php phải là phần cuối cùng của URL. Ví dụ: example.com/test.php hoặc example.com/articles.php.

include fastcgi_params;

  • Bao gồm file cấu hình fastcgi_params (thường nằm trong /etc/nginx/ hoặc conf.d/).
  • File này chứa các tham số FastCGI mặc định mà nginx gửi cho PHP-FPM. Cung cấp các thông số như: thông tin môi trường, request HTTP, đường dẫn tài nguyên, HTTP header.
  • Ví dụ: fastcgi_param SCRIPT_NAME, fastcgi_param REQUEST_METHOD, v.v.

fastcgi_pass unix:/var/run/php-fpm/www.sock;

  • fastcgi_pass: chỉ định nơi nginx chuyển tiếp request PHP.
  • unix:/var/run/php-fpm/www.sock: đường dẫn đến file socket Unix mà PHP-FPM đang lắng nghe.
  • Sử dụng Unix Domain Socket thường nhanh hơn TCP/IP và tiết kiệm tài nguyên hệ thống vì không cần qua giao thức mạng.

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

  • fastcgi_param: chỉ định một tham số môi trường cho PHP-FPM. Cho PHP-FPM biết đường dẫn tuyệt đối đến file PHP mà nginx đang yêu cầu.
  • SCRIPT_FILENAME: tham số PHP-FPM dùng để xác định file PHP cần xử lý.
  • $document_root: thư mục gốc của website (cấu hình trong directive root).
  • $fastcgi_script_name: tên script PHP user yêu cầu (vd: /test.php).
  • Khi kết hợp lại → đường dẫn đầy đủ đến file PHP, PHP-FPM dùng để thực thi mã PHP đúng.

fastcgi_index index.php;

  • Chỉ định tên file index mặc định mà nginx sẽ tìm khi request một thư mục.
  • Ví dụ: user truy cập /blog/ (không chỉ rõ file) → nginx tự động yêu cầu index.php trong thư mục đó.

Socket Unix là gì

Socket Unix (Unix Domain Socket) là một cơ chế giao tiếp giữa các tiến trình (Inter-Process Communication, IPC) trong hệ điều hành Unix và các hệ điều hành tương thích như Linux. Cho phép các tiến trình trên cùng một hệ thống chia sẻ dữ liệu và giao tiếp với nhau mà không cần thông qua mạng (khác với giao tiếp qua TCP/IP). Điều này giúp tăng tốc độ và hiệu suất.

Giữa nginx và PHP-FPM: PHP-FPM thường sử dụng Unix Domain Socket để giao tiếp với web server (nginx) thay vì TCP/IP, giúp giảm độ trễ và tăng hiệu suất.

Test config + reload:

sudo nginx -t
sudo systemctl reload nginx

Test PHP chạy — tạo file info.php:

echo "<?php phpinfo(); ?>" | sudo tee /home/www/test.com/info.php
sudo chown nginx:nginx /home/www/test.com/info.php

Vào https://test.com/info.php → thấy trang info PHP với version, module, config.

⚠️ Sau khi test xong, XOÁ info.php — file này leak nhiều thông tin server, hacker dùng để recon:

sudo rm /home/www/test.com/info.php

Phần 4 — WordPress

Download và setup

Điều hướng đến thư mục web root:

cd /home/www/test.com

Tải WordPress:

sudo curl -O https://wordpress.org/latest.tar.gz

Giải nén:

sudo tar -xvf latest.tar.gz
sudo mv wordpress/* .
sudo rm -rf wordpress latest.tar.gz

Đặt quyền sở hữu và phân quyền:

sudo chown -R nginx:nginx /home/www/test.com
sudo chmod -R 755 /home/www/test.com

Kiến trúc WordPress

Sau khi tải WordPress về, các file/folder chính trong thư mục gốc /home/www/test.com:

  • wp-admin/: quản lý backend của WordPress (login, viết bài, quản lý plugin).
  • wp-content/: chứa theme, plugin và các file user upload. Đây là thư mục bạn backup nhiều nhất.
  • wp-includes/: chứa các file lõi (core) của WordPress.
  • Các file chính: index.php, wp-config.php, wp-login.php, v.v. — chịu trách nhiệm xử lý các tác vụ cơ bản và cấu hình hệ thống WordPress.

Cấu hình wp-config.php

wp-config.php là file config kết nối WordPress với database. Đây là file quan trọng nhất của WordPress, chứa thông tin xác thực database và secret keys.

Copy config mẫu:

sudo cp wp-config-sample.php wp-config.php

Sửa file wp-config.php:

sudo vim wp-config.php

Cập nhật thông tin database:

define('DB_NAME',     'wordpress');
define('DB_USER',     'wordpress');
define('DB_PASSWORD', 'strong_password_here');
define('DB_HOST',     'localhost');

Quan trọng — thứ tự index trong nginx

⚠️ Cấu hình trong /etc/nginx/conf.d/test.com.conf trong thư mục gốc /home/www/test.com — đưa file index.php lên trước index.html. Nginx sẽ ưu tiên đọc file nào đứng trước.

index index.php index.html index.htm;

Khởi động lại nginx:

sudo systemctl restart nginx

Hoàn tất cài đặt qua browser

Vào https://test.com → WordPress sẽ hiển thị wizard cài đặt:

  • Site title, admin username, admin password, admin email.
  • Bấm "Install WordPress".

Xong — WordPress đã chạy.


Mô hình hoạt động end-to-end của WordPress

Tổng kết flow của 1 request từ user đến WordPress:

  1. Người dùng yêu cầu trang web: trình duyệt của user gửi HTTP/HTTPS request đến Web Server nginx.

  2. Web Server chuyển tiếp yêu cầu: nếu request là PHP (vd: trang bài viết hoặc trang admin), web server chuyển tiếp đến PHP-FPM để xử lý. Nếu là static file (CSS, JS, ảnh) → nginx serve trực tiếp từ disk.

  3. PHP-FPM xử lý mã PHP: PHP-FPM thực thi code PHP của WordPress, bao gồm truy vấn database MariaDB/MySQL để lấy dữ liệu (nội dung bài viết, thông tin user, cài đặt).

  4. Gửi kết quả về Web Server: sau khi PHP-FPM xử lý xong, kết quả (HTML) được trả về web server.

  5. Web Server trả kết quả cho người dùng: web server trả lại kết quả (HTML) cho user, và trang web được hiển thị trên trình duyệt.

4 component, mỗi cái chịu trách nhiệm 1 việc rõ ràng. Tách bạch đẹp = dễ debug + dễ scale (scale từng layer độc lập).


Câu chuyện thực tế

Lần đầu mình deploy LEMP cho khách, mọi thứ làm đúng theo doc — nginx OK, MariaDB OK, PHP-FPM OK. Vào trang chủ thấy WordPress wizard. Nhưng vừa bấm "Install WordPress" → 502 Bad Gateway.

Reload page — vẫn 502. Restart nginx — vẫn 502.

tail -f /var/log/nginx/error.log:

connect() to unix:/var/run/php-fpm/www.sock failed (13: Permission denied) while connecting to upstream

Permission denied khi nginx connect vào socket file của PHP-FPM.

Check socket:

ls -la /var/run/php-fpm/www.sock
# srw-rw---- 1 apache apache ...

Owner socket là apache:apache — không phải nginx:nginx. Lý do: file /etc/php-fpm.d/www.conf default có:

listen.owner = apache
listen.group = apache
user = apache
group = apache

Mình đã đổi usergroup thành nginx nhưng quên đổi listen.ownerlisten.group → socket file tạo ra thuộc user apache → nginx không có quyền connect.

Fix:

sudo vim /etc/php-fpm.d/www.conf
# Sửa cả 4 dòng:
# listen.owner = nginx
# listen.group = nginx
# user = nginx
# group = nginx

sudo systemctl restart php-fpm
ls -la /var/run/php-fpm/www.sock
# srw-rw---- 1 nginx nginx ...   ← OK

Reload trang → WordPress install xong.

Bài học:

  1. 502 Bad Gateway 90% là nginx ↔ php-fpm communication issue. Đọc nginx error log đầu tiên, không phải PHP log.
  2. Khi đổi user của PHP-FPM, đổi cả 4 dòng: user, group, listen.owner, listen.group. Quên 1 dòng là 502.
  3. Socket file permission quan trọng hơn nhiều người nghĩ — Unix socket cũng là 1 file, có owner và mode, đối xử y như file thường.
  4. SELinux nếu chưa tắt sẽ thêm 1 lớp permission nữa — có thể block kể cả khi file permission đúng. Production phải config policy đàng hoàng.

Lab — chạy từ đầu đến cuối

Toàn bộ command để dựng LEMP + WordPress trên CentOS 9:

# === SETUP ===
sudo setenforce 0
sudo sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config

# === NGINX ===
sudo dnf install -y nginx
sudo systemctl enable nginx --now
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

# === MARIADB ===
sudo dnf install -y mariadb-server
sudo systemctl enable mariadb --now
sudo mysql_secure_installation
# (trả lời các câu hỏi)

mariadb -u root -p <<EOF
CREATE DATABASE wordpress;
CREATE USER 'wordpress'@'localhost' IDENTIFIED BY 'MyStrongPass123!';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpress'@'localhost';
FLUSH PRIVILEGES;
EOF

# === PHP-FPM ===
sudo dnf install -y epel-release
sudo dnf install -y https://rpms.remirepo.net/enterprise/remi-release-9.rpm
sudo dnf module enable php:remi-8.2 -y
sudo dnf install -y php-fpm php-cli php-mysqlnd php-gd php-mbstring php-xml php-zip

# Sửa user trong www.conf — ĐỔI CẢ 4 DÒNG
sudo sed -i 's/^user = apache/user = nginx/' /etc/php-fpm.d/www.conf
sudo sed -i 's/^group = apache/group = nginx/' /etc/php-fpm.d/www.conf
sudo sed -i 's/^;listen.owner = .*/listen.owner = nginx/' /etc/php-fpm.d/www.conf
sudo sed -i 's/^;listen.group = .*/listen.group = nginx/' /etc/php-fpm.d/www.conf

sudo systemctl enable php-fpm --now

# === SSL SELF-SIGNED ===
sudo mkdir -p /etc/nginx/ssl
sudo openssl req -x509 -newkey rsa:4096 -nodes \
  -keyout /etc/nginx/ssl/nginx.key \
  -out /etc/nginx/ssl/nginx.crt \
  -days 365 \
  -subj "/CN=test.com"

# === VHOST ===
sudo mkdir -p /home/www/test.com

sudo tee /etc/nginx/conf.d/test.com.conf <<'EOF'
server {
    listen 443 ssl;
    server_name test.com www.test.com;

    ssl_certificate     /etc/nginx/ssl/nginx.crt;
    ssl_certificate_key /etc/nginx/ssl/nginx.key;
    ssl_protocols       TLSv1.2 TLSv1.3;

    root  /home/www/test.com;
    index index.php index.html;

    access_log /var/log/nginx/test.com.access.log;
    error_log  /var/log/nginx/test.com.error.log warn;

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

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/var/run/php-fpm/www.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_index index.php;
    }
}

server {
    listen 80;
    server_name test.com www.test.com;
    return 301 https://$host$request_uri;
}
EOF

# === WORDPRESS ===
cd /home/www/test.com
sudo curl -O https://wordpress.org/latest.tar.gz
sudo tar -xvf latest.tar.gz
sudo mv wordpress/* .
sudo rm -rf wordpress latest.tar.gz

sudo cp wp-config-sample.php wp-config.php
sudo sed -i "s/database_name_here/wordpress/" wp-config.php
sudo sed -i "s/username_here/wordpress/" wp-config.php
sudo sed -i "s/password_here/MyStrongPass123!/" wp-config.php

sudo chown -R nginx:nginx /home/www/test.com
sudo chmod -R 755 /home/www/test.com

# === RELOAD ===
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl restart php-fpm

echo ""
echo "==> Add to client /etc/hosts:"
echo "$(hostname -I | awk '{print $1}')  test.com www.test.com"
echo ""
echo "==> Then access: https://test.com"

Sau khi chạy hết, add domain vào /etc/hosts của máy client → vào https://test.com → hoàn tất wizard WordPress qua browser.


Pitfalls

  1. SELinux block silent — nginx báo 502 hoặc 403, log không rõ ràng. Check getenforce, tắt hoặc set permissive cho lab.

  2. Socket file owner sai — đổi user mà quên listen.owner → 502 Bad Gateway. Đổi cả 4 dòng trong www.conf.

  3. index.html đứng trước index.php trong vhost → WordPress không hoạt động (nginx ưu tiên html). Đổi thứ tự.

  4. Phân quyền sai web root — nginx không đọc được → 403 Forbidden. chown -R nginx:nginx cho web root.

  5. Quên reload nginx sau khi sửa configsudo nginx -t (test) + sudo systemctl reload nginx.

  6. MariaDB user dùng @'%' thay @'localhost' — WordPress cùng server không cần %. Dùng localhost an toàn hơn (chặn remote login).

  7. Để info.php trên production — leak version PHP, module, path → hacker recon. Xoá sau khi test.

  8. Self-signed cert trên production — browser warning "not secure", mobile app reject. Production dùng Let's Encrypt (free, auto-renew với certbot).

  9. Firewall không mở port 443 — site HTTPS không truy cập được. firewall-cmd --add-service=https --permanent.

  10. PHP-FPM pool config saipm.max_children thấp → site chậm khi nhiều user. Default 50 thường đủ cho site nhỏ, site lớn cần tune.


Tóm tắt

  • LEMP = Linux + Nginx + MariaDB + PHP-FPM. 4 component liên kết qua socket/port.
  • Flow request: Browser → Nginx → (static thì serve, .php thì forward) → PHP-FPM → MariaDB → trả ngược lại.
  • Nginx vhost trong /etc/nginx/conf.d/. Test bằng nginx -t trước khi reload.
  • PHP-FPM cùng máy → Unix socket (nhanh hơn TCP). Đổi user phải đổi cả 4 dòng trong www.conf.
  • MariaDB: tạo DB và user riêng cho mỗi app. Production dùng @'localhost' thay @'%'.
  • 502 Bad Gateway = nginx ↔ php-fpm issue. Đọc /var/log/nginx/error.log đầu tiên.
  • Production: dùng Let's Encrypt thay self-signed, config SELinux đàng hoàng thay tắt.

Bài tiếp: 02 — Nginx Reverse Proxy: từ web server đến lá chắn của backend.

Bài tiếp theo →
Bài 02
Zalo tư vấn