部署Linux服务器这件事,凡是搞过几年运维的人,大概都会遇到一个绕不过去的坎:搭LNMP环境(Linux + Nginx + MySQL + PHP)。说它是入门必修也好,说是生产环境标配也罢,这东西的门道其实比网上一堆一键脚本能告诉你的要多得多。今天我把自己这些年踩过的坑、摸索出来的流程完整写一遍,从零开始把一个可用的LNMP环境手工搭起来,每一步都讲清楚为什么这么干,适合刚接触Linux部署的新手照着做,也对已经会敲命令但没理清原理的同行有点参考价值。
1. 项目概述与部署思路
1.1 为什么LNMP是Web服务的经典组合
LNMP这四个字母分别代表:Linux操作系统、Nginx Web服务器、MySQL数据库、PHP动态脚本语言。这套组合之所以成为Web服务的事实标准,核心在于它把静态文件处理、动态请求解析、数据存储三件事分得清清楚楚,各有各的专长。
Nginx处理静态文件和高并发连接的效率极高,一个进程能扛住成千上万的并发连接,但它在处理动态逻辑方面天生弱。而PHP-FPM正好负责把PHP代码解析成可执行逻辑,处理完再交回Nginx返回给浏览器。MySQL则管数据的持久化存储和查询。三者各干各的强项,组合起来就是一套性能、稳定性、可维护性都很均衡的Web服务方案。
我见过不少刚开始学习部署的新手,上来就敲yum install nginx mysql php,然后又装各种插件,最后发现PHP页面打不开、数据库连不上,整个环境乱成一锅粥。原因很简单:不知道怎么把Nginx请求转给PHP-FPM,也不知道PHP-FPM怎么去连MySQL。这三者之间是通过明确的协议和配置串联的,理解这种分工和串联关系,比记住命令本身重要得多。
1.2 部署方案选型与版本规划
LNMP的部署方式主要有三种:操作系统包管理直接装、用宝塔等面板工具一键装、手工下载源码编译安装。三种方式各有适用场景。
用yum/dnf直接安装最省事,缺点是软件版本通常比较老,比如系统源里的PHP可能停留在7.x甚至更早,而且Nginx官方源和系统源混在一起容易冲突。面板工具一键装速度最快,但会把很多你不需要的组件一并装上,后期做精细化调优和故障排查时反而碍事。源码编译虽然麻烦,但胜在可控——每个模块、每个参数都能自己定制,版本也能选到官方最新稳定版,而且编译安装的路径清晰,出问题的时候知道去翻哪个目录。
我的做法是混合策略:Nginx和PHP用源码编译安装(版本可控,性能参数可定制),MySQL用官方仓库安装(MySQL编译极其耗时耗资源,仓库安装完全够用)。这样既保证关键组件版本新、配置灵活,又省去了编译MySQL那两三小时的折腾。采用的版本是Nginx 1.26稳定版、PHP 8.2、MySQL 8.0,这套组合目前无论是社区支持还是生产应用都很成熟。
1.3 部署前的整体规划
动手之前先规划好目标,避免中途反复改。
本次部署的环境规划如下:操作系统为Rocky Linux 9(如果用的是CentOS Stream 9或RHEL 9,方法和命令基本一致),如果手里只有CentOS 7的旧机器,建议先升级系统或者换台新系统,PHP 8.2和MySQL 8.0在旧系统上的兼容性问题比较多。业务目录统一放在/var/www下,源码包下载到/usr/local/src,组件安装到/usr/local下各自独立目录。运行用户统一用www,权限管理简单清晰,避免后面PHP-FPM和Nginx因为权限问题互相别扭。
网络方面,服务器IP按实际规划填写,需要提前放行80端口(HTTP)和443端口(HTTPS),这是对外提供Web服务的基础。后面每装完一个组件就单独验证一次,而不是全部装完再一起调,这样排查问题范围小得多。
2. 环境准备与基础配置
2.1 系统更新与基础依赖安装
新服务器到手,第一件事永远是更新系统软件包。这不仅仅是为了拿到安全补丁,更重要的是让系统内建的软件包索引和依赖关系和当前官方仓库保持同步,后面装任何东西都会顺畅很多。
dnf update -y dnf install -y vim wget tar gcc gcc-c++ make pcre-devel zlib-devel openssl-devel libxml2-devel libxslt-devel gd-devel这些依赖看着多,其实每个都有明确用途。gcc和gcc-c++是编译源码的编译器,没有它整个部署直接停摆。pcre-devel是Nginx支持正则表达式所必需的库,做URL重写(rewrite)功能时必须用到。openssl-devel提供SSL相关的头文件,给Nginx编译HTTPS模块(http_ssl_module)时要用。zlib-devel是Nginx响应内容Gzip压缩的基础库。后面编译PHP时需要的libxml2-devel和gd-devel也在这里一起装好,省得编译PHP到一半报缺依赖再回头补装。
注意:编译安装的最大成本就是依赖。务必用
dnf check-update确认这些基础包都装好了再继续,缺一个包后面编译报错时排查起来比较耗时。
2.2 创建专用运行用户和目录结构
Nginx和PHP-FPM都以root身份启动,但真正干活时建议降权到普通用户,这是一条基本的安全准则。如果以root身份运行PHP,一旦代码有漏洞被利用,后果就是整个服务器被接管。
groupadd www useradd -g www -s /sbin/nologin www mkdir -p /var/www/default mkdir -p /var/log/nginx mkdir -p /data/backup chown -R www:www /var/www这里给www用户指定的shell是/sbin/nologin,表示该用户无法直接登录系统,只能作为服务运行账户存在,这是运维上的标准做法。以后所有的Web项目都放到/var/www下面,维护方便,备份也只要盯这一个目录。
2.3 防火墙与系统安全策略
系统默认的firewalld会拦截未放行的端口,很多新手在这里栽跟头:服务本身明明启动正常,外部访问却死活不通。先把80和443端口放行,同时检查SELinux策略。
firewall-cmd --permanent --add-service=http firewall-cmd --permanent --add-service=https firewall-cmd --reloadSELinux默认是 enforcing 模式,会限制Nginx访问非标准目录、PHP-FPM读写文件等行为。生产环境按规范建议写好SELinux策略,但对个人学习环境或初期上线的服务器,把SELinux临时放宽松(setenforce 0)或者直接修改/etc/selinux/config设置为disabled能省去大量排查时间。我见过太多因为SELinux拦截导致Nginx返回403的例子,排查到最后才发现不是配置问题而是SELinux在中间捣乱。
配置完建议把系统快照或镜像存一下,后面任何一步折腾坏了都能快速恢复。这一步看着不起眼,实际节省的时间成本不可估量。
3. Nginx部署与核心配置
3.1 源码编译安装Nginx
去Nginx官网下载区找到稳定版源码包,目前推荐1.26.x系列。下载后解压进入源码目录直接编译。
cd /usr/local/src wget https://nginx.org/download/nginx-1.26.2.tar.gz tar zxf nginx-1.26.2.tar.gz cd nginx-1.26.2 ./configure --prefix=/usr/local/nginx \ --user=www --group=www \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_realip_module \ --with-http_stub_status_module \ --with-pcre make && make install编译参数里,--user=www --group=www指定Nginx的子进程以www身份运行。http_ssl_module是HTTPS支持,http_v2_module是HTTP/2加速支持,现在新部署的站点几乎都要求HTTPS,这两个模块必须提前编进去。http_stub_status_module提供/nginx_status监控页,后面排查Nginx连接数时要靠它。http_realip_module用于在Nginx后面再接一层负载均衡或CDN时获取真实客户端IP,一次性配全免得以后升级二进制。
编译过程中如果报错提示缺某个头文件,基本就是前面dnf install时漏了对应的-devel包,按报错信息去补装再重新configure即可。
3.2 核心配置与虚拟主机
安装完成后,Nginx主配置文件在/usr/local/nginx/conf/nginx.conf。不要急着堆配置,先把主配置文件里的关键参数理解清楚。最核心的是worker_processes,官方建议设为CPU核心数——可以先用nproc查出核数再填,比如8核就填8,这样每个worker进程都能跑满一个核心。
events块里的worker_connections是老生常谈的参数,它决定每个worker能同时处理的连接数,配合worker_processes就决定整个Nginx的并发上限。默认1024偏小,我一般调到4096或者更高,前提是服务器内存够用。
虚拟主机配置建议独立放在conf/conf.d/目录里,主配置里用include conf.d/*.conf;引入,这样做的好处是每个站点独立成文件,互不影响,维护时无需动主配置。以下是一份最精简的PHP虚拟主机配置样板:
server { listen 80; server_name localhost; root /var/www/default; index index.php index.html; access_log /var/log/nginx/default.access.log; error_log /var/log/nginx/default.error.log; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass unix:/run/php-fpm/www.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }这段配置里的关键点在于location ~ \.php$块。当浏览器请求一个以.php结尾的URL时,Nginx会把请求转发给unix:/run/php-fpm/www.sock这个Unix socket,也就是后文将要部署的PHP-FPM监听的地址。fastcgi_param SCRIPT_FILENAME这行指定了PHP脚本在服务器上的实际路径,$document_root对应root配置项,$fastcgi_script_name对应请求的URL路径。这两者拼接起来,PHP-FPM才能知道去哪个目录下找哪个文件来执行。很多PHP文件能访问但执行不了,都是因为这一行路径写错了。
3.3 启动验证与开机自启
编译安装的Nginx不在systemd的服务列表里,需要手动写一个服务文件,否则服务器重启后Nginx不会自动拉起。
/usr/local/nginx/sbin/nginx -t /usr/local/nginx/sbin/nginx-t会先检查配置语法是否正确,输出test is successful再放心启动。然后注册成服务,实现开机自启:
cat > /usr/lib/systemd/system/nginx.service <<'EOF' [Unit] Description=nginx - high performance web server After=network.target [Service] Type=forking ExecStart=/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf ExecReload=/usr/local/nginx/sbin/nginx -s reload ExecStop=/usr/local/nginx/sbin/nginx -s quit PrivateTmp=true [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable --now nginx systemctl status nginx这里有个坑:因为Nginx的master进程启动后会fork出worker进程,所以服务文件的Type必须设成forking,才会正确判断服务是否启动成功。如果设成simple,systemd会一直停留在启动状态或者误判失败。验证完systemctl status,再用curl -I http://localhost看返回头,能拿到200或者403都是正常的(403说明还没放置任意默认页面),只要不是connection refused就说明Nginx已经在正常监听了。
4. MySQL部署与初始化
4.1 官方仓库安装MySQL 8.0
MySQL的编译安装极其耗时,性价比不高。用MySQL官方提供的Yum仓库来安装,既稳定又省心。先添加官方源再安装:
dnf install -y https://dev.mysql.com/get/mysql80-community-release-el9-1.noarch.rpm dnf install -y mysql-community-server安装后启动数据库并检查初始状态:
systemctl start mysqld systemctl enable --now mysqld grep 'temporary password' /var/log/mysqld.logMySQL 8.0首次启动时会自动生成一个临时root密码,日志里有明确记录。这个密码必须以引号完整复制出来,因为里面往往带着各种特殊字符,手动输极易漏掉。有了临时密码后,第一件事永远是执行安全初始化脚本:
mysql_secure_installation这个脚本会一步步引导修改root密码、删除匿名用户、禁止root远程登录、清理测试库。安全策略默认要求密码包含大小写字母、数字和特殊字符,长度至少8位。我见过有人急着把密码设得特别简单,结果被MySQL的密码验证插件直接拒绝,这不是配置问题,是策略限制,要么设一个符合规则的强密码,要么在配置文件里调低validate_password策略,建议前者,毕竟这是数据库服务器的第一道防线。
4.2 创建业务数据库与专用账号
数据库跑起来之后,不建议直接用root跑业务,因为root权限过大,一旦业务侧代码有SQL注入漏洞,攻击者可以直接拖库或者删库。标准做法是给业务单独建一个库、单独建一个账号,权限只给该库范围。
CREATE DATABASE IF NOT EXISTS appdb DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'YourStrongPassword@2026'; CREATE USER 'appuser'@'127.0.0.1' IDENTIFIED BY 'YourStrongPassword@2026'; GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'localhost'; GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'127.0.0.1'; FLUSH PRIVILEGES;注意这里我反复给localhost和127.0.0.1两个主机都创建了账号。实际生产经验是:PHP-FPM连接数据库时,有时解析host会走到localhost,有时会走到127.0.0.1,如果只授权了其中一个,就会遇到本地程序连接数据库被拒绝、其他主机却能正常访问的怪象。两个都授权,能省一个隐形坑。utf8mb4字符集一定要选它,不是utf8。utf8在MySQL里最多存3字节,碰到emoji等生僻符号会报错或乱码,utf8mb4才是完整的Unicode支持方案。
数据库监听地址默认绑定在127.0.0.1,这是最安全的配置。如果业务只需要本机PHP连接,保持默认不要改。只有明确需要从其他机器连数据库的时候,才去修改/etc/my.cnf里的bind-address=0.0.0.0,同时还要配合防火墙放行3306端口——这一步非常容易被忽略,改完配置起服务发现连不上,十有八九是防火墙拦了3306。另外MySQL 8.0默认的认证插件是caching_sha2_password,旧版的PHP扩展(比如5.x时代的mysqlnd)可能连不上,好在PHP 8.2自带的mysqlnd驱动已经完全兼容,这个坑基本不用再关心。
4.3 连接性能验证
数据库和账号都就绪后,先用命令行客户端做一次真实连接测试,排除账号密码和权限问题:
mysql -uappuser -p'YourStrongPassword@2026' -h 127.0.0.1 -e "SHOW DATABASES;"能正常列出appdb和信息库说明账号没问题。这里有个小习惯:测试连接时用-h 127.0.0.1而不是-h localhost,因为后者走的是Unix socket连接,前者才是真正的TCP连接。PHP-FPM连MySQL时走的是TCP,所以这一步测试才能代表线上真实链路。
5. PHP解析环境部署与联动配置
5.1 PHP源码编译与扩展选择
PHP是整个LNMP环境里配置最繁琐、参数最多的一环。下载PHP 8.2或8.3的源码包后,进入源码目录执行configure。以下是一组经过验证的编译参数,兼顾常用功能与性能:
cd /usr/local/src wget https://www.php.net/distributions/php-8.2.20.tar.gz tar zxf php-8.2.20.tar.gz cd php-8.2.20 ./configure --prefix=/usr/local/php \ --with-config-file-path=/usr/local/php/etc \ --with-fpm-user=www --with-fpm-group=www \ --enable-fpm \ --enable-mysqlnd \ --with-mysqli=mysqlnd \ --with-pdo-mysql=mysqlnd \ --enable-mbstring \ --with-zlib \ --with-openssl \ --enable-opcache \ --enable-gd这些参数里,--with-config-file-path指定php.ini的存放位置,这个路径务必记好,后面启动PHP-FPM时如果提示找不到配置文件,就要去这个目录下操作。--with-mysqli=mysqlnd和--with-pdo-mysql=mysqlnd两种MySQL连接方式都启用,因为现在主流框架有的用PDO、有的用mysqli,都支持才能应对不同项目。--enable-fpm是必须的,它是PHP-FPM进程管理的开关。--enable-opcache对应PHP的字节码缓存,官方Zend OPcache模块,开启后PHP脚本不需要每次请求都重新编译一遍,性能提升不是百分之几的级别,而是成倍的级别,生产环境必须开。
编译完执行make -j$(nproc),用多核并行编译能大幅缩短时间,大概5-10分钟就能完成。然后make install安装到/usr/local/php目录。
5.2 PHP-FPM服务配置
安装完成后,PHP源码包里带着一份官方推荐配置样例,把它拷贝为真正的配置文件:
cp /usr/local/php/etc/php-fpm.conf.default /usr/local/php/etc/php-fpm.conf cp /usr/local/php/etc/php-fpm.d/www.conf.default /usr/local/php/etc/php-fpm.d/www.conf cp /usr/local/src/php-8.2.20/php.ini-development /usr/local/php/etc/php.ini生产环境建议拷贝php.ini-production,它会自动关掉开发环境的暴露信息。如果贪图方便用development版本,记得把display_errors改成Off——线上开着错误显示等于把服务器目录结构、数据库报错全裸奔给攻击者看。
www.conf里要重点检查listen配置:
listen = /run/php-fpm/www.sock listen.owner = www listen.group = www listen.mode = 0660 user = www group = www注意我写在Nginx配置里的fastcgi_pass是unix:/run/php-fpm/www.sock,这里PHP-FPM的listen必须完全一致,否则Nginx找不到socket文件,返回502 Bad Gateway。listen.owner和listen.mode是很多新手忽略的坑——socket文件默认归root所有,权限不够时Nginx的www用户无法通过socket与PHP-FPM通信,会出现PHP文件下载而不是执行的诡异现象。这里显式指定owner和mode,直接避免这个权限问题。
5.3 PHP-FPM与MySQL、Nginx的联动验证
PHP-FPM也写一个systemd服务,跟之前Nginx的处理方式一致,这里不再重复贴文件内容,核心是Type=forking,ExecStart指向/usr/local/php/sbin/php-fpm即可。
启动后必须验证三件事:
第一,PHP-FPM进程是否正常监听socket:
ss -lunx | grep php-fpm第二,在网站根目录创建一个探针文件:
<?php phpinfo(); ?>浏览器访问http://服务器IP/info.php,如果页面正常显示出PHP版本、配置项、扩展列表,说明Nginx到PHP-FPM这条链路已经通了。如果显示源码而不是渲染后的页面,说明fastcgi_pass或SCRIPT_FILENAME路径配置有问题。
第三,验证PHP到MySQL的连通性,这是LNMP三个组件之间最后的一环:
<?php $conn = new mysqli('127.0.0.1', 'appuser', 'YourStrongPassword@2026', 'appdb'); if ($conn->connect_error) { die('连接失败: ' . $conn->connect_error); } echo '数据库连接成功'; $conn->close(); ?>三环验证全过,整个LNMP环境就算真正部署完成了。
6. 常见问题排查与实践记录
6.1 部署验证测试
完整部署完成后的第一件事,不是急着上线业务,而是做一轮全面的功能验证。我会依次检查:Nginx是否开机自启、PHP-FPM是否开机自启、MySQL是否开机自启;HTTP端口外网是否可访问、PHP探针页是否正常渲染;数据库连接是否正常、读写权限是否受限;系统资源使用情况有没有异常偏高。每一项确认无误后,再做一次模拟重启的测试——直接把服务器重启,看所有服务是否都自动拉起来。这一步千万不要省,我踩过不只一次systemctl enable后仍然没有自启的坑,多半是服务文件里Type类型写错,或者依赖了未启动的网络服务。
全部验证通过后,建议给这套部署过程留一份简单的手工操作笔记沉淀下来。记录的要点包括:安装的版本号、重要的配置项及其含义、各种常用路径、默认账号密码清单。下次再部署或迁移时,这份笔记能帮你省掉大量回忆的时间。
6.2 高频故障排查速查表
LNMP环境日常运维中,有些故障出现频率极高,很多都是同一个原因导致的。下面这份速查表来自实际运维中重复遇到最多的问题。
| 现象 | 可能原因 | 快速排查命令 |
|---|---|---|
| 页面打不开,浏览器报502 | PHP-FPM未启动或socket路径不匹配 | systemctl status php-fpm,ss -lunx grep php-fpm |
| 访问.php文件直接下载 | fastcgi配置缺失或Nginx未加载PHP模块 | nginx -t,检查server块里是否有location ~ \.php$ |
| MySQL连接报Access denied | 账号密码错误或授权host不匹配 | 用命令行客户端实际测试连接,检查GRANT授权范围 |
| 外部访问不通但本机能访问 | 防火墙未放行该端口 | firewall-cmd --list-all,检查80/3306端口 |
| Nginx日志显示Permission denied | 目录权限不足或SELinux拦截 | ls -ld /var/www,getenforce,必要时chown -R www:www |
| PHP页面能开但执行出错 | PHP缺少必要扩展或display_errors被关 | php -m查看模块列表,临时打开display_errors看报错 |
| 数据库操作报utf8mb4相关错误 | 字符集配置不一致 | 检查库表字符集及PHP连接时charset设置 |
| 504 Gateway Timeout | PHP-FPM处理超时,请求积压 | 调大request_terminate_timeout或优化PHP代码 |
有个在实际操作中很容易忽略的地方:php-fpm的pm.max_children参数如果设小了,一旦并发请求上来,PHP-FPM进程池被占满,Nginx这边就会大量报502。这个时候第一反应别想着改Nginx配置,先去/usr/local/php/var/log/php-fpm.log看进程池日志,确认是不是这个参数的问题。这个错误在系统资源充足但压测报错时极难排查。
6.3 性能与安全的基线建议
部署完成只是起点,LNMP上线前建议再花一点时间做性能和安全的基线调整。
性能方面,Nginx的worker_processes按CPU核数设置,worker_connections按需调高到4096。PHP开启OPcache后,在php.ini里设置opcache.memory_consumption=128,opcache.max_accelerated_files=4000,opcache.validate_timestamps=Off(仅生产环境可关闭,开发环境保持On便于改动即时生效)。MySQL的innodb_buffer_pool_size设为物理内存的60%-70%,这是数据库性能影响最大的一个参数,默认128M对8G内存的机器远远不够,但也不要盲目调大,给操作系统和PHP留够内存。
安全方面,PHP探针文件在验收后务必立即删除。这个文件一旦留在线上暴露出去,服务器版本、配置信息全部一览无余。MySQL的3306端口尽量保持仅本机监听。定期用dnf update更新系统补丁,同时设置日志切割——Nginx的access_log会随时间越滚越大,不处理会把磁盘撑爆,可以用logrotate来做日志归档,Linux系统自带该工具,配置一下/etc/logrotate.d/nginx即可。
我个人的经验是,部署LNMP这件事本质上不是Linux命令的堆砌,而是三件事的贯穿:知道每个组件负责什么,知道它们之间用什么方式通信,知道出问题后去哪看日志、用什么命令定位。这三个维度想清楚了,就基本可以应对绝大多数线上环境了。
最后分享一个我觉得特别好用的小习惯:每次部署完成,我都会用history导出这次的完整命令行记录,存成一份markdown笔记放在本地。格式大概是“哪一步用了什么命令、为什么要这么写、出了什么错、怎么解决的”。后来做服务器巡检和迁移时,翻这个笔记比翻任何官方文档都顺手——这大概就是运维工作最有沉淀感的地方了。