ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

macOS上使用Docker搭建PHP开发环境:Nginx与PHP-FPM实战

macOS上使用Docker搭建PHP开发环境:Nginx与PHP-FPM实战 上个月接手一个 PHP 老项目需要在 macOS 上把本地环境跑起来。我一开始的打算是装个集成环境包但机器是 M 系列芯片试了几个版本都不太对扩展还得专门去编译折腾得人很烦躁。后来索性把 Nginx 和 PHP 全部塞进 Docker 容器用 Docker Compose 一套编排反而半个多小时就把开发环境立起来了。这篇文章就把这套完整流程记下来包括 macOS 上安装 Docker 的注意点、Nginx 与 PHP-FPM 协作的原理、Compose 配置的写法和几个高频报错的排错思路。适合想在 mac 上跑 PHP 项目的开发者不管是新手还是被多版本环境切换整怕了的老手按着这套走基本不会再在环境搭建上卡壳。1. 方案选型为什么我选了 Docker 而不是装集成环境1.1 集成环境包的常见尴尬在 macOS 上做 PHP 开发身边同事用得最多的方案是装图形化集成环境比如 MAMP、XAMPP或者用包管理器直接装 Nginx、PHP 到系统里。这些方案对简单项目确实快但一旦项目多起来就难受了。项目 A 用的是 PHP 7.4项目 B 要用 PHP 8.2项目 C 还需要扩展 gd、redis、swoole。集成环境通常只会给你一个固定的 PHP 版本想装第二个版本就得切换配置弄不好全局依赖就乱了。更别提 M 系列芯片刚普及那会儿很多集成环境的二进制包还没适配下载下来要么打不开要么运行卡到怀疑人生。包管理器装的 Nginx 和 PHP 属于系统级服务升级系统或者清理依赖的时候可能波及正在跑的项目这是我最害怕的点。1.2 Docker 方案到底解决了什么Docker 的思路是把每个项目的运行环境打包成独立的容器。Nginx 是一个容器PHP-FPM 是一个容器MySQL 又是独立的一个。它们就像三台各自运行的小型虚拟机互相不干扰但又通过网络联在一起工作。换项目时互不冲突是 Docker 方案最大的红利。项目 A 的 PHP 7.4 容器继续跑着项目 B 的 PHP 8.3 容器也可以同时跑着互不侵犯系统环境。想删掉一个项目的时候容器删掉即可不会残留一堆系统级的库和配置。迁移到新电脑也简单代码和配置文件一起拷过去一条命令把容器拉起来环境就恢复了。这一点是集成环境给不了的。适用边界我也说清楚如果你只是临时跑一个 5 分钟就完事的 PHP 文件或者电脑配置很低那装个轻量级单文件环境可能更合适。Docker 需要一点学习成本但只要是正经做 PHP 项目这个成本非常值得投入。1.3 方案确定的几个关键点确定用 Docker 之后具体选型我还做了几个明确决定使用 Docker Desktop 作为容器运行载体。macOS 本身跑不了 Linux 容器Docker Desktop 会在后台自动维护一个轻量 Linux 虚拟机对使用者透明体验最接近原生。使用 Docker Compose 管理容器编排。Nginx 和 PHP 两个容器之间有固定关系写成一个 compose 文件既能控制启动顺序又能让它们自动加入同一个网络比敲一长串 docker run 命令可靠得多。端口映射采用 8080 映射到容器内 80。macOS 上 80 端口经常被其他服务占掉本地开发用 8080 更省心必要时再改成别的端口。这些选择不是随手拍的后面配置过程中体现出来的便利性每一项都踩到了实处。2. macOS 上安装 Docker 的实操细节2.1 先确认芯片架构再下载安装包macOS 安装 Docker 第一步就藏着坑芯片架构不同安装包也不同。M 系列芯片和 Intel 芯片对应的安装包必须分开选选错的话最常见的结果是安装后双击提示无法打开或者 Docker Desktop 一直报架构错误。区分方法很简单打开终端执行uname -m输出arm64的就是 M 系列芯片比如 M1、M2、M3输出x86_64的就是 Intel 阵营。还可以再用sysctl -n machdep.cpu.brand_string这条命令直接把 CPU 型号打印出来一目了然。拿到架构信息后去 Docker 官网的下载页找对应的 Docker Desktop for Mac 安装包。现在官网页面上通常会标清楚 Apple Silicon 版本和 Intel Chip 版本按终端里的结果选就行。下载下来的是一个 .dmg 文件双击打开后把 Docker 图标拖进 Applications 文件夹就完成了最基础的安装。2.2 首次启动时需要完成的设置拖进 Applications 后在启动台打开 Docker Desktop。第一次启动会要求你接受使用条款部分场景下还会要求登录 Docker Hub 账号这是官方现在的常规流程按提示走就行。启动后右上角菜单栏会出现一个小鲸鱼图标等它从动画变成静止状态意味着 Docker 引擎已经准备就绪。这个等待时间第一次可能比较久因为后台虚拟机要初始化。打开终端验证docker --version docker compose version能正常打印出版本号说明整体装好了。我习惯顺手跑一条docker info这条命令输出内容多不用全看重点是确认Operating System和Architecture两个字段能直观看出当前 Docker 引擎挂载在什么环境上方便早期发现问题。2.3 镜像拉取慢的通用解法首次使用 Docker最直觉的操作是拉一个测试镜像跑跑看但很多人卡在这里拉取镜像速度十分感人一个几十 MB 的镜像可能要等半天。这个不是 Docker 本身的问题而是你的网络到镜像仓库之间的链路质量不稳定。通用的解法是配置 registry-mirrors也就是镜像源。在 Docker Desktop 的 Settings 里找到 Docker Engine 配置项在 JSON 配置里加一段{ registry-mirrors: [https://registry-mirror.example.com] }注意引号里的地址一定要换成你自己网络环境实际可用的镜像源地址。很多云厂商会提供公共服务公司内网有时也会搭建私有镜像源我建议优先咨询你所在公司的运维或者直接查云厂商文档选一个延迟低的。配置完点击应用并重启 Docker再用docker pull试一次速度正常就说明生效了。我见过有些朋友在这个环节花了很多心思找各种镜像源其实大可不必。如果基础镜像拉取只是慢可以耐心等一次后续本地有了缓存再次创建容器基本不会再走下载流程。2.4 用 hello-world 验证链路是否完整安装配置完之后建议先跑一个最基础的测试容器确认整条链路是通的。执行docker run --rm hello-world首次执行会先拉取 hello-world 镜像然后短暂运行一个容器并打印一段信息最后自动清理删除。看到终端里输出那句 Hello from Docker 的经典文案就表明安装、拉取、创建容器、运行容器这条链路全部正常。这一步通过后就可以放心进入 Nginx 和 PHP 的部署环节了。如果这一步都报错后面部署 Nginx 肯定也成不了先回头检查安装和镜像源配置。3. 先手工跑一套 nginxphp 容器链路3.1 理解 Nginx 和 PHP-FPM 是怎么配合的讲解部署之前得先把 Nginx 和 PHP-FPM 的关系讲清楚否则你后面看配置容易一头雾水。Nginx 本身只会处理静态资源和 HTTP 协议上的转发逻辑。当浏览器请求一个 .php 文件时Nginx 看不懂 PHP 代码它只能把这些请求按规则转交给 PHP-FPM——一个独立的 PHP 进程管理器。PHP-FPM 接收请求后定位到实际的 PHP 文件执行 PHP 代码把动态结果回传给 Nginx再由 Nginx 返回给浏览器。这个转交动作走的是 FastCGI 协议。配置里常见的fastcgi_pass php:9000;就是在告诉 Nginx把 PHP 请求转发到名为 php 的那台主机、9000 端口上。你可以把 Nginx 理解成前台接待把 PHP-FPM 理解成后台技术处理接待把活儿派给技术技术把结果交回前台。正因为是这种协作关系两个容器必须能够互通。Docker 里容器之间互访需要把它们放到同一个自定义网络中。3.2 准备代码目录和测试文件我先手工跑一遍完整链路目的是让你直观看到 Nginx 和 PHP 两个容器之间是怎么关联的再来写 Compose 时理解会深很多。先创建一个项目目录这里以php-nginx-demo为例mkdir -p ~/www/php-nginx-demo/www mkdir -p ~/www/php-nginx-demo/nginx/conf.d cd ~/www/php-nginx-demo在www目录下写一个测试文件index.php?php phpinfo();phpinfo()会输出当前 PHP 环境的完整信息用它验证 PHP-FPM 是否被正确执行是最直观的。然后创建一个 Docker 自定义网络让两个容器能通过域名互相访问docker network create app-net3.3 启动 PHP-FPM 容器执行docker run -d --name demo-php \ --network app-net \ -v ~/www/php-nginx-demo/www:/var/www/html \ php:8.3-fpm-alpine拆开来看每个参数的意思-d后台运行容器。--name demo-php给容器起名字后续别的容器可以通过这个名字访问它。--network app-net加入刚才创建的网络。-v ~/www/php-nginx-demo/www:/var/www/html把宿主机代码目录挂载到容器内/var/www/html目录。这样代码文件在宿主机修改容器内立即同步。这里我没加-p端口映射因为 PHP-FPM 的 9000 端口只需要被 Nginx 容器访问不需要暴露到宿主机。这也是容器隔离的一个特点内部服务按需开放。启动后用一条命令确认状态docker psdemo-php出现在列表里并且状态是 Up说明 PHP-FPM 已经跑起来了。3.4 编写 Nginx 站点配置并启动 Nginx写一份 Nginx 站点配置nginx/conf.d/default.confserver { listen 80; server_name localhost; root /var/www/html; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass demo-php:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }这里有几个关键点值得展开说。root /var/www/html;这条不是随手写的。前面启动 PHP-FPM 容器的时候我把代码目录挂载到了容器内的/var/www/html。Nginx 这边也把同一目录挂载到/var/www/html并且root也指向这里这样 Nginx 转交给 PHP-FPM 的SCRIPT_FILENAME路径才和 PHP-FPM 实际看到的文件路径完全一致。如果两边挂载路径不统一PHP-FPM 就会拿着 Nginx 给的路径找不到文件最终返回 404 或者报 File not found。这是整套配置里我踩过的最深的坑后面问题排查部分还会专门讲。location ~ \.php$是正则匹配凡是请求路径以.php结尾的都由这个块处理。块内的fastcgi_pass demo-php:9000;是转交目标demo-php是 PHP-FPM 容器的名字在这个网络里可以直接当作主机名用。SCRIPT_FILENAME是 PHP-FPM 用来定位具体文件的关键参数$document_root表示 root 指令指定的根目录$fastcgi_script_name表示请求的脚本路径。启动 Nginx 容器docker run -d --name demo-nginx \ --network app-net \ -p 8080:80 \ -v ~/www/php-nginx-demo/www:/var/www/html \ -v ~/www/php-nginx-demo/nginx/conf.d:/etc/nginx/conf.d:ro \ nginx:stable-alpine参数里新增的部分-p 8080:80把宿主机的 8080 端口映射到 Nginx 容器的 80 端口。反过来理解就是浏览器访问http://localhost:8080时请求会进入 Nginx 容器的 80 端口。:ro挂载配置文件目录时指定只读防止容器内部意外修改宿主机配置文件。然后在浏览器访问http://localhost:8080如果能看到phpinfo()的黄色表格页面就说明 Nginx 和 PHP-FPM 的整条链路已经打通了。3.5 手工方案的优势和局限手工 docker run 的优势是把每个参数的含义展示得非常清楚适合第一次接触的人理解原理。但它的局限也明显两条命令要手动指定网络、挂载、端口一长串记不住容器之间依赖关系靠人脑记忆换台电脑就得全部重敲一遍。所以在确认链路能跑通之后正确的下一步是把这套手动配置固化成 Docker Compose 文件。4. 用 Docker Compose 把环境固化下来4.1 Compose 到底在干什么Docker Compose 可以理解成一种基础设施即代码的写法。你把需要运行的容器、它们之间的关系、挂载目录、端口映射全部写进一个 YAML 文件里然后执行一条docker compose up -d所有容器按声明的方式创建和启动。这样做的好处是项目环境定义化。团队协作时把 compose 文件和项目代码放进同一仓库新人克隆代码后执行一条命令就能获得和开发环境完全一致的容器编排。不再需要整理冗长的 shell 命令给人看文件本身就是文档。4.2 编写 docker-compose.yml在项目根目录下创建docker-compose.yml文件内容如下services: nginx: image: nginx:stable-alpine container_name: demo-nginx ports: - 8080:80 volumes: - ./www:/var/www/html - ./nginx/conf.d:/etc/nginx/conf.d:ro depends_on: - php networks: - app-net php: image: php:8.3-fpm-alpine container_name: demo-php volumes: - ./www:/var/www/html networks: - app-net networks: app-net: driver: bridge和手工方案对比内容几乎一一对应但有几个新知识./www是相对路径指向宿主机当前目录下的 www 文件夹。使用相对路径后整个项目目录可以整体移动只要目录内部结构不变就能继续工作。container_name指定容器名不写的话 Compose 会生成一个带项目前缀的随机名字但显式指定便于用 docker exec 调试。depends_on声明启动顺序让 Nginx 等 PHP 先启动避免启动瞬间 Nginx 找不到解析目标而报错。用php作为服务名网络内主机名就是php所以 Nginx 配置里fastcgi_pass php:9000;代替了手工方案中的demo-php:9000。新版 Docker Compose 已经不需要写version字段直接写services即可写到旧版本也不会报错只是会有弃用提示。另外driver: bridge是默认网络类型写出来是为了明确语义不写效果也一样。4.3 启动、停止和查看容器状态启动命令docker compose up -d-d表示后台运行。首次执行会拉取两个镜像可能需要一点时间。完成后查看状态docker compose ps应该能看到 nginx 和 php 两个服务都是 Up 状态。此时访问http://localhost:8080同样能看到 phpinfo 页面。日常开发中常用的相关命令我也一并列出来# 查看容器日志 docker compose logs -f # 进入 PHP 容器操作比如执行 php -v docker compose exec php sh # 重新构建镜像修改了 Dockerfile 后执行 docker compose up -d --build # 停止并删除容器不会删除代码目录和命名卷 docker compose down停掉再启动是开发时最频繁的操作之一。改完 nginx 配置文件要让它生效可以执行docker compose exec nginx nginx -s reload用 Nginx 自带的平滑重载指令不用整个容器重启。4.4 挂载路径一致性的一个细节Compose 把代码目录挂载到了 Nginx 容器的/var/www/html也挂载到了 PHP 容器的/var/www/htmlNginx 配置里的root也指向/var/www/html。三个路径必须保持一致是部署成功的关键。为什么是三个而不只是两个因为 Nginx 配置里SCRIPT_FILENAME的$document_root取自root指令。如果 Nginx root 指向/usr/share/nginx/html而它的/var/www/html也挂着同一份代码那么 PHP-FPM 容器里找/usr/share/nginx/html/index.php就会扑空。很多人在手工 docker run 时随手加了一句-v ~/www:/usr/share/nginx/html导致 Nginx 容器里文件是存在的可配置的 root 路径和它不一致问题就出现了。所以我的建议很直接所有容器挂载同一目录时尽可能用相同的容器内路径Nginx 的 root 也写这个路径不要自作聪明地分开放。5. 开发中最高频的 5 类问题与排查实录5.1 80 端口和 8080 端口被占用本地开发最容易遇到的就是端口占用。macOS 上 80 端口经常被系统自带的 Apache 或其他服务占用8080 也可能被某个前端开发服务器占着。碰到这种情况先别急着猜用系统自带命令查占用情况lsof -i :8080如果返回了进程信息就说明端口确实被占了。最简单的处理方式是改映射端口把 compose 文件里的映射改成其他端口比如ports: - 8081:80然后重新docker compose up -d。不推荐去强行关闭占用进程容易被误伤。保留原始容器内 80 端口不变只改宿主机侧的映射对容器内部无感知。5.2 502 Bad Gateway为什么 Nginx 找不到 PHP-FPM502 是部署 Nginx PHP-FPM 时最常见的报错含义是 Nginx 把请求转发到了某个目标但目标服务没有应答。排查方向就围绕目标到底通不通展开。先确认 PHP 容器里 PHP-FPM 进程是否在运行docker compose exec php ps aux | grep php-fpm能看到主进程和多个 worker 进程说明服务本身没挂。接着确认 Nginx 容器能否解析到 php 这个主机名docker compose exec nginx getent hosts php如果这一步没有输出对应的 IP说明网络配置有问题比如两个容器不在同一个网络里。用docker compose inspect php看 php 服务的 Networks 部分再用同样方法看 nginx 服务的 Networks确保两者都挂载了 app-net。通常 502 的根源就是网络不在同一张网里或者 php-fpm 进程崩溃了。网络配置正确但进程不在则重新启动 php 容器。5.3 访问 PHP 文件变成下载或者直接空白请求一个 .php 文件Nginx 返回空白或者浏览器把它当成文件下载说明 Nginx 根本没有把请求转交给 PHP-FPM而是当作普通静态文件处理了。原因基本集中在 Nginx 的 location 正则匹配上。检查location ~ \.php$这一行是否写对。容易犯的错是写成了location \.php$少了不区分大小写的~修饰符或漏掉了结尾的$。另外检查是否真的加载到了这份配置。我遇到过配置文件放错目录的情况放在/etc/nginx/的nginx.conf里而不是conf.d目录下导致整份站点配置从未生效。加载配置文件的地方用docker compose exec nginx nginx -t这条指令会检查配置文件的语法并打印出实际读取了哪些配置文件是排查配置问题的第一利器。5.4 改了代码不生效文件不同步或被权限卡住代码改了但浏览器里没变化最直接的原因是文件根本没有同步进容器。先用一条命令验证docker compose exec nginx ls -l /var/www/html看看容器内的文件时间戳是不是最新的。如果不是检查挂载路径是否写对。macOS 上 Docker Desktop 的文件共享本身有极小的延迟但正常情况下秒级就能同步。发现迟迟不同步就重启一下 Docker Desktop 的文件共享服务或者在 Docker Desktop 设置里检查文件共享目录是否包含了项目路径。另一种常见原因是 PHP 的 opcache 缓存了旧代码。开发环境建议在 PHP 容器的配置中关闭 opcache或者启用自动重载docker compose exec php php -m | grep opcache如果看到 opcache需要在Dockerfile或容器配置里设置opcache.validate_timestamps1并且opcache.revalidate_freq0。这样每次文件变化PHP 都会重新加载。有些框架自己也会缓存配置比如 Laravel 的bootstrap/cache目录改了.env后需要php artisan config:clear。这类框架缓存问题和 PHP 运行环境无关但也常让人误以为 Docker 坏了。5.5 Docker Desktop 启动慢、电脑发热、磁盘占用高M 系列芯片的机器跑 Docker Desktop正常情况下性能是够用的但有些场景会明显卡顿。最典型的是把整个项目目录直接挂载进容器然后项目里又有几万个依赖文件比如node_modules、vendor。macOS 上的文件挂载走的是虚拟机文件共享通道海量小文件场景性能会明显下降。对策是分清哪些需要实时同步哪些不用。代码目录用挂载依赖目录则尽量放进镜像或者使用命名卷。比如在 Compose 里为 vendor 目录单独建一个命名卷services: php: volumes: - ./www:/var/www/html - php-vendor:/var/www/html/vendor volumes: php-vendor:这样 vendor 不会落到宿主机文件系统里读写性能由 Docker 的卷存储管理比直挂目录快不少。代价是执行 composer install 时容器内已经有了 vendor宿主机项目目录里没有代码提交时需要注意.gitignore策略。电脑发热和磁盘占用通常是因为 Docker Desktop 里堆积了大量不再使用的镜像、容器、日志。我是按月清理一次先查看占用docker system df看到悬空镜像和无用缓存执行docker system prune -af这条命令会删除所有未使用的镜像、容器和网络磁盘空间能明显回收。执行前想清楚开发环境里跑着的容器不受影响。6. 再扩展一层定制 PHP 镜像和加入 MySQL6.1 为什么需要定制的 PHP 镜像官方php:8.3-fpm-alpine镜像只包含最基础的 PHP 环境很多常用的扩展没有预装。实际项目里几乎必然会用到 PDO、pdo_mysql可能还需要 gd、redis、zip 等。定制镜像是通过编写 Dockerfile 实现的。在项目根目录建一个php/DockerfileFROM php:8.3-fpm-alpine RUN docker-php-ext-install pdo pdo_mysql mysqli \ docker-php-ext-install opcache COPY ./php/conf.d/opcache.ini /usr/local/etc/php/conf.d/opcache.inidocker-php-ext-install是 PHP 官方镜像自带的扩展安装脚本一条命令就能编译并安装扩展。如果需要 gd 这类依赖外部库的扩展得先用apk add安装系统依赖再执行安装这里不展开了按项目需求逐步加即可。然后修改 Compose 中 php 服务php: build: context: . dockerfile: php/Dockerfile image: php-nginx-demo-php container_name: demo-php volumes: - ./www:/var/www/html networks: - app-netbuild指令告诉 Compose 使用 Dockerfile 构建镜像而不是从远程拉取。image字段则给构建出来的镜像一个本地标签名。启动命令改成docker compose up -d --build之后每次修改 Dockerfile都执行带--build的命令重新构建。6.2 加入 MySQL 服务项目要跑动态数据就绕不开数据库。在 Compose 中增加一个 MySQL 服务mysql: image: mysql:8.4 container_name: demo-mysql environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: demo MYSQL_USER: demo MYSQL_PASSWORD: demo123 ports: - 3306:3306 volumes: - mysql-data:/var/lib/mysql networks: - app-net volumes: mysql-data:注意几个点MYSQL_ROOT_PASSWORD是初始化时设定的 root 密码只在首次创建数据卷时生效。开发环境用简单密码可以生产环境绝对不行。mysql-data是一个命名卷数据库文件不会随着容器删除而丢失。容器内的 MySQL 会监听 3306 端口。ports映射后宿主机上的数据库客户端也能通过 localhost:3306 连接。关键点PHP 容器连接数据库时主机名要写mysql不能写localhost。因为 localhost 在 PHP 容器内部指向它自己而实际的 MySQL 服务是在另一个容器里DNS 解析mysql才能找到它。在 PHP 代码里数据库连接配置会是类似这样$pdo new PDO(mysql:hostmysql;dbnamedemo;port3306, demo, demo123);这个连接池的问题和早期手工跑的时候 Nginx 容器内部访问 PHP-FPM 不能写 localhost 是同一种逻辑只是现在把这条道理应用到了数据库上。6.3 常用扩展与初始化数据的经验在开发阶段我通常会把比较固定的扩展提前固化在 Dockerfile 里比如 pdo、pdo_mysql、opcache、redis避免每次容器重建都手动编译。对国际化项目还会加 intl、bcmath。MySQL 初始化数据的做法我推荐把初始化 SQL 脚本放到mysql/init目录并在 Compose 中挂载volumes: - ./mysql/init:/docker-entrypoint-initdb.d镜像第一次启动数据卷为空时会自动执行这个目录下的 SQL 脚本完成建表、导入初始数据。注意只有首次初始化会执行数据卷已存在后脚本不会再次运行。需要重置时执行docker compose down -v删除数据卷再重新 up。这个-v参数会连命名卷一起删除是个危险的命令确定不要数据才用。最后说点实际操作中的体会这套环境我前后搭过不止一次最大的感受是Docker 方案的复杂性集中在最开始一旦把标准目录结构和 Compose 文件定下来后面就变成机械操作。每次在新电脑上重建环境时我最常忘的就是 Nginx 的 root 路径必须和代码挂载路径一致这个点我建议所有人都先在配置文件里反复确认能省下大量排错时间。还有一个小技巧开发时尽量用docker compose exec nginx nginx -s reload做 Nginx 配置热加载比反复重启容器快得多。如果你也是 macOS 上的 PHP 开发者建议从手工 docker run 跑通一次再写 Compose理解会扎实很多。这套流程跑顺后后续加 Redis、加队列、加定时任务都是在同一个模板上向外扩展而已。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表