看到“LNMP 环境部署”这个标题,如果你以为就是装完 Linux、Nginx、MySQL、PHP 这四个东西再用浏览器访问一下 IP 就能完事,那大概率要踩坑。我见过不少开发者在本地 Windows 上跑得好好的,一到服务器上部署就翻车:80/443 端口没放行、MySQL 密码带了特殊字符把命令行解析爆、PHP 文件被浏览器下载而不是执行……这些问题几乎都和“组件之间是否协同”有关,而不是某个软件单独装没装好。这篇内容适合刚从本地开发过渡到线上部署的开发者,也适合想在自己的云服务器上从零搭一套可靠 Web 环境的读者。我按部署前规划、组件安装、配置联动、调优验证、故障排查、安全备份这条完整链路来讲,把实际踩过的坑和最终落地方案都写出来。所有命令以 Ubuntu 22.04 LTS 为例,Debian 12 及派生发行版基本可以直接照抄。
1. 部署前的整体规划:组件选型与方案取舍
1.1 为什么还要手动部署一遍
在动手之前,先明确一个底层认知:LNMP 不是一套软件,而是一条请求链路。Linux 提供操作系统环境,Nginx 作为入口接收 HTTP 请求,PHP-FPM 负责解析 PHP 代码,MySQL 负责持久化存储。浏览器发起请求后,请求先进 Nginx,Nginx 看到 .php 结尾的 URI,就通过 FastCGI 协议转给 PHP-FPM,PHP-FPM 执行代码时再去连接 MySQL 取数据。这个链路里任何一个环节连接不上,整个网站就会表现为 502、504 或者“数据库连接出错”。
我强烈建议不要一上来就用一键安装包或者控制面板。手动装一遍能让你知道 Nginx 的配置文件在哪、PHP-FPM 的 socket 路径是什么、MySQL 的鉴权方式是什么。出了问题你能自己定位,而不是卸载重装换个环境再来一遍。很多同学觉得自己用面板更快,但面板同样要配置 PHP 版本、数据库密码、站点路径,一旦业务量上来出现性能瓶颈,面板反而变成黑盒。手动部署一次之后,后续的维护成本是明显降低的。
1.2 组件版本选择:稳定优先还是新特性优先
版本选型这件事,直接决定后面三个月你会不会半夜被告警吵醒。我整理了一张当前比较稳的组合表,照着选基本不出大问题:
| 组件 | 推荐版本 | 选型理由 |
|---|---|---|
| Linux | Ubuntu 22.04 LTS / Debian 12 | LTS 支持周期长,包管理成体系,网上资料多 |
| Nginx | 1.24 或 1.26 | mainline 稳定版,功能覆盖绝大多数业务场景 |
| PHP | 8.2 / 8.3 | PHP 8 系列性能比旧版提升明显,主流框架都要求 8.1+ |
| MySQL | 8.0 | 字符集、JSON 支持完善,社区资料最全 |
| MariaDB | 10.11(可选) | 如果团队已经有 MariaDB 运维经验,可以替代 MySQL |
这里有个原则性建议:生产环境不要追新。PHP 8.3 刚发布的时候,插件生态跟不上的情况就出现过不少。框架本身支持没问题,但某些老项目依赖的扩展没有及时适配,升级完直接白屏。版本选择上保守一点,比升级时临时踩雷强得多。另外要注意的是,云主机厂商提供的内核镜像版本和官方源可能略有差异,安装前先确认当前系统的版本号,不要想当然。
1.3 服务器与网络准备:不是所有云主机都开箱即用
把操作系统选成 Ubuntu 22.04 LTS 之后,别急着上去装软件,先花十分钟把基础环境理清楚。我给的准备清单如下:
- 用密钥或者强密码登录服务器,创建一个非 root 的 sudo 用户,日常操作不要用 root。
- 执行
apt update && apt upgrade,把系统包和安全补丁更新到最新。 - 确认防火墙和安全组都放行了 22、80、443 三个端口。云主机通常有两层网络控制,安全组没放行,服务器内部打开端口也没用。
- 如果机器内存小于 2GB,建议先加一块 swap。这个动作很容易被忽略,但在后面编译源码、PHP-FPM 并发升高时,swap 能救命的。
尤其是最后一条,我见过太多小内存机器因为没配 swap,运行一段时间后 MySQL 被 OOM 直接杀死。swap 不用设太大,2G 内存的机器给 2G swap 就够。配完之后用free -h确认一下即可。
2. 安装环节:先把四大组件跑起来
2.1 安装 Nginx:系统源还是源码编译
Nginx 的安装最没有什么争议,Ubuntu 系统源里就有现成的包。执行下面三行:
sudo apt update sudo apt install -y nginx sudo systemctl enable --now nginx装完检查版本:
nginx -v ss -lntp | grep :80如果看到 80 端口在监听,说明安装成功了。此时在浏览器里访问服务器 IP,能看到 Nginx 的默认欢迎页。
有人会问为什么不直接源码编译,原因是:apt 安装的 Nginx 已经包含常用模块,和 systemd 集成得也好,日志轮转全部配好了,对一个普通 Web 应用来说完全够用。源码编译通常是为了集成第三方模块,或者对性能参数做深度定制,但代价是升级麻烦。如果你确实需要定制模块,我建议维护一套自己的编译脚本,别在线上环境临时改,这个坑我踩过,编译出来启动不了,回滚很痛苦。
2.2 安装 MySQL 8.0:初始化这一步最容易翻车
数据库安装的命令也很短:
sudo apt install -y mysql-server sudo systemctl enable --now mysql mysql --version接下来是重点,mysql_secure_installation这一步一定要跑:
sudo mysql_secure_installation它会引导你设置 root 密码、删除匿名用户、禁用 root 远程登录、清理测试库。注意 MySQL 8 默认情况下 root 用户走的是auth_socket插件,也就是说你在命令行里直接sudo mysql才能进去,用你刚才设置的密码反而进不来。这是安全设计,不用觉得奇怪。生产环境也不要拿 root 账户去连业务库。
正确做法是创建专用应用用户:
CREATE DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'myapp_user'@'localhost' IDENTIFIED BY '这里是强密码'; GRANT ALL PRIVILEGES ON myapp.* TO 'myapp_user'@'localhost'; FLUSH PRIVILEGES;这里有个细节,MySQL 8 默认认证插件是caching_sha2_password,PHP 8.1 以上的官方扩展已经能直接连接,所以不用特意改成老旧的mysql_native_password。如果你用的是很老的 PHP 版本,连不上再去处理认证插件兼容问题。
2.3 安装 PHP-FPM 与扩展:不要全装
PHP 部分需要装 FPM 和常用扩展,一条命令搞定:
sudo apt install -y php8.2-fpm php8.2-cli php8.2-mysql php8.2-curl php8.2-gd php8.2-mbstring php8.2-xml php8.2-zip php8.2-intl sudo systemctl enable --now php8.2-fpm php -v扩展这个东西,核心原则是“按需安装,宁缺勿滥”。有的人为了省事把能装的扩展全部装上去,结果 PHP 进程的内存占用直接飙高,还容易出现扩展之间的符号冲突。尤其要注意php-xdebug这种调试扩展,它只属于开发环境,放到线上会明显拖慢性能。如果后面发现缺了某个扩展,动态安装再重启 PHP-FPM 就行,没必要一开始就追求大而全。
3. 配置环节:Nginx 与 PHP-FPM 的“握手”是灵魂
3.1 写一份完整的 server 块
到这里,三大组件都装好了,但默认配置下它们互相之间还不知道对方的存在。Nginx 的默认站点只是展示一个静态欢迎页,它不会处理 PHP。我们需要新建一个站点配置文件,放在/etc/nginx/sites-available/下,文件名用项目名区分,例如myapp:
server { listen 80; server_name your_domain.com; root /var/www/myapp/public; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_pass unix:/run/php/php8.2-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } location ~ /\.ht { deny all; } }写完以后要启用这个站点,Ubuntu 的机制是把文件软链到sites-enabled:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp sudo nginx -t sudo systemctl reload nginxnginx -t是检查配置语法,出现syntax is ok再执行 reload。reload 比 restart 友好,不会中断现有连接。如果改了 PHP-FPM 的配置,才需要重启 PHP-FPM。
3.2 FastCGI 参数为什么这么重要
上面那个配置里,最关键的是fastcgi_pass和SCRIPT_FILENAME这两行。
fastcgi_pass有两种写法:一种是 Unix socket,像unix:/run/php/php8.2-fpm.sock;另一种是 TCP 端口,像127.0.0.1:9000。同一台机器上部署,我强烈建议用 socket,少一次 TCP 握手开销,性能更好。如果架构是 PHP-FPM 和 Nginx 不在同一台机器,才需要改成 TCP 方式,但这时要考虑内网联通和权限控制,复杂度会高不少。
SCRIPT_FILENAME是 Nginx 告诉 PHP-FPM“你要执行哪个文件”。少了这一行,就会出现访问 .php 文件返回空白或者直接下载的情况。很多部署教学贴里默认使用的fastcgi_params文件并没有包含这个参数,所以你需要在 location 里显式加上。$document_root和$fastcgi_script_name拼接出来的就是服务器上的绝对路径,这两个变量在 root 和 alias 混用时容易出错,建议保持简单的目录结构,不要给自己挖坑。
3.3 目录权限与运行用户的匹配
权限配置是另一个容易翻车的地方。在 Ubuntu 上,Nginx 和 PHP-FPM 默认运行用户都是www-data,三者应该保持一致,否则就会出现 Nginx 有权限读文件,但 PHP-FPM 没有的情况。
站点根目录的常规权限是:
目录:755 文件:644 所有者:root 或 www-data 都行,关键是可写目录要给 www-data需要让应用写入的目录,比如storage、cache、upload,要单独授权:
sudo mkdir -p /var/www/myapp/storage sudo chown -R www-data:www-data /var/www/myapp/storage这里我特别强调一句:不要图省事对整个项目目录执行chmod -R 777。这种权限配置在出问题的时候会掩盖真正的问题,而且一旦网站被上传了 webshell,整个项目目录可写等于把服务器大门敞开了。权限细致一点,后面省心不止一点。
4. 调优与验证:从“能跑”到“跑得稳”
4.1 PHP-FPM 进程数怎么估算
环境搭完,访问流程通了,但这只是“能跑”。我认为你必须理解 PHP-FPM 的进程管理方式,才能把环境调到“稳”。打开/etc/php/8.2/fpm/pool.d/www.conf,核心参数是这几项:
pm = dynamic pm.max_children = 24 pm.start_servers = 5 pm.min_spare_servers = 3 pm.max_spare_servers = 8pm.max_children决定 PHP-FPM 最多同时跑多少个进程,它的估算公式是:
max_children = ( 服务器总内存 - 系统预留内存 - MySQL 预留内存 ) ÷ 单个 PHP 进程平均内存举个例子,一台 4G 内存的云主机,系统加 MySQL 预留 1.5G,剩下 2.5G 给 PHP。如果观察到单个 PHP 进程平均占用 60M,那么理论值是 2500 / 60 ≈ 41,保守一点取 32 就好。很多教程会让你直接写 128 甚至 256,但小内存机器上这么配,并发一上来内存立刻被打满,进程被 OOM 杀掉,比不配还惨。
用下面的命令可以看实际内存占用:
ps aux | grep php-fpm | awk '{print $6/1024 " MB"}'业务跑起来之后,你可能会发现单个进程的内存占用比预想的高不少,这时再回来调整max_children,公式只是起点,不是终点。
4.2 MySQL 基础调优:内存和慢查询
MySQL 安装完默认配置是偏保守的,能跑但性能一般。基础调优主要动两个地方,打开/etc/mysql/mysql.conf.d/mysqld.cnf:
[mysqld] innodb_buffer_pool_size = 512M slow_query_log = 1 long_query_time = 2 max_connections = 200innodb_buffer_pool_size是 InnoDB 最重要的内存参数,它决定了表数据和索引的缓存大小。给 MySQL 分配多少内存一定要看整机资源,4G 的机器给 512M 到 1G 是合理的,千万不要盲目给到能用的最大值。很多教程上来就写“这个参数占物理内存的 70%”,但那是针对专用数据库服务器说的,Web 后端这么配会让系统内存吃紧,最终连 SSH 都登不上。
slow_query_log和long_query_time是慢查询日志,这个配合业务开发非常有用。刚开始就开着,定位性能问题的时候就不用临时改配置了。改完 MySQL 配置要sudo systemctl restart mysql才会生效。
4.3 Nginx 基础优化与整体验证
Nginx 的主配置/etc/nginx/nginx.conf里,有四个参数值得关注:
worker_processes auto; worker_connections 1024; keepalive_timeout 65; server_tokens off;worker_processes auto表示按 CPU 核心数自动分配,不用手动指定。worker_connections是每个 worker 进程能同时处理的连接数,1024 是个稳妥起点。server_tokens off可以隐藏 Nginx 版本号,避免被扫描到特定版本漏洞后针对性攻击。
全部改完后,做一次整体验证。创建一个 PHP 探针文件:
echo "<?php phpinfo(); " > /var/www/myapp/public/phpinfo.php浏览器访问http://your_domain.com/phpinfo.php,能看到 PHP 配置信息说明全链路通了。然后立刻删掉这个文件:
rm /var/www/myapp/public/phpinfo.php这个文件是安全大忌,它会把服务器上所有环境信息完整暴露给访问者,验证完绝对不能留。
5. 常见故障排查:一个真实场景的避坑记录
5.1 502 Bad Gateway:八成是 socket 通讯问题
502 是线上出现频率最高的错误,表象是 Nginx 已经把请求转给 PHP-FPM,但 PHP-FPM 没有正常回话。排查顺序如下:
systemctl status php8.2-fpm ls -l /run/php/php8.2-fpm.sock sudo tail -f /var/log/nginx/error.log最常见的原因是 PHP-FPM 没启动,或者 socket 文件路径和 Nginx 配置里的fastcgi_pass不一致。改过 PHP-FPM 监听路径后忘记同步改 Nginx,一定会 502。另外还有一种情况:socket 文件的属主不是 Nginx 运行用户,导致Permission denied。总之,一是路径一致,二是权限一致,这两点确认完,502 基本解决。
5.2 404 与 403:文件没进来还是权限不对
404 和 403 经常容易被混在一起查,其实原因完全不同。
404 大概率是root路径写错了,或者 Nginx 找不到入口文件。比如项目代码放到/var/www/myapp/public,但配置里写的是/var/www/myapp,Nginx 去不存在的地方找文件,自然 404。还有一个常见原因是没有配try_files,访问/user/1这种伪静态路由时 Nginx 不知道交给前端控制器,直接返回找不到资源。
403 则一般是权限问题,服务器能定位到文件,但进程没有读权限。先检查目录是不是 755、文件是不是 644,再看运行用户是不是www-data。我曾经排查过一个 403,到最后发现是项目目录的父亲目录权限是 700,导致系统用户根本进不去,问题不在项目本身。排查权限时要从根目录一层层往下看。
5.3 504 Gateway Timeout:慢接口的超时设置
如果要执行一个耗时较长的操作,比如导出数据、生成报表,很容易遇到 504。这个问题的本质是超时时间设置太短。Nginx 默认的fastcgi_read_timeout是 60 秒,PHP 默认的max_execution_time是 30 秒,两个参数任何一个先到,请求都会被中断。
我的处理思路是:先排查业务本身是否有问题,比如数据库查询是不是没有索引导致全表扫描。确认业务正常后,再把 Nginx 的fastcgi_read_timeout调到 120 秒,PHP 的max_execution_time调到 60 秒。注意max_execution_time不要为了方便设成 0,那会让 PHP 脚本无限执行,一个死循环直接拖垮 FPM 进程池。
5.4 “连不上数据库”的经典排查
数据库连接报错是另一大类高频问题,最典型的报错是:
SQLSTATE[HY000] [1045] Access denied for user遇到这个报错,第一反应不是“密码错了”,而是先看用户授权的主机范围。MySQL 的'myapp_user'@'localhost'和'myapp_user'@'127.0.0.1'是两个完全不同条目。如果应用配置文件里用的是127.0.0.1连接,但 MySQL 只授权了localhost,那么必然被拒绝。处理办法是重新创建用户,把 host 设置成你实际使用的连接来源,或者干脆授%,但要配合防火墙和访问控制。
还有一个隐蔽问题:PHP 的 MySQL 连接中,host 填localhost会走 Unix socket,填127.0.0.1会走 TCP。如果 MySQL 没有监听 TCP 端口,后者也会失败。这类问题日志里不会直接写原因,排查的时候要有一个全局视角,把 host、端口、socket、账号权限串起来看。
5.5 一个综合实战场景:凌晨的 502 告警
有一次我接到凌晨的 502 告警,第一反应是看 Nginx 错误日志,里面出现的是:
connect() to unix:/run/php/php8.2-fpm.sock failed (11: Resource temporarily unavailable)这句英文说明 socket 文件还在,PHP-FPM 也没停,但 Nginx 暂时连不上,本质是 FPM 的进程处理能力被打满了。我登录服务器后先看ps aux | grep php-fpm,发现进程数正好卡在设好的max_children上限,再查内存还有一定余量,于是把max_children从 24 调到 32,并同步确认了pm.start_servers和pm.max_spare_servers,reload 之后恢复。这个场景说明一个问题:监控系统只能告诉你出了什么错,但真正定位还是得靠对 LNMP 链路结构的理解。
这类问题还有一个隐藏点:如果改了 php-fpm 的www.conf却没有重启服务,旧的配置是不会生效的。Nginx 配置改动用 reload 就行,但 PHP-FPM 的 pool 配置修改必须systemctl restart php8.2-fpm。很多同学改完配置疯狂 reload Nginx,问题一点没变,就是因为漏了这一步。
6. 部署后的安全收尾与备份习惯
6.1 最小权限:端口、用户、隐藏版本
LNMP 环境跑起来之后,接下来就是把安全底线守住。我按重要性从高到低列几个必做项:
第一,防火墙和安全组只放行必要端口。SSH 的 22、HTTP 的 80、HTTPS 的 443,这三个是常规需要。MySQL 的 3306 千万不要直接暴露到公网,如果确实需要远程访问数据库,优先考虑 SSH 隧道或者跳板机,不要图方便在安全组里开 3306。
第二,确认 MySQL 监听在 127.0.0.1。编辑 MySQL 配置里的bind-address,保持默认的127.0.0.1即可,这样即使服务器有多个网卡,外部也连不进来。
第三,隐藏 Nginx 版本号,也就是前面提到的server_tokens off。这个动作虽然简单,但能减少被扫描工具识别出具体版本的概率。
第四,检查 PHP 配置里的危险函数。生产环境可以考虑在php.ini里禁用exec、shell_exec、system等可以直接执行系统命令的函数。不过要注意,有些业务框架的正规功能也会用到这些函数,禁用前先确认没有依赖。
6.2 备份与恢复:救命脚本一定要做恢复演练
部署完环境,备份策略是最容易被拖延的任务,但这恰恰是线上事故后的最后一道防线。我常用的 MySQL 备份脚本如下:
#!/bin/bash BACKUP_DIR="/data/backup" MYSQL_CMD="mysqldump -umyapp_user -p'PASSWORD' --single-transaction myapp" mkdir -p "$BACKUP_DIR" $MYSQL_CMD | gzip > "$BACKUP_DIR/myapp_$(date +%Y%m%d).sql.gz" find "$BACKUP_DIR" -type f -name "myapp_*.sql.gz" -mtime +7 -delete脚本里注明了密码,所以文件权限要收紧:
sudo chmod 700 /usr/local/bin/backup_task.sh然后用 cron 定时执行:
30 2 * * * /usr/local/bin/backup_task.sh >> /data/backup/backup.log 2>&1--single-transaction这个参数对 InnoDB 表非常重要,它可以在不锁表的情况下做一致性备份,避免备份过程中业务写入产生脏数据。文件备份的话,用 tar 排除缓存目录,比如:
tar -czf /data/backup/myapp_files_$(date +%Y%m%d).tar.gz --exclude='*/storage/cache' /var/www/myapp最后,备份一定要定期做恢复演练。我见过有人备份文件天天生成,真到恢复的时候才发现解压后文件损坏,或者备份脚本里数据库名写错,备份了一个根本不存在的数据集。每个月把备份恢复到一台测试机器上,确认应用能正常启动、数据能查到,这个流程才算完整。
6.3 最后一次配置所有链路后的经验清单
所有配置都改完之后,不要急着把服务器交付出去。我习惯做一轮冷静期检查:清空浏览器缓存,重新打开页面,看一眼 Nginx 的 access.log 和 error.log 新增的几条记录,再以普通业务身份走一遍全流程,包括注册、发帖、数据查询,确保 MySQL 用户和表能正常读写。很多环境问题不是在配置时爆发的,而是在第一次真实业务操作时冒出来的。
最后分享一个我自己踩出来的原则:LNMP 部署没多少玄学,绝大多数问题都出在 socket 路径、权限和防火墙三个地方。把这三个点提前盯住,后面能省下大把排查时间。你用一键脚本装的也行,但至少要能回答出这三个问题——PHP-FPM 的 socket 文件在哪、Nginx 用哪个用户跑、MySQL 的端口有没有暴露到公网。能答上来,这个环境才算真正属于你。