> nginx 1.28 + PHP 8.5 + MySQL 8.4 完整安装配置,含 PHP-FPM 调优、虚拟主机、SSL、502/504 故障排查。
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl wget gnupg2 ca-certificates lsb-release \
software-properties-common unzip zip
# 添加 nginx 官方 APT 仓库
curl -fsSL https://nginx.org/keys/nginx_signing.key | sudo gpg --dearmor \
-o /usr/share/keyrings/nginx-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://nginx.org/packages/ubuntu $(lsb_release -cs) nginx" \
| sudo tee /etc/apt/sources.list.d/nginx.list
sudo apt update
sudo apt install -y nginx=1.28.*
nginx -v
# nginx version: nginx/1.28.0
sudo systemctl status nginx
sudo systemctl enable nginx
/etc/nginx/
├── nginx.conf # 主配置文件
├── conf.d/ # HTTP server 配置片段
├── sites-available/ # 可用站点配置
├── sites-enabled/ # 已启用站点(符号链接到 sites-available)
├── snippets/ # 可复用的配置片段
│ ├── ssl-params.conf
│ └── proxy-params.conf
├── fastcgi_params # FastCGI 标准参数
└── mime.types # MIME 类型映射
/var/log/nginx/
├── access.log # 访问日志
└── error.log # 错误日志
/var/www/ # 网站根目录
# 添加 ondrej/php PPA
sudo add-apt-repository ppa:ondrej/php -y
sudo apt update
sudo apt install -y \
php8.5-fpm \
php8.5-cli \
php8.5-common \
php8.5-mysql \
php8.5-mbstring \
php8.5-xml \
php8.5-curl \
php8.5-gd \
php8.5-zip \
php8.5-intl \
php8.5-bcmath \
php8.5-imagick \
php8.5-redis \
php8.5-opcache
php -v
# PHP 8.5.x (cli)
sudo systemctl status php8.5-fpm
sudo systemctl enable php8.5-fpm
# 配置文件位置
# PHP CLI: /etc/php/8.5/cli/php.ini
# PHP-FPM: /etc/php/8.5/fpm/php.ini
# Pool 配置: /etc/php/8.5/fpm/pool.d/www.conf
关键 php.ini 调整:
# /etc/php/8.5/fpm/php.ini
# 上传限制
upload_max_filesize = 100M
post_max_size = 120M
max_execution_time = 300
max_input_time = 300
# 内存限制
memory_limit = 256M
# 错误报告(生产环境)
display_errors = Off
log_errors = On
error_log = /var/log/php8.5-fpm.log
# 时区
date.timezone = Asia/Shanghai
# OPcache
opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 8
opcache.max_accelerated_files = 10000
opcache.revalidate_freq = 2
编辑 /etc/php/8.5/fpm/pool.d/www.conf:
; 进程管理模式:dynamic / static / ondemand
pm = dynamic
; dynamic 模式下的关键参数
; 最大子进程数 = (总内存 - 预留内存) / 每个进程内存
; 假设总内存 4GB,预留 1GB 给系统/nginx/mysql,每进程 ~50MB
; 则 max_children ≈ (3072) / 50 ≈ 60
pm.max_children = 60 ; 最大子进程数
pm.start_servers = 10 ; 启动时创建的子进程数
pm.min_spare_servers = 5 ; 最小空闲进程数
pm.max_spare_servers = 20 ; 最大空闲进程数
pm.max_requests = 500 ; 每个进程处理 500 个请求后重启(防内存泄漏)
| 模式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| dynamic | 一般 Web 应用 | 自动扩缩、内存效率好 | 突发流量时响应稍慢 |
| static | 高并发 API | 无 fork 开销、响应快 | 始终占用最大内存 |
| ondemand | 低频访问站点 | 空闲时不占内存 | 请求到来时有 fork 延迟 |
#!/bin/bash
# 计算建议的 max_children 值
# 保存为 /home/user/tools/php-fpm-tune.sh
TOTAL_MEM_MB=$(free -m | awk '/^Mem:/{print $2}')
RESERVED_MB=1024 # 给系统 + nginx + MySQL 预留
AVAILABLE_MB=$((TOTAL_MEM_MB - RESERVED_MB))
# 获取单个 PHP-FPM 进程的平均内存(MB)
PHP_PROC_MEM=$(ps -eo rss,cmd | grep "php-fpm: pool www" | \
awk '{sum+=$1} END {if (NR>0) printf "%.0f", sum/NR/1024; else print "50"}')
MAX_CHILDREN=$((AVAILABLE_MB / PHP_PROC_MEM))
echo "Total Memory: ${TOTAL_MEM_MB} MB"
echo "Available: ${AVAILABLE_MB} MB"
echo "PHP Process Avg: ${PHP_PROC_MEM} MB"
echo "Suggested max_children: ${MAX_CHILDREN}"
echo ""
echo "Recommended www.conf settings:"
echo " pm.max_children = ${MAX_CHILDREN}"
echo " pm.start_servers = $((MAX_CHILDREN / 4))"
echo " pm.min_spare_servers = $((MAX_CHILDREN / 8))"
echo " pm.max_spare_servers = $((MAX_CHILDREN / 3))"
; Unix Socket(推荐,同机部署,性能更好)
listen = /run/php/php8.5-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
; TCP Socket(跨服务器部署时需要)
; listen = 127.0.0.1:9000
; listen.allowed_clients = 127.0.0.1
# 修改配置后测试
sudo php-fpm8.5 -t
# 重启
sudo systemctl restart php8.5-fpm
# 查看进程数
ps aux | grep php-fpm | wc -l
# 添加 MySQL APT 仓库
wget https://dev.mysql.com/get/mysql-apt-config_0.8.32-1_all.deb
sudo dpkg -i mysql-apt-config_0.8.32-1_all.deb
# 选择 MySQL 8.4 LTS → OK
sudo apt update
sudo apt install -y mysql-server mysql-client
sudo mysql_secure_installation
# 按提示设置:
# - VALIDATE PASSWORD: 根据需要选择
# - root 密码:设置强密码
# - 移除匿名用户:Y
# - 禁止 root 远程登录:Y(建议)
# - 删除 test 数据库:Y
# - 重新加载权限表:Y
sudo systemctl status mysql
sudo mysql -u root -p -e "SELECT VERSION();"
-- 进入 MySQL
sudo mysql -u root -p
-- 创建数据库
CREATE DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 创建用户
CREATE USER 'myapp_user'@'localhost' IDENTIFIED BY 'StrongPassword123!';
CREATE USER 'myapp_user'@'%' IDENTIFIED BY 'StrongPassword123!';
-- 授予权限(本地权限)
GRANT ALL PRIVILEGES ON myapp.* TO 'myapp_user'@'localhost';
-- 授予远程权限(如需要)
GRANT ALL PRIVILEGES ON myapp.* TO 'myapp_user'@'%';
-- 刷新权限
FLUSH PRIVILEGES;
-- 验证
SHOW GRANTS FOR 'myapp_user'@'localhost';
EXIT;
# 编辑 MySQL 配置文件
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
# 将 bind-address 从 127.0.0.1 改为 0.0.0.0(或注释掉)
bind-address = 0.0.0.0
mysqlx-bind-address = 0.0.0.0
# 重启 MySQL
sudo systemctl restart mysql
# 如果开启了防火墙
sudo ufw allow 3306/tcp
-- 查看连接数
SHOW STATUS LIKE 'Threads_connected';
SHOW VARIABLES LIKE 'max_connections';
-- 查看当前连接
SHOW PROCESSLIST;
-- 杀死长时间运行的查询
KILL <process_id>;
-- 查看数据库大小
SELECT table_schema AS "Database",
ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS "Size (MB)"
FROM information_schema.tables
GROUP BY table_schema;
-- 创建备份用户(只读)
CREATE USER 'backup_user'@'localhost' IDENTIFIED BY 'BackupPass123!';
GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON *.* TO 'backup_user'@'localhost';
FLUSH PRIVILEGES;
#!/bin/bash
# 保存为 /home/user/tools/mysql-backup.sh
BACKUP_DIR="/home/user/backups/mysql"
MYSQL_USER="backup_user"
MYSQL_PASS="BackupPass123!"
DATABASES="myapp"
RETENTION_DAYS=7
mkdir -p "$BACKUP_DIR"
DATE=$(date +%Y%m%d_%H%M%S)
for DB in $DATABASES; do
mysqldump -u "$MYSQL_USER" -p"$MYSQL_PASS" \
--single-transaction --routines --triggers --events \
"$DB" | gzip > "${BACKUP_DIR}/${DB}_${DATE}.sql.gz"
done
# 删除 7 天前的备份
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
echo "Backup completed: ${BACKUP_DIR}/"
ls -lh "${BACKUP_DIR}/" | tail -5
# 添加到 crontab(每天凌晨 2 点)
# crontab -e
# 0 2 * * * /bin/bash /home/user/tools/mysql-backup.sh >> /var/log/mysql-backup.log 2>&1
/var/www/
├── myapp.com/
│ ├── public/ # Web 根目录(index.php 等)
│ ├── logs/ # 站点日志
│ └── private/ # 不应直接访问的文件
├── api.myapp.com/
│ └── public/
└── ...
# /etc/nginx/sites-available/myapp.com
server {
listen 80;
listen [::]:80;
server_name myapp.com www.myapp.com;
root /var/www/myapp.com/public;
index index.html index.htm;
access_log /var/log/nginx/myapp-access.log;
error_log /var/log/nginx/myapp-error.log warn;
location / {
try_files $uri $uri/ =404;
}
# 禁止访问隐藏文件
location ~ /\. {
deny all;
}
}
# /etc/nginx/sites-available/myapp.com
server {
listen 80;
listen [::]:80;
server_name myapp.com www.myapp.com;
root /var/www/myapp.com/public;
index index.php index.html index.htm;
access_log /var/log/nginx/myapp-access.log;
error_log /var/log/nginx/myapp-error.log warn;
# 主 location
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP 处理
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# Unix Socket 方式
fastcgi_pass unix:/run/php/php8.5-fpm.sock;
# TCP 方式
# fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
# 静态资源缓存
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
# 禁止访问隐藏文件和敏感目录
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# 禁止直接访问 private 目录
location ~ ^/private/ {
deny all;
return 404;
}
# =404 兜底
location = /favicon.ico {
log_not_found off;
access_log off;
}
location = /robots.txt {
log_not_found off;
access_log off;
}
}
# /etc/nginx/sites-available/api.myapp.com
upstream api_backend {
# 负载均衡
server 127.0.0.1:8080 weight=5;
server 127.0.0.1:8081 weight=3;
# server 10.0.0.2:8080 backup; # 备份服务器
keepalive 32;
}
server {
listen 80;
server_name api.myapp.com;
access_log /var/log/nginx/api-access.log;
error_log /var/log/nginx/api-error.log;
location / {
proxy_pass http://api_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 缓冲设置
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;
}
}
# 创建符号链接
sudo ln -s /etc/nginx/sites-available/myapp.com /etc/nginx/sites-enabled/
# 删除默认站点(如需要)
sudo rm /etc/nginx/sites-enabled/default
# 测试配置
sudo nginx -t
# 重载
sudo nginx -s reload
sudo apt install -y certbot python3-certbot-nginx
# 自动配置 nginx(推荐)
sudo certbot --nginx -d myapp.com -d www.myapp.com
# 或仅获取证书(手动配置 nginx)
sudo certbot certonly --webroot \
-w /var/www/myapp.com/public \
-d myapp.com -d www.myapp.com
创建 /etc/nginx/snippets/ssl-params.conf:
# SSL 安全参数
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
# DH 参数
ssl_dhparam /etc/nginx/dhparam.pem;
# 会话缓存
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
# HSTS(谨慎开启,一旦开启很难回退)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
sudo openssl dhparam -out /etc/nginx/dhparam.pem 2048
# 注意:2048 位约需 1-2 分钟,4096 位约需 10-30 分钟
# /etc/nginx/sites-available/myapp.com
server {
# HTTP → HTTPS 重定向
listen 80;
listen [::]:80;
server_name myapp.com www.myapp.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name myapp.com www.myapp.com;
# SSL 证书
ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
include snippets/ssl-params.conf;
root /var/www/myapp.com/public;
index index.php index.html index.htm;
access_log /var/log/nginx/myapp-ssl-access.log;
error_log /var/log/nginx/myapp-ssl-error.log warn;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/run/php/php8.5-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
location ~ /\. {
deny all;
}
}
Certbot 默认会自动配置 systemd timer。验证:
sudo systemctl status certbot.timer
sudo systemctl list-timers | grep certbot
手动测试续期(干运行):
sudo certbot renew --dry-run
如果 certbot.timer 未自动配置,手动添加:
# 创建 systemd service
sudo tee /etc/systemd/system/certbot-renewal.service << 'EOF'
[Unit]
Description=Certbot SSL Renewal
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --post-hook "nginx -s reload"
EOF
# 创建 timer(每天 03:00 + 随机延迟)
sudo tee /etc/systemd/system/certbot-renewal.timer << 'EOF'
[Unit]
Description=Daily Certbot SSL Renewal
[Timer]
OnCalendar=daily
RandomizedDelaySec=3600
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now certbot-renewal.timer
# 1. 检查 PHP-FPM 是否运行
sudo systemctl status php8.5-fpm
ps aux | grep php-fpm
# 2. 检查 Socket 文件
ls -la /run/php/php8.5-fpm.sock
# 检查权限和所有者(应为 www-data:www-data)
# 3. 检查 nginx FastCGI 配置
# 确认 fastcgi_pass 的 socket 路径与 PHP-FPM 一致
grep -r "fastcgi_pass" /etc/nginx/sites-enabled/
# 4. 查看错误日志
sudo tail -50 /var/log/nginx/error.log
sudo tail -50 /var/log/php8.5-fpm.log
# 5. 检查 PHP-FPM 是否耗尽子进程
sudo tail -20 /var/log/php8.5-fpm.log | grep "max_children"
常见原因及解决方案:
| 原因 | 检测方法 | 解决方案 |
|---|---|---|
| PHP-FPM 未运行 | systemctl status |
sudo systemctl restart php8.5-fpm |
| Socket 路径不匹配 | grep fastcgi_pass vs ls /run/php/ |
统一 socket 路径 |
| Socket 权限错误 | ls -la /run/php/ > 权限不是 0660、owner 不是 www-data |
修改 www.conf 的 listen 权限 |
| 进程耗尽 | 日志有 "max_children reached" | 增大 pm.max_children |
| PHP 脚本超时 | 脚本执行时间 > max_execution_time |
优化代码或增大超时 |
| 内存耗尽 | PHP 进程 OOM,dmesg 查看 | 减小 max_children 或增加内存 |
| SELinux/AppArmor | sudo aa-status \| grep php |
调整 AppArmor 规则 |
# 1. 检查 nginx 超时设置
grep -E "(fastcgi_read_timeout|proxy_read_timeout)" /etc/nginx/sites-enabled/*
# 2. 检查 PHP 执行时间
grep "max_execution_time" /etc/php/8.5/fpm/php.ini
# 3. 检查慢查询(MySQL)
sudo tail -50 /var/log/mysql/slow.log
# 4. 确认是哪个请求超时
sudo tail -100 /var/log/nginx/access.log | awk '$9==504'
解决方案:
# 在 nginx location 中增加超时
location ~ \.php$ {
fastcgi_read_timeout 300s; # 从默认 60s 增加
fastcgi_send_timeout 300s;
# ... 其他配置
}
# 对于 proxy_pass
location /api/ {
proxy_read_timeout 300s;
proxy_connect_timeout 30s;
# ... 其他配置
}
; php.ini 增加执行时间(如确有必要)
max_execution_time = 300
max_input_time = 300
#!/bin/bash
# 保存为 /home/user/tools/lnmp-diag.sh
echo "=== LNMP Diagnostics ==="
echo ""
# nginx
echo "[nginx]"
sudo systemctl is-active nginx || echo " WARNING: nginx not running"
sudo nginx -t 2>&1 | grep -E "ok|error"
echo " Active connections: $(curl -s http://localhost/nginx_status 2>/dev/null | grep 'Active' || echo 'stub_status not enabled')"
echo ""
# PHP-FPM
echo "[PHP-FPM]"
sudo systemctl is-active php8.5-fpm || echo " WARNING: PHP-FPM not running"
echo " Socket: $(ls -la /run/php/php8.5-fpm.sock 2>/dev/null || echo 'NOT FOUND')"
FPM_PROCS=$(ps aux | grep "php-fpm: pool" | wc -l)
echo " Worker processes: $FPM_PROCS"
echo " Last errors:"
sudo tail -3 /var/log/php8.5-fpm.log 2>/dev/null
echo ""
# MySQL
echo "[MySQL]"
sudo systemctl is-active mysql || echo " WARNING: MySQL not running"
echo " Connections: $(sudo mysqladmin status 2>/dev/null | grep -oP 'Threads: \d+' || echo 'N/A')"
echo ""
# 最近的 HTTP 错误
echo "[Recent 502/504 errors]"
sudo tail -200 /var/log/nginx/access.log 2>/dev/null | awk '$9~/502|504/' | tail -5
echo ""
echo "=== Done ==="
# /etc/nginx/nginx.conf
user www-data;
worker_processes auto; # 自动匹配 CPU 核心数
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 100;
# Gzip 压缩
gzip on;
gzip_vary on;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types text/plain text/css application/json
application/javascript text/xml application/xml
application/xml+rss text/javascript image/svg+xml;
gzip_proxied any;
gzip_disable "msie6";
# 客户端上传大小
client_max_body_size 100M;
client_body_buffer_size 128k;
# 在 conf.d/ 和 sites-enabled/ 中加载配置
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
# InnoDB 缓冲池(设为总内存的 50-70%)
innodb_buffer_pool_size = 2G
innodb_log_file_size = 512M
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
# 连接数
max_connections = 200
# 查询缓存(MySQL 8.0+ 已移除)
# 改用应用程序级缓存(Redis/Memcached)
# 慢查询日志
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
# 字符集
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
#!/bin/bash
# 保存为 /home/user/tools/deploy-php-site.sh
# 用法: bash deploy-php-site.sh myapp.com
set -e
DOMAIN=${1:?Usage: $0 domain.com}
WEBROOT="/var/www/${DOMAIN}/public"
NGINX_CONF="/etc/nginx/sites-available/${DOMAIN}"
echo "=== Deploying ${DOMAIN} ==="
# 1. 创建目录
sudo mkdir -p "${WEBROOT}"
sudo mkdir -p "/var/www/${DOMAIN}/logs"
sudo chown -R www-data:www-data "/var/www/${DOMAIN}"
# 2. 创建默认页面
sudo tee "${WEBROOT}/index.php" << 'PHPEOF'
<?php phpinfo(); ?>
PHPEOF
# 3. 创建 nginx 配置
sudo tee "$NGINX_CONF" << NGXEOF
server {
listen 80;
server_name ${DOMAIN} www.${DOMAIN};
root ${WEBROOT};
index index.php index.html;
access_log /var/log/nginx/${DOMAIN}-access.log;
error_log /var/log/nginx/${DOMAIN}-error.log;
location / {
try_files \$uri \$uri/ /index.php?\$query_string;
}
location ~ \.php\$ {
try_files \$uri =404;
fastcgi_pass unix:/run/php/php8.5-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
}
location ~ /\. { deny all; }
}
NGXEOF
# 4. 启用站点
sudo ln -sf "$NGINX_CONF" /etc/nginx/sites-enabled/
# 5. 测试并重载
sudo nginx -t && sudo nginx -s reload
# 6. 可选:申请 SSL
read -p "Apply Let's Encrypt SSL? [y/N] " apply_ssl
if [ "$apply_ssl" = "y" ] || [ "$apply_ssl" = "Y" ]; then
sudo certbot --nginx -d "${DOMAIN}" -d "www.${DOMAIN}"
fi
echo "=== Site ${DOMAIN} deployed ==="
echo "URL: http://${DOMAIN}/"