
1. 项目概述为什么在 Ubuntu 20.04 上搭建 LEMP 栈仍是硬核入门的黄金路径如果你刚接触服务器运维、Web 开发或全栈部署看到“Linux, Nginx, MySQL, PHP”这四个词组合在一起大概率会下意识联想到 LAMPApache 版本——但真正跑在生产环境里扛住百万级并发请求的八成是 LEMP。LEMP 不是缩写游戏它是 Linux Nginx MySQL PHP 四个组件构成的轻量、高效、可扩展的 Web 服务基础栈。而 Ubuntu 20.04 LTSFocal Fossa作为长期支持版本内核稳定、软件源成熟、社区文档丰富至今仍是企业测试环境、中小型业务后端、学生实验平台和 DevOps 入门训练的首选发行版。我带过十几届实习生从零开始配环境90% 的人卡在第一步不是不会敲命令而是不理解每一步背后的“意图”。比如sudo apt update看似只是刷新包列表实则是告诉系统“我要从官方源拉取最新软件元数据包括版本号、依赖树、校验哈希——如果跳过这步后面装的 nginx 可能是旧版连 TLS 1.3 都不支持。”再比如systemctl enable nginx和systemctl start nginx的区别前者是“开机自启注册”后者是“此刻立刻运行”就像给汽车插上钥匙并点火——没注册重启就熄火只注册不点火车停在车位里纹丝不动。这篇内容不讲“复制粘贴就能跑”而是带你把每个命令拆开揉碎看清它在操作系统层面触发了什么动作、修改了哪些配置文件、监听了哪个端口、以哪个用户身份运行。你不需要提前懂 C 语言或 TCP/IP 协议栈但读完后当浏览器输入http://localhost显示 “Welcome to nginx!”你会知道那行字背后是 Nginx 主进程 fork 出 worker 进程worker 用 epoll 模型监听 80 端口收到 HTTP GET 请求后从/var/www/html目录读取index.nginx-debian.html并返回响应头Content-Type: text/html。这才是真正意义上的“安装完成”——不是终端没报错而是你心里有图。2. 整体设计与思路拆解为什么选 Ubuntu 20.04 LEMP 而非其他组合2.1 发行版选择Ubuntu 20.04 的不可替代性很多人问“CentOS 7 不是更稳定吗Debian 11 不是更精简吗Arch Linux 不是更纯粹吗”答案很实在Ubuntu 20.04 是一个“平衡点工程”。它不像 Arch 那样要求你手动编译内核模块也不像 CentOS Stream 那样把开发分支当稳定版推给用户。它的 APT 包管理器经过 20 年打磨依赖解析准确率极高它的 systemd 服务管理逻辑清晰journalctl -u nginx一行命令就能查清 Nginx 启动失败是因为/etc/nginx/sites-enabled/default里少了个分号它的默认防火墙 ufw 命令简洁ufw allow Nginx Full就自动放行 80/443 端口比手写 iptables 规则少出 80% 的低级错误。更重要的是Ubuntu 20.04 的生命周期到 2025 年 4 月这意味着你现在装的环境未来三年内都能获得安全补丁更新。我去年帮一家本地教育机构部署在线考试系统他们用的是物理服务器管理员只会基础 Linux 命令我们坚持用 Ubuntu 20.04 而非更新的 22.04就是因为 20.04 的 PHP 7.4 和 MySQL 8.0 组合在他们的老旧 ThinkPad T440p 笔记本上跑 Docker 容器时内存占用更低——实测下来同样加载 500 个学生试卷 PDF20.04 平均内存占用 1.2GB22.04 因为 systemd-journald 日志压缩策略更激进反而多占 300MB。这不是参数堆砌而是真实硬件约束下的务实选择。2.2 组件选型Nginx 替代 Apache 的底层逻辑Apache 是 Web 服务器的“老大哥”但它用的是“进程/线程模型”每个 HTTP 请求分配一个独立进程或线程处理完再销毁。这种模式在小流量网站上很友好但当并发连接数超过 1000进程创建销毁的开销就会吃掉大量 CPU。Nginx 则采用“事件驱动异步非阻塞模型”核心就一个 master 进程加多个 worker 进程每个 worker 用单线程处理成千上万个连接——靠的是 Linux 的 epoll 机制。你可以把它想象成餐厅经理master只负责派单服务员worker手里拿个电子点餐器同时盯 50 张桌子哪张桌按铃就立刻响应而不是每张桌配一个专属服务员干等。所以当你执行nginx -t测试配置语法再systemctl restart nginx重载服务时Nginx 实际上是先启动新 worker等新 worker 完全就绪后再优雅关闭旧 worker整个过程用户无感知。而 Apache 的apachectl graceful虽然也叫“优雅重启”但底层还是得 fork 新进程旧进程要等所有请求处理完才退出高负载下容易卡顿。这也是为什么搜索热词里“深入浅出 nginx 实战”“nginx 反向代理”“nginx 配置文件详解”常年霸榜——它不只是个静态文件服务器更是现代 Web 架构的流量调度中枢。2.3 数据库与语言层MySQL 8.0 与 PHP 7.4 的协同设计Ubuntu 20.04 默认源里的 MySQL 是 8.0.28PHP 是 7.4.3。这个组合不是偶然。MySQL 8.0 引入了原子 DDL数据定义语言意味着ALTER TABLE修改表结构时要么全部成功要么全部回滚不会出现“字段加了一半卡住”的尴尬它的默认认证插件从mysql_native_password改为caching_sha2_password安全性更高但 PHP 7.4 的mysqli扩展原生支持它不用额外装php-mysqlnd。反观如果强行升级到 PHP 8.2虽然性能提升 15%但很多老项目用的mysql_*函数早已废弃会直接报错迁移成本远超收益。我见过最典型的案例某政务系统后台用 CodeIgniter 2.x 框架数据库连接字符串里写的是mysql://user:passlocalhost/dbname升级 PHP 8 后mysql://协议被彻底移除必须改成mysqli://或pdo://光改配置文件就花了两天还得逐个测试 37 个控制器是否兼容。所以“安装教程”里强调版本匹配本质是在帮你避开这些看不见的坑。PHP 7.4 的另一个优势是 OPCache 默认开启且配置合理opcache.enable1和opcache.memory_consumption128这两个参数在 Ubuntu 20.04 的/etc/php/7.4/fpm/php.ini里已经预设好你不用像在 CentOS 上那样手动调优。2.4 安全基线从安装第一天就埋下防护意识很多人把 LEMP 当作“能跑就行”的玩具环境结果上线三天就被扫出漏洞。Ubuntu 20.04 的设计者早就考虑到了这点。它的默认 SSH 配置禁用了密码登录只允许密钥ufw防火墙默认拒绝所有入站连接apt安装的软件包都经过 GPG 签名验证。我们在安装 MySQL 时执行sudo mysql_secure_installation它不只是让你设 root 密码还会问“是否删除匿名用户是否禁止 root 远程登录是否删除 test 数据库是否重新加载权限表”每一个“yes”都在加固攻击面。比如“禁止 root 远程登录”意味着黑客即使爆破出 root 密码也只能在服务器本地登录无法从外网直接拖库。再比如 Nginx 默认不启用.htaccess类似的覆盖文件所有配置必须写在主配置里避免开发者误传.git目录或备份文件config.php.bak被直接下载。这些不是“高级功能”而是 Ubuntu 20.04 把安全当作默认属性写进基因里的体现。所以这篇内容里所有sudo命令都会明确告诉你“为什么需要提权”所有chmod操作都会解释“为什么目录要 755 而文件是 644”因为权限混乱是 70% 的 Web 服务故障根源。3. 核心细节解析与实操要点从系统初始化到服务就绪的完整链路3.1 系统初始化别跳过apt update和apt upgrade的深层含义很多新手看到教程第一句“先运行sudo apt update sudo apt upgrade -y”就机械执行却不知道这两条命令在做什么。apt update本质是下载/var/lib/apt/lists/目录下的索引文件这些文件包含所有软件包的版本、大小、依赖关系和 SHA256 校验值。它不安装任何东西只是“更新菜单”。而apt upgrade是根据新菜单检查已安装软件是否有更新并批量升级。关键点在于apt upgrade不会升级发行版大版本比如从 20.04 升到 22.04它只做小版本迭代如 MySQL 8.0.28 → 8.0.32。如果你跳过apt updateapt upgrade就会拿着过期菜单去查可能漏掉关键安全补丁。我遇到过最离谱的一次某公司测试服务器半年没apt updateapt upgrade后 MySQL 自动升到 8.0.33但他们的 PHP 应用用的是mysql_connect()函数PHP 7.4 已废弃结果整个后台白屏查日志才发现是Call to undefined function mysql_connect()。所以我的实操习惯是先sudo apt update观察输出末尾是否有Hit:Get:行确认源地址正常再sudo apt list --upgradable列出所有待升级包人工核对有没有 MySQL、Nginx、PHP 相关项最后sudo apt upgrade -y并用sudo apt autoremove -y清理无用依赖。提示apt autoremove不是可选项。Ubuntu 在安装软件时会自动装一堆依赖包如libnginx-mod-http-geoip2卸载主程序后这些依赖还在硬盘上占空间。定期清理能省下 200MB~1GB对虚拟机尤其重要。3.2 Nginx 安装与验证从二进制包到进程树的全视角观察Ubuntu 20.04 的官方源里 Nginx 版本是 1.18.0足够满足绝大多数需求。安装命令sudo apt install nginx看似简单但背后发生了很多事APT 会自动解决依赖比如nginx-core核心二进制、nginx-common配置模板、libnginx-mod-http-image-filter图片处理模块安装脚本会创建系统用户www-dataUID 33这是 Nginx worker 进程的运行身份所有 Web 文件必须对它可读它会把默认站点配置写入/etc/nginx/sites-available/default并创建软链接/etc/nginx/sites-enabled/default最后自动执行systemctl daemon-reload让 systemd 重新加载服务定义。验证是否成功不能只看curl http://localhost要分三层检查进程层ps aux | grep nginx应看到root用户运行的 master 进程和www-data用户运行的 worker 进程端口层sudo ss -tuln | grep :80确认LISTEN状态且 PID 对应 nginx日志层sudo tail -f /var/log/nginx/access.log然后在另一终端curl http://localhost日志里应立即出现127.0.0.1 - - [xx/xxx/xxxx:xx:xx:xx 0000] GET / HTTP/1.1 200 612 - curl/7.68.0。注意如果curl返回Connection refused90% 是防火墙问题。执行sudo ufw status verbose确认Nginx Full状态是ALLOW IN。如果是云服务器还要检查安全组规则是否开放 80 端口。3.3 MySQL 安装与安全加固mysql_secure_installation的每一问都是防线sudo apt install mysql-server安装后MySQL 会自动生成 root 密码并存入/etc/mysql/debian.cnf但这个密码只供系统内部使用。我们必须运行sudo mysql_secure_installation来设置真正的管理密码。它的交互式提问看似简单实则每一步都对应一个攻击面Switch to unix_socket authentication?是否切换为 Unix socket 认证选No。Ubuntu 默认用auth_socket插件root 只能通过sudo mysql本地登录无法用密码远程连接。选No才会进入密码设置流程New password for root?密码必须含大小写字母数字特殊字符长度≥8。我建议用openssl rand -base64 12生成Remove anonymous users?删除匿名用户选Yes。匿名用户localhost可以不输密码登录是最大安全隐患Disallow root login remotely?禁止 root 远程登录选Yes。生产环境必须用普通用户如app_user授权访问特定数据库Remove test database?删除 test 数据库选Yes。test 库默认允许所有用户读写扫描器最爱扫它Reload privilege tables now?重载权限表选Yes否则前面设置不生效。执行完后用sudo mysql -u root -p输入新密码验证再执行SELECT user,host,plugin FROM mysql.user;确认 root 用户的 plugin 是caching_sha2_passwordhost 是localhost没有%。3.4 PHP 安装与 FPM 配置为什么不用libapache2-mod-php而用php-fpmUbuntu 20.04 的 PHP 7.4 默认安装的是php7.4-fpmFastCGI Process Manager不是 Apache 的模块。这是因为 Nginx 本身不解析 PHP它需要把.php请求转发给外部 PHP 解释器。FPM 就是这个解释器的管理者它用 master-worker 模型比传统 CGI 模式快 10 倍。安装命令sudo apt install php7.4-fpm php7.4-mysql php7.4-curl php7.4-gd php7.4-mbstring php7.4-xml php7.4-xmlrpc php7.4-zip中php7.4-mysql是关键——它提供 MySQLi 和 PDO_MySQL 扩展让 PHP 能连 MySQL 8.0。安装后FPM 服务默认是inactive (dead)必须手动启动sudo systemctl start php7.4-fpm并sudo systemctl enable php7.4-fpm。验证方式sudo systemctl status php7.4-fpm查看 Active 状态sudo ss -tuln | grep :9000确认 FPM 监听 9000 端口Unix socket/run/php/php7.4-fpm.sock更常用性能更好。实操心得Nginx 配置里fastcgi_pass必须和 FPM 监听方式一致。如果 FPM 用 socketNginx 就写fastcgi_pass unix:/run/php/php7.4-fpm.sock;如果用 TCP就写fastcgi_pass 127.0.0.1:9000;。我始终推荐 socket因为/run/php/是内存文件系统 tmpfs读写速度比磁盘快 3 倍。4. 实操过程与核心环节实现手把手构建可运行的 PHPMySQL 站点4.1 创建测试数据库与用户最小权限原则的落地实践不要用 root 用户开发这是铁律。我们创建一个专用数据库myapp_db和用户myapp_usersudo mysql -u root -p # 输入 root 密码后进入 MySQL 命令行 CREATE DATABASE myapp_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER myapp_userlocalhost IDENTIFIED WITH caching_sha2_password BY StrongPass123!; GRANT SELECT, INSERT, UPDATE, DELETE ON myapp_db.* TO myapp_userlocalhost; FLUSH PRIVILEGES; EXIT;注意三个细节CHARACTER SET utf8mb4是必须的。MySQL 的utf8实际只支持 3 字节 UTF-8 字符如中文不支持 emoji4 字节utf8mb4才是真正的 UTF-8IDENTIFIED WITH caching_sha2_password明确指定认证插件避免因 MySQL 8.0 默认插件变更导致 PHP 连接失败GRANT语句只给 CRUD 权限不给DROP或CREATE遵循最小权限原则。验证用户是否创建成功sudo mysql -u myapp_user -p -D myapp_db能登录且只能操作myapp_db说明权限控制生效。4.2 配置 Nginx 虚拟主机从默认站点到项目专属的平滑过渡Ubuntu 的默认站点配置在/etc/nginx/sites-available/default我们不直接修改它而是创建新配置/etc/nginx/sites-available/myappserver { listen 80; server_name localhost; root /var/www/myapp; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php7.4-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } location ~ /\.ht { deny all; } }关键点解析root /var/www/myapp定义网站根目录必须手动创建sudo mkdir -p /var/www/myapptry_files $uri $uri/ /index.php?$query_string这是 Laravel、WordPress 等框架必需的“前端控制器”规则把所有非静态资源请求都转给index.php处理fastcgi_param SCRIPT_FILENAME必须显式设置否则 PHP 会找不到脚本路径报错File not foundlocation ~ /\.ht禁止访问.htaccess类文件虽然 Nginx 不认它但防止误传敏感文件。启用配置sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/然后sudo nginx -t测试语法sudo systemctl reload nginx重载配置。提示reload比restart更安全。reload是平滑重启旧 worker 处理完当前请求再退出restart是强制杀进程再启动可能导致正在上传的文件中断。4.3 编写 PHP 连接测试脚本用最简代码验证全链路在/var/www/myapp/index.php写入以下内容?php $host localhost; $dbname myapp_db; $username myapp_user; $password StrongPass123!; try { $pdo new PDO(mysql:host$host;dbname$dbname;charsetutf8mb4, $username, $password, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::MYSQL_ATTR_INIT_COMMAND SET NAMES utf8mb4 ]); echo ✅ MySQL 连接成功br; // 创建测试表 $pdo-exec(CREATE TABLE IF NOT EXISTS test_table ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;); echo ✅ 测试表创建成功br; // 插入测试数据 $stmt $pdo-prepare(INSERT INTO test_table (name) VALUES (?)); $stmt-execute([LEMP Stack Test]); echo ✅ 测试数据插入成功br; // 查询数据 $rows $pdo-query(SELECT * FROM test_table)-fetchAll(); echo 查询结果pre . print_r($rows, true) . /pre; } catch (PDOException $e) { echo ❌ 连接失败 . $e-getMessage(); } ?这段代码做了四件事建立 PDO 连接、建表、插数据、查数据。其中PDO::MYSQL_ATTR_INIT_COMMAND SET NAMES utf8mb4是关键它确保连接初始化时就设置字符集避免中文乱码。访问http://localhost如果看到 ✅ 和 说明 LEMP 全链路打通。如果报错按顺序排查Nginx 错误日志sudo tail -f /var/log/nginx/error.logPHP-FPM 日志sudo tail -f /var/log/php7.4-fpm.logMySQL 错误日志sudo tail -f /var/log/mysql/error.log。4.4 权限与所有权设置www-data用户的正确归位很多 PHP 报错如Permission denied或Failed to open stream根源都在文件权限。Ubuntu 的标准做法是Web 根目录/var/www/myapp所有者设为你的普通用户如ubuntu组设为www-data目录权限755所有者读写执行组和其他人读执行文件权限644所有者读写组和其他人只读www-data组对目录有执行权限x才能进入目录读取文件。执行命令sudo chown -R $USER:www-data /var/www/myapp sudo find /var/www/myapp -type d -exec chmod 755 {} \; sudo find /var/www/myapp -type f -exec chmod 644 {} \;特别注意/var/www/myapp目录本身必须对www-data组有x权限否则 Nginx worker 进程以www-data身份运行根本进不去目录。你可以用ls -ld /var/www/myapp验证输出应类似drwxr-xr-x 3 ubuntu www-data 4096 ...。如果看到drwx------说明权限太严Nginx 会被拒之门外。5. 常见问题与排查技巧实录那些文档里不会写的实战血泪史5.1 Nginx 启动失败bind() to 0.0.0.0:80 failed (98: Address already in use)这是新手最高频报错。原因只有一个80 端口被占用了。但“被谁占”需要深挖第一顺位检查 Apachesudo systemctl status apache2如果 activesudo systemctl stop apache2 sudo systemctl disable apache2第二顺位检查其他 Nginx 实例sudo ss -tuln | grep :80看 PID再sudo ps aux | grep PID确认进程名第三顺位检查 Dockersudo docker ps看有没有容器映射了 80 端口如nginx:alpine第四顺位检查nginx.conf是否重复listen 80比如在sites-enabled/default和myapp里都写了listen 80Nginx 会尝试绑定两次。排查技巧用sudo lsof -i :80一键定位占用进程。如果lsof未安装sudo apt install lsof即可。5.2 PHP 连接 MySQL 失败SQLSTATE[HY000] [2054] The server requested authentication method unknown to the client这是 MySQL 8.0 的经典坑。错误提示直指认证插件不兼容。解决方案分两步登录 MySQL把用户认证插件切回旧版ALTER USER myapp_userlocalhost IDENTIFIED WITH mysql_native_password BY StrongPass123!; FLUSH PRIVILEGES;在 PHP 连接字符串里显式指定插件$pdo new PDO(mysql:hostlocalhost;dbnamemyapp_db;charsetutf8mb4, $username, $password, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::MYSQL_ATTR_PLUGIN_DIR /usr/lib/mysql/plugin/, ]);但更推荐的做法是保持caching_sha2_password升级 PHP 到 7.4.1 以上Ubuntu 20.04 默认满足并在php.ini里确保extensionmysqli和extensionpdo_mysql已启用。因为caching_sha2_password更安全是未来趋势。5.3 页面显示 PHP 源码而非执行结果Nginx 未正确转发给 PHP-FPM现象是浏览器直接显示?php echo hello; ?的原始代码。原因必然是 Nginx 的location ~ \.php$块没生效。排查步骤检查 Nginx 配置是否启用ls -l /etc/nginx/sites-enabled/确认myapp软链接存在检查fastcgi_pass地址是否和 PHP-FPM 实际监听一致sudo cat /etc/php/7.4/fpm/pool.d/www.conf | grep listen输出应为listen /run/php/php7.4-fpm.sock检查fastcgi_param SCRIPT_FILENAME是否拼写正确常见错误是$document_root$fastcgi_script_name写成$document_root/fastcgi_script_name少了$检查 PHP-FPM 服务状态sudo systemctl status php7.4-fpm必须是active (running)。实操心得在location ~ \.php$块里加一行add_header X-PHP-Status Processed;然后curl -I http://localhost/test.php如果响应头里有X-PHP-Status说明 Nginx 确实进了这个 location 块如果没有说明请求没匹配上正则可能是文件后缀不是.php或路径不对。5.4 MySQL 表碎片问题php mysql 某个表有碎片,一般怎么处理搜索热词里提到的“表碎片”本质是 InnoDB 表在频繁DELETE或UPDATE后页内产生空闲空间但未被回收。它不影响功能但浪费磁盘且降低查询效率。检测方法SELECT table_name, round(((data_length index_length) / 1024 / 1024), 2) as size_mb, round((data_free / 1024 / 1024), 2) as free_mb FROM information_schema.TABLES WHERE table_schema myapp_db AND data_free 0;如果free_mbsize_mb的 20%就需要优化。安全做法是OPTIMIZE TABLE test_table;它会重建表并释放碎片。但注意OPTIMIZE TABLE会锁表高并发写入时慎用。更温和的方式是ALTER TABLE test_table ENGINEInnoDB;效果相同但语法更直观。对于超大表10GB建议在业务低峰期执行并监控SHOW PROCESSLIST确保没长事务阻塞。5.5 Ubuntu 20.04 下systemctl命令无响应Failed to connect to bus: No such file or directory这个错误通常出现在非登录 shell 环境比如用sudo -i切换到 root 后执行systemctl。原因是systemctl需要连接 D-Bus 系统总线而sudo -i创建的 shell 没有继承会话环境变量。解决方案用sudo systemctl代替sudo -i后的systemctl或者手动设置环境变量export XDG_RUNTIME_DIR/run/user/$(id -u)最佳实践是永远用sudo systemctl不要sudo -i。注意sudo systemctl和sudo -i systemctl的权限上下文完全不同。前者是“以 root 身份执行 systemctl”后者是“切换到 root 用户再执行”后者容易引发环境变量污染。6. 进阶延伸与生产就绪建议从能跑走向稳跑6.1 日志集中管理把 Nginx、PHP、MySQL 日志统一到journalctlUbuntu 的 systemd 把所有服务日志都收集到 journald但默认 Nginx 和 MySQL 日志仍写文件。我们可以改造Nginx编辑/etc/nginx/nginx.conf注释掉error_log和access_log行添加error_log syslog:serverunix:/dev/log;MySQL编辑/etc/mysql/mysql.conf.d/mysqld.cnf在[mysqld]下加log_error /dev/stderrPHP-FPM编辑/etc/php/7.4/fpm/pool.d/www.conf把access.log和slowlog路径改为/dev/stdout和/dev/stderr。重启服务后sudo journalctl -u nginx -u mysql -u php7.4-fpm -f就能一站式查看所有日志用journalctl --since 2 hours ago快速定位问题时段。6.2 性能调优worker 进程数与 PHP-FPM 子进程池的黄金比例Ubuntu 20.04 默认 Nginxworker_processes auto;即等于 CPU 核心数。但对 Web 服务更优解是worker_processes 2;双核机器或worker_processes 4;四核因为过多 worker 会增加上下文切换开销。PHP-FPM 的www.conf里pm dynamic动态模式比 static 更省资源pm.max_children 50最大子进程数按内存算每个 PHP 进程约 20MB50×201GBpm.start_servers 10启动时创建 10 个pm.min_spare_servers 5最少空闲 5 个pm.max_spare_servers 20最多空闲 20 个。调整后sudo systemctl reload php7.4-fpm生效。用ab -n 1000 -c 100 http://localhost/压测观察htop里 CPU 和内存使用率是否平稳。6.3 安全加固用fail2ban自动封禁暴力破解 IPfail2ban是 Linux 下的“智能防火墙”能实时分析日志并封禁恶意 IP。安装sudo apt install fail2ban。配置/etc/fail2ban/jail.local[sshd] enabled true maxretry 3 [nginx-http-auth] enabled true filter nginx-http-auth port http,https logpath /var/log/nginx/error.log maxretry 3重启sudo systemctl restart fail2ban。它会自动监控 Nginx 错误日志当同一 IP 出现 3 次auth failed就用iptables封禁该 IP 10 分钟。用sudo fail2ban-client status nginx-http-auth查看封禁列表。6.4 备份自动化用cronmysqldump实现每日数据库快照创建备份脚本/usr/local/bin/backup-mysql.sh#!/bin/bash DATE$(date %Y%m%d) BACKUP_DIR/backup/mysql mkdir -p $BACKUP_DIR mysqldump -u myapp_user -pStrongPass123! --single-transaction myapp_db | gzip $BACKUP_DIR/myapp_db_$DATE.sql.gz find $BACKUP_DIR -name myapp_db_*.sql.gz -mtime 7 -delete赋予执行权限sudo chmod x /usr/local/bin/backup-mysql.sh。添加定时任务sudo crontab -