
简介这份PDF文档面向需要从零构建Web运行环境的运维人员、后端开发者与计算机专业学生系统梳理Linux、Nginx、MySQL、PHP四件套的源码编译式搭建流程。资源包内仅含1个PDF文件体积约3.22MB以图文排版呈现命令、配置参数与执行截图便于在单机或虚拟机上边看边做。内容从LNMP定义切入说明Nginx直接处理静态请求、动态请求经FastCGI交由php-fpm再访问MySQL的完整链路并给出rhel7.6系统与nginx-1.18.0、mysql-boost-5.7.30、php-7.4.5的参考版本。搭建部分覆盖依赖安装、源码解压、./configure与cmake参数配置、make编译安装、隐藏Nginx版本号、关闭debug缩减编译量、修改nginx.conf开启PHP模块等环节还记录了MySQL预编译缺boost、ncurses-devel、gcc-c、bison时的报错与清理缓存重试的排错过程。目前已有1812人学习适合希望摆脱一键脚本、理解编译细节与配置文件含义的读者参考。1. 一台 2C4G 的机器上为什么最后都收敛到 LNMP接手一台 2C4G 的机器域名解析好了代码也推上去了接下来要装的东西通常就是四样Linux 做底座Nginx 顶在最前面接连接MySQL 存数据PHP 跑业务逻辑。这套组合叫 LNMP 环境搭建听起来像四步安装真做起来坑几乎不在安装本身而在四者之间的进程模型、权限边界和超时链路对不对得上。Nginx 是事件驱动的多进程模型PHP-FPM 是独立进程池MySQL 有自己的连接数上限任何一层参数比另一层小压力上来就表现为 502 或 504而不是日志里清楚地写着哪一层挂了。这套方案适合手上真有 PHP 业务要上线、又不打算用面板托管的人也适合需要在国产化机器上做离线部署、或者想把 nginx 配置和 mysql 架构串起来看一遍的从业者。接下来的推进顺序是 Linux 侧准备、Nginx 编译与配置、MySQL 与 PHP 打通、联调与排错每一步给可复制的命令和参数取值的理由。2. Linux 侧准备系统信息确认、依赖补齐与用户目录规划2.1 装机后先确认三件事发行版、内核、安全模块LNMP 里最容易翻车的一步是把一台机器当成另一台机器来操作。CentOS 7、Rocky、Ubuntu、银河麒麟这些发行版的包管理器和默认安全策略都不一样照着某篇教程敲yum install在 Debian 系上必然失败。我一般在登录后的第一分钟就把版本信息、内核、防火墙和强制访问控制的状态一次性打出来。# 发行版与版本号决定后面用 yum/dnf 还是 apt cat /etc/os-release # 内核版本PHP 编译时 glibc 兼容性参考 uname -r # 强制访问控制状态Enforcing 时 Nginx 读不到站点目录是常见坑 getenforce 2/dev/null || echo no selinux # 防火墙状态确定要不要放 80/443/3306 systemctl is-active firewalld 2/dev/null || systemctl is-active ufwgetenforce只有 RHEL 系有Debian 系没有这个命令所以加了兜底输出。systemctl is-active返回的是 active/inactive 文本比看status的整页日志快得多。如果 SELinux 处于 Enforcing而站点目录又放在/data/www这类非标准路径下Nginx 会直接返回 403 且错误日志写Permission denied——这时候不要急着chmod 777正确做法是用semanage fcontext给目录打上httpd_sys_content_t标签。提示把上面四条命令的输出记在部署笔记里后面出现任何环境相关的问题第一步都是回来比对这四行。2.2 用 linux 常用命令一次补齐编译依赖Nginx 和 PHP 我更倾向源码编译原因是发行版仓库里的版本往往偏老模块也固定后期想加--with-http_v2_module或者换 OpenSSL 版本就得重来。编译前需要把工具链和库一次装齐缺什么后面 configure 阶段会反复报错。# RHEL/CentOS/Rocky 系 yum groupinstall -y Development Tools yum install -y pcre-devel zlib-devel openssl-devel \ libxml2-devel sqlite-devel curl-devel \ libpng-devel libjpeg-devel freetype-devel # Debian/Ubuntu 系对应写法 # apt install -y build-essential libpcre3-dev zlib1g-dev \ # libssl-dev libxml2-dev libsqlite3-dev libcurl4-openssl-dev \ # libpng-dev libjpeg-dev libfreetype6-devpcre-devel提供 Nginx 的 location 正则支持zlib-devel提供 gzipopenssl-devel决定能不能上 HTTPSPHP 侧的libxml2-devel、libcurl-devel、libpng系列分别对应 DOM、curl 扩展和 GD 图像处理。少一个configure 就会在最后打印一屏not found挨个回装即可。编译过程中若内存吃紧make -j的并行度要降到 2 以下2C4G 机器上make -j4编 PHP 很容易被 OOM Killer 干掉表现为cc1: error: Killed。2.3 linux 新建用户与目录规划让 Nginx 和 PHP 不打架LNMP 的权限问题大多源于所有东西都用 root 跑。合理的做法是建一个专用的www用户Nginx 的 worker 和 PHP-FPM 的 pool 都切到它下面代码目录归属它Nginx 只读不要写上传目录单独放开写权限。# 建一个不能登录的系统用户专门跑 web 进程 useradd -r -s /sbin/nologin www id www # 确认 uid/gid后面配置里要用 # 目录规划代码、日志、运行态分开方便单独挂盘和清理 mkdir -p /data/www/{site1,site2} mkdir -p /data/logs/{nginx,php} chown -R www:www /data/www /data/logs chmod -R 755 /data/www-r创建系统用户uid 落在 1000 以下-s /sbin/nologin禁止登录避免被当成普通账号利用。上传目录一般设为755且属主为www不要用777——777配合 Nginx 上的 PHP 解析等于把任意文件执行的口子留出来了。路径属主权限用途/data/www/site1www:www755站点代码根目录/data/www/site1/uploadswww:www755用户上传需写权限/data/logs/nginxwww:www755access/error 日志/data/logs/phpwww:www755php-fpm 慢日志/usr/local/nginxroot:root755程序与配置只有 root 能改3. Nginx 编译安装与反向代理配置落地3.1 源码编译安装 Nginx 的最小命令序列与 configure 参数从官网拿到稳定版源码包后解压、configure、make、make install 四步走。configure 的参数是整套方案里最值得花时间的一项装完再想加模块只能重编。cd /usr/local/src tar zxf nginx-version.tar.gz cd nginx-version ./configure \ --prefix/usr/local/nginx \ --userwww --groupwww \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_realip_module \ --with-http_gzip_static_module \ --with-http_stub_status_module \ --with-pcre --with-stream make -j2 make install /usr/local/nginx/sbin/nginx -V # 回显编译参数部署记录留档--prefix决定二进制、配置、日志的落点统一放/usr/local/nginx便于备份--user/--group让 worker 进程直接以 www 身份运行省掉后期改配置--with-http_realip_module在使用 CDN 或上层代理时必须开否则日志和业务拿到的客户端 IP 全是代理 IP--with-stream提供四层转发能力用来做数据库或 TCP 服务的转发--with-http_stub_status_module打开状态页压测时能看到连接数。nginx -V的输出要留档将来排查「为什么某功能没生效」时第一反应是看模块到底编进去没有。3.2 nginx 配置文件详解全局块、events、http、server 四层各管什么nginx 配置文件的层级关系决定了参数的作用范围很多人改错位置就是没分清这四层。全局块管进程和用户events 管单进程连接模型http 管所有虚拟主机共享的默认值server 管单个站点的匹配规则。# 全局块进程身份与数量 user www www; worker_processes auto; # 跟随 CPU 核数2 核就是 2 error_log /data/logs/nginx/error.log warn; # events 块单 worker 的连接上限 events { worker_connections 10240; # 单进程并发连接数 use epoll; # Linux 下的事件模型 } http { include mime.types; default_type application/octet-stream; sendfile on; # 零拷贝发送静态文件 keepalive_timeout 65; client_max_body_size 32m; # 影响上传必须大于业务最大上传体积 log_format main $remote_addr - $http_x_real_ip [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent; access_log /data/logs/nginx/access.log main; include /usr/local/nginx/conf/vhost/*.conf; # 站点配置外置拆分 }worker_processes auto让 Nginx 按 CPU 核数自动分配理论并发上限是worker_processes × worker_connections但这个数字受限于系统ulimit -n所以要同步调/etc/security/limits.conf里的 nofile。client_max_body_size默认只有 1m不改的话后台上传稍大一点的文件就是 413而日志里只写一行client intended to send too large body很容易被忽略。把站点配置用include拆到vhost/目录是后面部署多个 web 项目的前提。3.3 nginx 反向代理与 nginx 部署多个 web 项目的 server 拆分写法一台机器上跑多个项目标准做法是一个项目一个 server 块靠server_name区分后端如果是 Java、Python 或者别的服务再用location做 nginx 反向代理。# 站点一PHP 项目走本机 php-fpm server { listen 80; server_name site1.example.com; root /data/www/site1; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass unix:/dev/shm/php-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~* \.(jpg|png|css|js)$ { expires 7d; access_log off; } } # 站点二把 /api/ 前缀的请求转到本机 8080 的后端服务 server { listen 80; server_name site2.example.com; location /api/ { proxy_pass http://127.0.0.1:8080/; 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_read_timeout 60s; } }try_files的作用是优先找静态文件找不到才交给 PHP 入口这是绝大多数 PHP 框架的伪静态标准写法。fastcgi_pass用 unix socket 比 127.0.0.1:9000 少一层 TCP 开销/dev/shm是内存文件系统注意机器重启后 socket 会消失由 php-fpm 自己重建。反向代理那段的proxy_pass末尾带斜杠会剥掉/api/前缀不带斜杠则原样透传这是最容易配错的一个字符差异X-Forwarded-For配合前面的realip模块后端才能拿到真实客户端 IP。3.4 银河麒麟等离线环境装 nginx 的依赖补齐顺序国产化机器的典型场景是内网、没有外网源装 Nginx 时会卡在pcre.h或openssl/ssl.h找不到。此时不要一个个去 yum 猜按下面的顺序准备离线包更稳先在能联网的同架构同版本机器上用yumdownloader --resolve把依赖连同二级依赖一起拉下来再拷进内网用rpm -ivh ./*.rpm --nodeps之前务必先rpm -ivh试一遍依赖顺序。如果连 rpm 都不方便就自己编译 pcre、zlib、openssl 到/usr/local下然后在 configure 时显式指定路径./configure --prefix/usr/local/nginx \ --with-pcre/usr/local/src/pcre-version \ --with-zlib/usr/local/src/zlib-version \ --with-openssl/usr/local/src/openssl-version \ --with-http_ssl_module这三个--with-*后面的路径指向的是源码目录不是安装目录这是离线编译时最常见的错误理解写错了 configure 会报not found但要翻很久日志才能定位。4. MySQL 安装配置与 PHP-FPM 打通4.1 mysql 安装配置教程8.0 初始化、字符集与远程账号MySQL 8.0 的安装包分 rpm 捆绑包和通用二进制包两类内网机器推荐通用二进制包解压即用不依赖系统库版本。初始化阶段要一次把字符集定死UTF-8 系列在 8.0 默认已经是utf8mb4但排序规则建议显式写成utf8mb4_0900_ai_ci避免从旧版本迁移时出现奇怪的比较结果。groupadd mysql useradd -r -g mysql -s /bin/false mysql tar xf mysql-8.0.x-linux-glibc2.17-x86_64.tar.xz -C /usr/local mv /usr/local/mysql-8.0.x-linux-glibc2.17-x86_64 /usr/local/mysql chown -R mysql:mysql /usr/local/mysql # 初始化数据目录同时生成临时 root 密码到 error log /usr/local/mysql/bin/mysqld --initialize --usermysql \ --basedir/usr/local/mysql --datadir/usr/local/mysql/data # 启动后必须先用临时密码登录再改密码 grep temporary password /usr/local/mysql/data/*.err--initialize会生成一个随机 root 密码并写进错误日志这是 8.0 相对 5.7 的一个明显变化很多人卡在「不知道初始密码」这一步。登录之后立刻改密码并建业务库和业务账号业务账号只授权到具体库不要给*.*ALTER USER rootlocalhost IDENTIFIED BY 新密码; CREATE DATABASE appdb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; -- 业务账号限定来源网段比 % 安全得多 CREATE USER appuser10.0.0.% IDENTIFIED BY 强密码; GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO appuser10.0.0.%; FLUSH PRIVILEGES;utf8mb4是四字节字符集能存 emoji 和生僻字授权粒度到库级等于给后续的误操作和数据外泄各加一道闸。远程连接还需要注意 8.0 默认的认证插件是caching_sha2_password老版本的 PHP mysql 扩展可能不认实在要兼容就对该账号执行ALTER USER ... IDENTIFIED WITH mysql_native_password BY ...但优先方案是升级 PHP 侧的驱动。4.2 PHP 编译参数与 php-fpm pool 的关键几项PHP 编译时容易被忽略的是--enable-fpm和--with-fpm-user/group前者决定有没有 php-fpm 这个进程管理器后者决定它跑在哪个身份下。./configure --prefix/usr/local/php \ --enable-fpm --with-fpm-userwww --with-fpm-groupwww \ --with-mysqlimysqlnd --with-pdo-mysqlmysqlnd \ --with-curl --with-openssl --with-zlib \ --with-gd --with-jpeg --with-freetype \ --enable-mbstring --enable-opcache make -j2 make install cp php.ini-production /usr/local/php/lib/php.ini cp /usr/local/php/etc/php-fpm.conf.default /usr/local/php/etc/php-fpm.confmysqlnd是 PHP 自带的 MySQL 原生驱动不需要额外链接 libmysqlclient交叉编译时省事很多--enable-opcache打开字节码缓存PHP 8.x 下对吞吐的提升非常直接。pool 配置里几个值要按机器内存算pm用dynamicpm.max_children大致按「可用内存 / 单个 PHP 进程平均占用」估单进程按 30~50MB 算4G 机器留一半给系统设 40 上下比较稳pm.max_requests设 500 到 1000用请求数兜住可能的内存泄漏。4.3 FastCGI 串联nginx 把 .php 请求交给 php-fpmNginx 和 PHP-FPM 之间靠 FastCGI 协议通信socket 路径两边必须完全一致。除了 3.3 节里 server 块的写法还需要在fastcgi_params里确保SCRIPT_FILENAME被正确赋值否则会出现「File not found」但文件明明存在的情况——本质是 PHP-FPM 在按它自己的document_root找文件。参数Nginx 侧PHP-FPM 侧不一致时的表现通信地址fastcgi_pass unix:/dev/shm/php-fpm.socklisten /dev/shm/php-fpm.sock502 Bad Gateway运行身份user wwwuser www / group www403 或权限拒绝脚本路径SCRIPT_FILENAME $document_root...无对应项依赖传入值File not found请求体上限client_max_body_sizeupload_max_filesize / post_max_size413 或空 POST改完php-fpm.conf或 pool 文件后要kill -USR2重载而不是直接重启前者能平滑换进程后者会掐断正在处理的请求。Nginx 侧改完配置先用nginx -t校验语法再nginx -s reload。4.4 四层超时与连接数对齐表502 和 504 的根因就是这几层超时参数不对称。原则是外层比内层大Nginx 等 PHP-FPM 的时间要大于 PHP 的max_execution_timePHP 等 MySQL 的时间要小于 MySQL 的wait_timeout。层参数建议值说明Nginxfastcgi_read_timeout60s等 php-fpm 返回的读超时Nginxproxy_read_timeout60s反代到后端服务的读超时PHPmax_execution_time30s脚本最长执行时间PHP-FPMrequest_terminate_timeout60s兜底杀死僵死进程MySQLwait_timeout600s空闲连接回收别小于连接池存活时间request_terminate_timeout必须大于max_execution_time否则 PHP 自己还没到执行上限FPM 先把进程杀了日志里会出现execution timed out而 PHP 错误日志一片空白很难排查。5. 联调验证与故障定位的具体手法5.1 三段式验证端口、静态页、动态页配置全部落地后不要直接开浏览器按从内到外的顺序验三层哪一层断了问题就锁定在那一层。# 第一层进程与端口是否在听 ss -lntp | grep -E 80|3306|9000 # 第二层Nginx 能否返回静态内容 echo ?php echo ok; /data/www/site1/t.php curl -s -o /dev/null -w %{http_code}\n http://127.0.0.1/t.php # 第三层PHP 能否连上 MySQL把探针写进临时文件再访问 curl -s http://127.0.0.1/t.phpss -lntp同时看端口和持有进程能立刻区分「服务没起来」和「起来了但没监听对地址」。curl加-w %{http_code}只输出状态码适合放进脚本做批量探测。如果第二层返回 200 但内容是 PHP 源码原文说明 Nginx 没把.php匹配给 FastCGI回到location ~ \.php$检查正则是否被前面的 location 截胡。5.2 502/504/1045 的信号与处置顺序502 的排查顺序固定为先看php-fpm.sock文件在不在、属主对不对再看 Nginx 的error_log里connect() to unix:... failed的具体 errno最后看 php-fpm 的www-error.log有没有启动失败。504 优先怀疑 PHP 卡在慢 SQL 或外部接口调用先把request_slowlog_timeout和slowlog打开慢日志会把卡住的那段调用栈直接打出来。MySQL 报 1045 是账号密码或来源主机不匹配报 1040 是连接数打满前者查mysql.user表的 host 字段后者核对max_connections与应用连接池的总量。# 打开慢日志定位 504 的调用栈需要重启 php-fpm 生效 # 在 pool 配置里加 # request_slowlog_timeout 5s # slowlog /data/logs/php/$pool-slow.log tail -f /data/logs/php/www-slow.log # 卡住的脚本与堆栈 tail -f /data/logs/nginx/error.log # upstream 相关行压测阶段用ab -n 2000 -c 100 http://127.0.0.1/t.php打一轮同时开着nginx状态页看Active connections和Waiting的变化再配合top -H看是不是 PHP 进程吃满 CPU。真正能省时间的习惯只有一个把 Nginx 的error_log级别临时调到info用一条curl -I打过去然后 grep 出带upstream的那几行比毫无方向地改配置快得多。本文还有配套的精品资源点击获取