ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MacOS重装后一步到位:Shell脚本自动化配置Nginx+PHP环境

MacOS重装后一步到位:Shell脚本自动化配置Nginx+PHP环境 刚重装完系统那会儿我盯着空白的终端心想又要把 nginx、PHP、MySQL 这一整套重新过一遍头都是大的。如果你也经历过 MacOS 重装后开发环境全部推倒重来的滋味或者每次换新机器都要花一下午在 brew install 和改配置文件之间来回折腾那你应该能理解我为什么要把这些流程全部塞进一个 Shell 脚本里。这篇文章就把我在 MacOS 上用 Shell 脚本一步到位配置 nginxPHP 环境的完整方法写出来。没有花哨操作全是日常真在用的东西从系统工具链检测、Homebrew 安装、nginx 和 PHP 的自动部署到站点配置自动生成、自定义域名、多端口访问再到最后我把虚拟机联调、日志排查这些场景也揉了进来。读完你不仅能得到一段能直接跑的脚本还能理解每一步为什么要这么写以及踩到 404、502 这类经典问题该怎么定位。1. 重装系统后的环境重建困境为什么选择 Shell 脚本这条路1.1 手动配 nginxPHP 到底慢在哪MacOS 上装 nginx 和 PHP看起来就是两条 brew install 命令的事但真正把本地开发环境跑起来远没这么简单。我最早也是照着网上的教程一步步点每次新建一个 PHP 项目就要去改一次 nginx 配置改完还要记得nginx -s reload。配置写错了没有直观提示只能瞪着浏览器里的 404 not found nginx 发呆。日子久了/usr/local/etc/nginx/nginx.conf被我改得面目全非加了一堆注释掉的 server 块根本不敢乱动。重装系统时这个问题会被无限放大。你不仅要把 nginx、PHP 重新装一遍还要重新建目录、重新配 php-fpm、重新设置 sites-enabled 的软链接、重新把 hosts 里那一堆本地域名加回来。手动来一遍少说半小时多则一个下午。中间只要漏了一步比如忘了启动 php-fpm后端页面全给你 502 Bad Gateway。1.2 面板、手动、Docker、脚本四种方式的实际对比我身边同事常用的方案有这么几类各有利弊我放个表直接说清楚方案核心操作耗时预估可重复性主要痛点面板类工具下载 App、图形界面点按钮10 分钟好版本往往滞后资源占用高和命令行工作流脱节手动 brew 安装逐条执行命令、手改配置文件30 分钟以上差步骤零散重装系统后全得重来Docker 容器写 docker-compose、拉镜像15 分钟很好本地文件权限、端口映射偶尔让人头大内存开销偏高Shell 脚本一条命令自动执行完整流程5 分钟内极好需要维护脚本本身一开始我用的是 Docker后来发现本地开发频繁调整 nginx 配置时容器内和宿主机之间的路径映射要反复确认不太顺手。而且一旦遇到要在宿主机的hosts文件和多个域名之间做联调容器方案反而多了一层要翻译的逻辑。用 Shell 脚本是更贴近 MacOS 原生习惯的做法装完系统拉下脚本跑一遍环境回来了。1.3 脚本方案的深层价值脚本方案最大的价值不是“省那几十分钟”而是把环境配置从“手动记忆”变成了“代码资产”。你的 nginx 配置规则、PHP 版本选择、监听端口、站点目录这些约束全部固化在脚本里。下次重装或者给另一台 Mac 配置克隆仓库下来一条命令跑完环境一致。我在公司里就是这么干的新同事入职领的是同一份脚本大家的本地环境完全同步出问题都好交流。2. 动手前的准备工具链、目录约定与版本选型2.1 先补上系统最基础的编译工具写脚本之前得明确一个前提MacOS 的 nginx 虽然可以用 brew 直接装二进制包但 brew 本身依赖 Xcode Command Line Tools 提供的基础编译环境。新系统上这个未必装好了所以脚本的第一件事就是检测它。xcode-select -p会输出 Command Line Tools 的安装路径如果为空就执行xcode-select --install触发系统安装。这一步虽然不能完全做到无人值守但能把流程堵在第一关比跑到 install 到一半报“缺少编译器”要舒服得多。2.2 Homebrew 的架构差异与安装判断Homebrew 在 Intel Mac 和 Apple Silicon Mac 上的安装路径不一样。Intel 是/usr/localM 系列芯片是/opt/homebrew。这个差异直接决定了 nginx 配置、PHP 配置的读取路径脚本里必须兼容否则换一台机器就跑不通。我的做法是先让脚本检测芯片架构把前缀存进一个变量if [[ $(uname -m) arm64 ]]; then HOMEBREW_PREFIX/opt/homebrew else HOMEBREW_PREFIX/usr/local fi然后判断 brew 是否已存在不存在才去执行安装命令。这么写还有一个好处无论你机器上的 brew 是用官方脚本装的、还是国内镜像装的脚本都不会因为重复安装而报错。安装 brew 本身在国内网络环境下容易卡如果遇到下载很慢的情况可以用中科大或清华的镜像源但脚本内部建议保留官方源的注释别写死方便你自己切换。2.3 PHP 版本怎么选不踩雷brew 现在提供php和php8.2、php8.3这类版本化包。直接装不带版本的php当前 brew 主版本是什么就会装什么短期没问题但过两年 brew 更新主版本你项目里跑得好好的东西可能突然行为变了。我脚本里用的是php8.3它是当前兼容性最好、跑 WordPress、ThinkPHP、Laravel 都稳的一个版本。在 MacOS 上同时装多个 PHP 版本也常见切换靠brew link --force完成。但注意一旦用版本化包名安装php-fpm 的服务名也是版本化的比如php8.3对应的服务是brew services start php8.3。我见过不少人在这一步栽跟头装的是php8.3启动时却敲brew services start php报错说服务不存在。2.4 目录约定一开始就定好省得后面全乱我建议在脚本里定义一个标准的站点根目录比如~/www。所有项目都放在~/www/project-name下每个项目一个子目录。这样做的好处是 nginx 配置生成脚本可以根据项目名自动拼出 root 路径不用每次手动填。目录规划示例SITES_ROOT$HOME/www SITE_NAMEdemo SITE_ROOT$SITES_ROOT/$SITE_NAME mkdir -p $SITE_ROOT/public mkdir -p $HOMEBREW_PREFIX/var/log/nginx日志目录我也单独建好。nginx 默认日志会写到 brew 的 var/log 下这个路径在 Apple Silicon 机器上属于/opt/homebrew/var/log/nginx提前确认它存在可以避免后续权限问题。千万别把日志直接放站点目录里后面日志一多备份站点时痛苦死。2.5 端口规划要提前想清楚MacOS 上做本地 PHP 开发80 端口一般是够用的但不少人会遇到两个问题一是 AirPlay 接收器会占用 5000 和 7000 端口你如果把服务监听到 5000大概率跟系统服务撞车二是多个项目同时跑一个 80 端口不够分。我的规划很简单本机默认唯一入口走 80 端口配合域名区分项目如果要用到虚拟机联调或者多实例并行第二个及以后的站点就走 8080、8081 这类高位端口。端口从一开始就在脚本里参数化后面扩展不会改得手忙脚乱。3. 脚本初版从 brew 安装到 nginxphp-fpm 联通3.1 完整初版脚本逐段解析先把第一版脚本完整贴出来这段做的是最基础的事装工具、启动服务、验证可访问。#!/bin/bash set -e # 架构检测Intel 和 Apple Silicon 的 brew 路径不同 if [[ $(uname -m) arm64 ]]; then HOMEBREW_PREFIX/opt/homebrew else HOMEBREW_PREFIX/usr/local fi # 1. 检测 Xcode Command Line Tools if ! xcode-select -p /dev/null; then echo 正在安装 Command Line Tools请稍候... xcode-select --install # 等待用户手动完成安装 until xcode-select -p /dev/null; do sleep 5 done fi # 2. 安装 Homebrew如果不存在 if ! command -v brew /dev/null; then echo 正在安装 Homebrew... /bin/bash -c $(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh) # Apple Silicon 需要把 brew 路径加入 PATH if [[ $(uname -m) arm64 ]]; then echo export PATH/opt/homebrew/bin:$PATH ~/.zshrc export PATH/opt/homebrew/bin:$PATH fi fi # 3. 安装 nginx 和 php8.3 echo 正在安装 nginx 和 php8.3... brew install nginx php8.3 # 4. 确认 php-fpm 监听地址脚本统一用 127.0.0.1:9000 PHP_FPM_CONF$HOMEBREW_PREFIX/etc/php/8.3/php-fpm.d/www.conf if [[ -f $PHP_FPM_CONF ]]; then sed -i s/^listen .*/listen 127.0.0.1:9000/ $PHP_FPM_CONF echo php-fpm 监听地址已设置为 127.0.0.1:9000 fi # 5. 启动服务 brew services start nginx brew services start php8.3 # 6. 验证基础可用性 curl -s -o /dev/null -w nginx 状态码: %{http_code}\n http://localhost php -v | head -1这个脚本里每一段都有原因。set -e必不可少任何一条命令失败立即终止避免带着残缺环境往下跑报错时你一眼能定位到哪一步。sed -i 是 MacOS 的 BSD sed 语法后面必须有一个空字符串参数Linux 的 GNU sed 不这么写这条新手很容易抄错。3.2 php-fpm 监听地址socket 还是 TCP脚本里我把 php-fpm 统一改成了127.0.0.1:9000。brew 装完的 PHP 默认监听可能不是这个地址不同版本行为不完全一样所以显式设置一次最稳妥。关于用 Unix socket 还是 TCP 地址这里多说一句。很多人推荐 socket 方式说性能更好这在高并发下确实成立。但本地开发环境TCP 的127.0.0.1:9000有一个不可替代的优势排错方便。你随时可以用lsof -i :9000确认 php-fpm 是否在监听也可以用nc -vz 127.0.0.1 9000直接测试端口连通性。而 socket 方式一旦路径写错页面上一律 502排查起来你只能去翻error.log。本地开发图的就是省心我选 TCP。3.3 跑通后的第一轮验证脚本执行完先看输出内容nginx 状态码应该是 200php -v会打印版本号。如果 nginx 状态码是 404别慌因为 brew 装完 nginx 后默认 root 指向的是/usr/local/var/www或/opt/homebrew/var/www这个目录下有个默认的 index.html正常会返回 200。如果是 404 not found nginx很可能上一次安装残留了以前改过的配置root 指到了不存在的目录。接着验证 PHP 是否真的被 nginx 处理了。在站点根目录放一个info.php里面写?php phpinfo(); ?然后在浏览器访问http://localhost/info.php。如果看到的是 PHP 信息页说明 fastcgi 通路已经打通。如果直接变成下载文件那说明 nginx 没有把.php结尾的请求交给 php-fpm问题基本锁定在配置里的location ~ \.php$块。4. 一步到位的关键Nginx 与 PHP-FPM 的配置自动生成4.1 手工维护配置文件的麻烦brew 装完 nginx 后主配置文件在$HOMEBREW_PREFIX/etc/nginx/nginx.conf。第一次打开这个文件你会发现里面满是示例配置注释占了半屏。如果你直接往里面堆 server 块前几次还行等站点多了改一个就要小心别碰另一个。我的做法是主配置只留一个骨架http 块里加一行include servers/*.conf;然后在一个独立的servers目录下给每个站点建独立配置文件。站点增删不影响主配置想停哪个站点直接删对应文件再 reload干净利落。4.2 脚本里的 add_site 函数下面这段是脚本里最有价值的部分它把“新建一个 PHP 站点”这个动作封装成了一个函数add_site() { local site_name$1 local site_port$2 local site_root$3 local server_names$4 local nginx_conf$HOMEBREW_PREFIX/etc/nginx/servers/$site_name.conf if [[ -f $nginx_conf ]]; then echo 配置文件已存在跳过生成: $nginx_conf return 0 fi mkdir -p $site_root/public cat $nginx_conf EOF server { listen $site_port; server_name $server_names; root $site_root/public; index index.php index.html; location / { try_files \$uri \$uri/ /index.php?\$query_string; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name; } access_log $HOMEBREW_PREFIX/var/log/nginx/$site_name.access.log; error_log $HOMEBREW_PREFIX/var/log/nginx/$site_name.error.log; } EOF echo 已生成站点配置: $nginx_conf }用的时候这样调用add_site demo 80 $HOME/www/demo demo.test add_site blog 8081 $HOME/www/blog blog.test有几个细节必须解释清楚。第一try_files $uri $uri/ /index.php?$query_string;这一行的作用是把不存在的静态路径统一交给index.php处理这是 Laravel、ThinkPHP 这类单入口框架能跑起来的核心不用这行前端路由全部 404。第二fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;很关键它告诉 php-fpm 要执行的脚本路径很多人配完出现“No input file specified”就是漏了这行或者写错了变量名。第三heredoc 里我把$uri、$query_string、$document_root都加了转义因为不加转义的话这些都是当前 Shell 的空变量生成出来的配置会是空的这在脚本里属于很容易出现但又最阴的坑。4.3 一次执行、重复执行都不出错脚本要做到“一步到位”还得保证幂等性也就是同一条命令跑十次结果是一样的不会因为重复执行把环境搞坏。关键在于每步操作前都要做判断。command -v brew判断 brew 是否存在-f $nginx_conf判断配置是否已存在存在就跳过。日志文件已经存在时mkdir -p也不会报错。这样即便是误操作重跑了一遍脚本它顶多是确认服务已经启动不会把你自己后加的 nginx 配置给覆盖了。脚本能自动“识别增量”才是真正能日常放心用的脚本。4.4 生成配置后的重载与验证配置文件生成完之后不能直接就算完要强制过一遍 nginx 的校验nginx -t nginx -s reloadnginx -t会提示配置是否有语法错误如果报错会精确到文件路径和行号。看到syntax is ok之后再 reload线上改动配置的时候我也是这个习惯先测再生效这习惯能避免把正在跑的服务弄挂。顺手再验证一下curl -I http://demo.test curl -I http://blog.test这里有个小地方提醒一下如果用的是自定义域名比如demo.test那么访问它之前必须先在/etc/hosts里加一行127.0.0.1 demo.test否则域名解析会跑到公共 DNS 去拿到的很可能是一个不存在的公网 IP。5. 多站点与自定义域名从单机到“本地虚拟机”联调5.1 hosts 与 server_name 的配合逻辑MacOS 上做本地多站点核心思路就两条域名映射靠/etc/hosts请求分发靠 nginx 的server_name。浏览器访问demo.test时先向本机发起请求nginx 拿到请求后用 Host 头匹配定义的 server 块。匹配规则是精确优先于通配符和正则的所以只要 server_name 不重复就不会有路由错乱。在 hosts 里加静态映射127.0.0.1 demo.test 127.0.0.1 blog.test ::1 demo.test ::1 blog.testIPv6 的::1也加上是有原因的MacOS 的 curl 在某些网络环境下默认优先::1解析只加127.0.0.1时可能出现“偶尔通、偶尔超时”的怪现象。5.2 多端口方案同机并行和虚拟机访问自定义域名解决的是“多项目识别”问题但如果你本地还要跑 Vue 的 dev server或者要起一个 mock 服务那 80 端口就可能不够用。此时第二个、第三个站点可以用高位端口来跑。比如add_site api 8080 $HOME/www/api api.test访问方式就是http://localhost:8080或者http://api.test:8080。注意端口在 listen 里写了URL 里也要带hosts 只管域名不管端口端口由 nginx 监听决定。我实际开发中还经常用到“本地虚拟机”的联调场景就是在虚拟机里跑一个测试站宿主机这边通过局域网 IP 去访问。这种情况下 nginx 的 listen 要把监听地址放宽listen 0.0.0.0:8080;因为默认 listen 80 在 MacOS 上不需要 root 权限但在虚拟机里访问宿主机时请求是走局域网网卡进来的不是走回环地址。只监听localhost的话虚拟机访问宿主机 IP 就会连接被拒。宿主机 IP 可以通过ipconfig getifaddr en0查到en0 一般是 Wi-Fi 网卡。在虚拟机的浏览器里访问http://宿主机IP:8080就能访问到宿主机上的 PHP 站点。这个组合很实用比如你要在 Windows 虚拟机里测试 IE 兼容的页面或者用虚拟机跑一个旧版浏览器做调试都能靠这个方案接上。5.3 用 for 循环批量创建站点多站点如果都是一条条 add_site 命令写脚本会变得很长。我用一个关联数组把站点信息集中起来后面有 for 循环自动遍历创建declare -A SITES( [demo]80 demo.test [blog]8081 blog.test [api]8080 api.test ) for site_name in ${!SITES[]}; do read -r site_port server_names ${SITES[$site_name]} add_site $site_name $site_port $HOME/www/$site_name $server_names done # 统一写入 hosts for line in ${!SITES[]}; do read -r site_port server_names ${SITES[$line]} for domain in $server_names; do grep -q $domain /etc/hosts || echo 127.0.0.1 $domain | sudo tee -a /etc/hosts done done写这段时我用到了两个 Shell 考点正好对应很多人在学的 for 循环和 if 判断。第一个考点是关联数组的遍历方式for site_name in ${!SITES[]}注意感叹号在前是取键列表没有感叹号取的是值列表别写反。第二个考点是read -r port server_names ${SITES[$site_name]}这是把字符串按空白拆成两个变量的常用写法。第三个考点是grep -q ... || echo ...这个模式相当于“如果不存在才写入”用在幂等脚本里很干净。批量创建完记得统一nginx -t nginx -s reload。以后加新站点只需要改数组再跑一次比手写配置效率高一截。6. 调试与排错我从实际使用中遇到的几个典型问题6.1 404 not found nginx先分清楚是哪一层 404浏览器里看到 404 not found nginx 时说明请求已经到 nginx 了问题出在配置或者文件路径上。最常见的是 root 路径写错、目录没建对、或者 index 文件不存在。排查链路我建议这样走tail -f $HOMEBREW_PREFIX/var/log/nginx/error.log同时再访问一次出错页面。日志会明确告诉你open() /xxx/xxx/index.php failed (2: No such file or directory)路径对不上就一目了然。另外一个隐蔽原因是 nginx 的 location 匹配优先级。很多人手动配置时会这样写location / { root /path/to/site; } location ~ \.php$ { root /path/to/site; }看着没问题但当顶层另有location /app/带二级路径时PHP 文件的SCRIPT_FILENAME会根据 URI 拼接二级路径被重复拼到目录里同样会 404。这种带二级路径的站点我在公司见过不少次排查到后面基本都是在看路径拼接规则。6.2 502 Bad Gateway十有八九是 php-fpm 没起来502 和 404 不同502 说明 nginx 已经找到了虚拟主机配置但请求往后转发时php-fpm 那边没有响应。最常见的死法是重装 PHP 或者升级 brew 后忘了重新启动php8.3服务。排查命令很简单lsof -i :9000没有任何输出说明监听不存在那就启动brew services start php8.3还有一种情况是 php-fpm 起来了但监听地址和 nginx 配置里写的不一致。你手动改过 php-fpm 的listen参数但 nginx 那段fastcgi_pass 127.0.0.1:9000还是老配置也会 502。解决方式就是配置生成脚本里强制统一用一个变量管理这个地址让两边同时生成从源头避免不一致。6.3 脚本重复执行时的小坑我在写脚本时遇到的另一个问题是重复执行时sed会反复修改 php-fpm 配置。这个其实问题不大因为sed -i s/^listen .*/listen 127.0.0.1:9000/是“匹配到就去替换”重复执行也是原来的结果。真正要小心的是站点配置的生成函数如果不检查配置是否已存在就覆盖写入你自己手动改过的特殊配置会被脚本冲掉。所以函数里必须有[[ -f $nginx_conf ]] return这步判断。6.4 端口被 AirPlay 和系统服务占用如果你发现 nginx 一直启动失败日志提示bind() to 0.0.0.0:5000 failed那几乎可以确定是 macOS 的 AirPlay 接收器占了 5000 和 7000 端口。你可以在系统设置的“通用”里关掉“AirPlay 接收器”或者干脆让 nginx 换端口。窝里斗没意义直接错开。再看一眼所有监听端口可以用lsof -iTCP -sTCP:LISTEN -P -n | grep -E nginx|php这个输出会列出当前所有 nginx 和 php-fpm 的监听状态哪个端口没起来一目了然。6.5 站点目录的访问权限问题PHP 页面偶尔会有权限相关报错例如提示Permission denied通常是站点目录对其他用户不可读。brew 服务默认以当前用户身份运行正常情况下没这个问题但如果你的站点文件是 sudo 创建或者从压缩包解压出来的属主都变成 root 了nginx 可能读不了。我的习惯是在脚本里顺手把站点的属主改回当前用户chown -R $USER:staff $SITE_ROOT这句执行完权限问题九成都能消掉。最后分享一个我写脚本时的体会一开始只想着“跑通就行”后来慢慢加了幂等判断、日志输出和错误退出这个脚本才真正从一个一次性工具变成日常依赖的“环境还原脚本”。每次重装系统后跑一遍看着终端里刷刷刷的输出那种安心感是手动配置给不了的。你现在拿到的这套流程如果按自己的站点目录和端口习惯改一改基本就能直接入仓使用。后面我还在计划把 MySQL 的自动安装也加进去到时候再单独更新一篇。
返回列表