
最近在macOS上折腾Docker目标很明确把NginxPHP这套经典组合放到容器里跑通。折腾完发现这类教程网上虽然不少但很多只给命令不讲为什么遇到502和404容易一脸懵。这篇文章把从安装Docker到最终访问PHP页面的全过程写一遍把我踩过的坑、改过的配置、排查问题的顺序都记录下来适合刚接触容器、想在本地快速跑起PHP环境的开发者参考。1. 安装Docker先确认芯片型号再动手macOS上跑Docker和Linux上有本质区别。Linux本身支持容器macOS内核不支持Linux容器所以必须借助一层轻量虚拟机。Docker Desktop在macOS上就是做了这件事后台跑一个Linux虚拟机所有容器都在这台虚拟机里运行。你可以把容器理解成标准化的货柜而Linux内核是码头装卸设备macOS没有这套设备只能先虚拟一个码头出来。这个底层机制搞清楚之后后面遇到文件挂载、资源占用这些问题就比较好理解了。安装前第一件事是先确认自己这台Mac的芯片型号。命令行执行uname -m输出arm64表示Apple SiliconM系列芯片输出x86_64表示Intel芯片。这个信息在后面拉镜像、遇到平台兼容问题时很关键。普通用户直接装Docker Desktop一般不需要手动指定架构Docker会自动根据当前架构拉取对应平台的镜像但偶尔会碰到某些老镜像只有x86_64版本。Apple Silicon机器上可以加platform: linux/amd64强制跑代价是性能差一些本地调试勉强能用。提前知道这个知识点遇到报错时就不会一头雾水。1.1 用Docker Desktop安装还是用轻量工具新手我推荐直接用Docker Desktop。原因很直接安装简单自带图形界面网络、磁盘、资源占用都能可视化调整对刚接触容器的人最友好。安装包从官方页面下载dmg双击拖进Applications即可。第一次启动会提示允许一些扩展权限需要输入本机密码这个不是异常直接确认。如果不想用图形界面或者觉得Docker Desktop太重也可以考虑Colima这类轻量方案。Colima在命令行里启动一个Linux虚拟机和Docker CLI配合使用。优点是资源占用小、命令可控缺点是对不熟悉命令行的人不友好而且配置网络端口时偶尔需要额外参数。我的建议是本地学习阶段先用Docker Desktop跑通等理解了容器基本概念再考虑轻量方案没必要一开始就给自己增加心智负担。安装完之后打开Docker Desktop等右下角图标变成稳定状态再在终端验证docker --version docker compose version如果两个命令都能输出版本号说明安装成功。还可以跑一个最小镜像确认整个链路是通的docker run --rm hello-world这个镜像体积很小输出一段欢迎信息就退出了。这一步能同时验证客户端、守护进程、镜像拉取这几个核心环节是否正常。我第一次装完跑这一步的时候卡了很久后来发现是虚拟机还没完全启动多等几秒再试就好了。1.2 镜像版本选择为什么推荐alpine项目里要用Nginx和PHP的官方镜像具体标签有很多种。我推荐Nginx用nginx:stable-alpinePHP用php:8.2-fpm-alpine。alpine是种极简Linux发行版体积小镜像通常只有几十兆拉取快、占用磁盘少很适合本地调试。Debian系镜像功能更全但体积动辄几百兆本地起个环境完全没必要用那么重的东西。选php:8.2-fpm-alpine而不是php:8.2-apache也很讲究。前者只带PHP-FPM不包含Web服务器正好和Nginx搭配后者自带Apache如果你不需要Apache就别用。项目里如果只是跑PHP脚本、不接数据库直接拿官方镜像就能用如果以后要装PDO、mysqli这类扩展就需要自己基于官方镜像写一个Dockerfile。这个后面的章节会详细说。2. 先把项目目录和配置规划好很多新手学Docker时习惯在一堆命令里直接docker run拼参数容器一多就变成一坨看不懂的咒语。我经历过这些之后强烈建议用docker-compose来管理。它的核心价值是把多个容器的启动参数写进一个yaml文件一条命令启动、一条命令停止换台机器也能复现同样的环境。2.1 一个干净到能直接抄作业的目录结构在动手写配置前先在本地建一个项目目录。我的习惯是web/ ├── docker-compose.yml ├── conf/ │ └── nginx/ │ └── default.conf └── www/ └── index.phpconf放Nginx相关配置www放PHP代码文件docker-compose.yml放容器编排配置。为什么刻意把配置和代码分开因为后面Nginx容器需要把配置文件挂载进去如果把配置放在代码目录里混着修改一个文件就会牵动整个项目结构后期维护很麻烦。分开之后代码目录干干净净配置变更也能单独追踪。代码文件在www/index.php里随便放一段测试内容?php echo hello, nginx php in docker;先不放phpinfo()这种大页面等下排查问题时会更清晰。等确认能跑通了再换成phpinfo()验证扩展和环境变量也不迟。2.2 镜像选型的三个判断标准镜像怎么选我总结成三点。第一看基础系统alpine精简、debian万全本地优先alpine。第二看进程模型Nginx要选官方nginx镜像PHP不能选php:apache那种自带Web服务器的而选php:fpm或php:fpm-alpine。第三看版本要用自己熟悉的PHP版本比如项目依赖老语法就选php:7.4或php:8.0没有特殊要求就选8.2稳定且社区资料多。这里还有个容易忽略的点不要一看到“官方”两个字就以为镜像里什么都有。PHP官方镜像默认只带很基础的扩展数据库驱动、GD库这些大部分都没有。后面如果发现php -m里缺扩展不要慌这是默认状态需要自己加。这部分我会在3.4小节给出一个可以直接用的Dockerfile。2.3 nginx为什么必须搭配php-fpmNginx本身不执行PHP代码它收到一个.php请求时只会把它当作一个普通文件要么返回内容要么404。要让Nginx把PHP请求交给真正的解析器就得靠FastCGI协议。PHP-FPM就是PHP的FastCGI进程管理器它监听一个端口默认9000等待Nginx把请求转发过来然后解析PHP并把结果原路返回。用生活场景打比方Nginx是前厅接待负责收请求、处理静态文件这些“迎客”的事PHP-FPM是后厨负责真正“做菜”也就是执行PHP脚本。两者之间通过一条固定的传菜窗口通信这个窗口就是端口9000。在Docker编排里Nginx容器和PHP容器需要处于同一个容器网络里Nginx才能访问到php:9000这个地址。这就是为什么compose里要显式声明网络而不是简单把两个容器跑起来就完事。3. 用docker-compose把两个容器编排起来3.1 docker-compose.yml逐段拆解直接在项目根目录创建docker-compose.ymlservices: nginx: image: nginx:stable-alpine container_name: web-nginx ports: - 8080:80 volumes: - ./www:/var/www/html:ro - ./conf/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro depends_on: - php networks: - webnet php: image: php:8.2-fpm-alpine container_name: web-php volumes: - ./www:/var/www/html networks: - webnet networks: webnet: driver: bridge不用写顶部version字段新版docker compose已经忽略它了写了反而容易误导。services下面定义两个服务nginx和php。nginx里image指定用哪个镜像container_name给容器起名ports把宿主机8080端口映射到容器内80端口。为什么宿主机不直接用80因为macOS上很多软件会占用80端口或者权限限制导致绑定失败。用8080这种高位端口能避开绝大多数冲突。volumes这段是精华。./www:/var/www/html:ro把宿主机当前目录下的www目录挂载进Nginx容器内的/var/www/html只读./conf/nginx/default.conf挂载为Nginx的default.conf只读。PHP容器也把./www挂载进/var/www/html但没加:ro因为PHP-FPM可能要写文件。两个容器共享同一个宿主机目录Nginx才能把请求转给PHP后正确找到对应的脚本文件。3.2 先搞清楚depends_on的局限性depends_on: - php表示Nginx容器会在PHP容器启动之后才启动。但注意它只管启动顺序不管服务是否已经准备好接收请求。PHP-FPM容器的9000端口可能要过几秒才真正就绪这时候Nginx可能已经启动、尝试连接php:9000但失败。表现在浏览器上通常是短暂的502过几秒刷新就好了。如果希望避免这个“启动瞬间的502”可以给PHP容器加一个健康检查让Nginx等待它健康后再启动。compose标准里有healthcheck和condition: service_healthy的写法但对于本地开发环境我一般不折腾这层毕竟只是启动那几秒的事。理解了“depends_on不保证就绪”这个坑比单纯抄配置更值钱。3.3 nginx配置文件fastcgi_pass是关键在conf/nginx/default.conf里写一套最精简的可运行配置server { 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 php:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }listen 80对应容器内的80端口宿主机的8080映射到这里。root /var/www/html必须是两个容器共享的那个目录在Nginx容器里的路径。如果这里的路径和实际挂载路径对不上最常见的表现是访问首页出现404而nginx容器里文件确实存在。最重要的一行是fastcgi_pass php:9000。这个php不是域名是compose里的服务名。Compose会在默认网络里为每个服务生成一个DNS记录所以Nginx容器可以直接用服务名访问PHP容器。如果不用compose、手动用docker run这里就要写成容器的IP或者加了--link的别名比较麻烦这也是我推荐compose的原因之一。SCRIPT_FILENAME这个参数也很关键。Nginx要把请求转发给PHP-FPM时需要告诉PHP-FPM“你要解析的文件完整路径是什么”。$document_root$fastcgi_script_name组合起来就是容器内的/var/www/html/index.php。以前常见错误是只写$fastcgi_script_name结果路径不完整PHP-FPM找不到文件返回404或白屏。3.4 PHP容器加扩展一个简单的Dockerfile直接用官方php:8.2-fpm-alpine镜像能跑纯PHP但真实项目几乎都要接MySQL、连Redis至少PDO扩展是少不了的。这时候最好的做法不是手动进容器里装而是写一个Dockerfile把扩展固化下来。在项目目录下建docker/php/DockerfileFROM php:8.2-fpm-alpine RUN apk add --no-cache tzdata \ docker-php-ext-install pdo_mysql mysqli ENV TZAsia/Shanghai然后用build替代imagephp: build: ./docker/php container_name: web-php volumes: - ./www:/var/www/html networks: - webnetdocker-php-ext-install是PHP官方镜像里自带的脚本专门用来编译安装扩展。apk add tzdata是为了处理容器时区问题默认容器是UTC时间如果PHP业务里用到date()函数会发现比北京时间少8小时。加了ENV TZAsia/Shanghai后新起的PHP进程就会读取这个时区。注意改了Dockerfile后要重新构建docker compose build php docker compose up -d如果只是想在本地快速验证不接数据库也可以完全跳过Dockerfile直接用官方镜像跑。扩展方案遇到再说不要一步到位把配置搞复杂。4. 从启动到验证完整走一遍4.1 启动容器并确认状态配置都写好后启动就特别简单cd web docker compose up -d docker compose psup -d会在后台启动所有服务。docker compose ps能看到容器运行状态如果STATUS是Up说明启动了。Nginx容器会显示端口映射0.0.0.0:8080-80/tcp。这个输出信息很有用后面排查有没有绑定成功都靠它。然后访问首页curl http://localhost:8080如果返回了我们写的hello, nginx php in docker说明整个链路已经通了。用浏览器打开也一样。第一次跑通的时候会有点成就感因为这中间Nginx、PHP-FPM、容器网络三个环节任何一个出错结果都不是这个。4.2 如果出现502按这个顺序排查本地搭建最常见的错误就是502。出现502时先从日志看docker compose logs nginx日志里如果出现connect() failed (111: Connection refused) while connecting to upstream说明Nginx容器连接php:9000失败。接下来依次确认三件事一PHP容器是否在运行docker compose ps看到web-php状态二compose网络里服务名是不是php和fastcgi_pass php:9000是否一致三PHP-FPM到底有没有监听9000端口。第三点很多人会忽略可以在PHP容器里直接验证docker compose exec php sh -c netstat -tuln | grep 9000有些精简镜像可能没装netstat也可以用ss -tuln或者php-fpm -t测试配置语法。如果9000端口没有监听多半是PHP-FPM启动失败这时看PHP容器日志docker compose logs php日志里会给出具体错误比如配置文件路径不对、用户不存在等。按提示处理即可。4.3 如果出现404或直接下载PHP文件404通常不是网络问题而是文件路径问题。确认一下Nginx容器里能不能看到代码文件docker compose exec nginx ls -l /var/www/html如果看不到index.php说明挂载有问题重新检查compose里的volumes映射。如果能看到文件但还是404重点看root路径和index指令没有index index.php时Nginx不会自动把请求交给PHP索引文件。还有一种现象是浏览器直接下载.php文件而不是解析这说明Nginx根本没用location ~ \.php$那段配置处理PHP请求请求被当成普通静态文件返回了。检查一下挂载的配置文件是否真的生效可以在Nginx容器里docker compose exec nginx cat /etc/nginx/conf.d/default.conf如果看到的内容和你本地的default.conf不一致说明容器里的配置文件没有更新需要重启Nginx容器。4.4 用phpinfo验证环境细节能访问到PHP内容后可以把index.php临时改成?php phpinfo();再刷新页面就能看到PHP版本、配置项、已加载模块等一整套信息。重点看两个地方PHP Version确认版本和镜像里一致Server API显示FPM/FastCGI说明PHP确实是由PHP-FPM解析的。phpinfo()信息量很大本地调试用起来特别方便但生产环境绝对不要暴露给外部访问这点要养成习惯。想看PHP扩展也可以直接进容器docker compose exec php php -m这个命令会列出所有已安装模块比如PDO、pdo_mysql、mysqli如果装了就都会在里面。整个排查链路走下来基本覆盖了这类组合环境90%的启动问题。5. 常见问题与避坑速查整理了一份常见问题表按现象直接查比反复看日志省时间现象可能原因处理方式启动报端口被占用8080被宿主机其他进程占用改compose里8080:80为8081:80502 Bad GatewayNginx连不上php:9000检查PHP容器状态、服务名、9000端口监听404 Not Found根路径或文件名不匹配检查root路径、挂载和index指令PHP文件被下载没有走location ~ \.php$检查nginx配置是否生效修改代码后不生效本地文件和容器内路径不一致docker compose exec nginx ls确认挂载时区差8小时容器默认UTC在Dockerfile装tzdata并设置TZ5.1 端口被占用别硬刚换个端口macOS上“端口被占用”几乎是必踩的坑。我遇到过好多次不是系统服务占用了80就是别的项目占用了8080。改端口不是丢人的事本地开发本来就该避开常用端口。把compose里的ports改成8081:80再docker compose up -d重建就好了。不用改Nginx里的listen 80那是容器内部端口宿主机端口只由ports这行控制。5.2 docker compose down到底删了什么docker compose down会停止并删除容器但不会被删除挂载的宿主机目录所以代码文件不会丢。加了-v之后会连匿名卷一起删如果数据放在命名卷或宿主机目录里一般也不受影响。但我在早期不熟悉时有一次执行down -v把数据库容器里的数据卷删了当场懵掉。这里我的建议是本地项目里凡是需要持久化的数据都明确挂载到宿主机目录比如./data:/var/lib/mysql永远不要把数据只放在容器里。容器是随时可以重建的数据不是。5.3 Docker Desktop资源占用和清理Docker Desktop默认分配的资源和本机配置有关跑两个容器可能很轻但如果同时开很多容器会吃内存。打开Docker Desktop设置可以调高CPU和内存限制调完需要重启Docker服务。另外镜像越拉越多磁盘会被撑满。清理时我用docker system prune清理悬空镜像、停止的容器和无用资源会提示确认。这个命令会删除停止的容器和悬空的镜像谨慎使用但磁盘吃紧时真的管用。6. 一点配置心得和后续扩展6.1 为什么这套组合适合本地开发很多人觉得在本地装Docker麻烦不如直接装个Nginx、PHP。我个人的体会是Docker方案最大的价值不在“安装”而在“一致性”。一份docker-compose.yml放进项目仓库不管换电脑还是换同事只要装了Docker一条命令就能复现同一个环境。再也不用为“我本机是PHP 7.4你本机是8.2跑出来结果不一样”这种问题浪费时间。如果你经常做PHP相关开发这几乎是最省心的本地环境方案。6.2 在这个基础上还能加什么跑通NginxPHP只是第一步。后续最自然的扩展是加数据库和缓存服务在compose里再加一个mysql和redis服务PHP容器通过安装对应扩展连接它们整个架构就和真实项目非常接近了。再往后可以把PHP项目的代码通过Dockerfile直接打包进镜像彻底摆脱源码目录挂载变成一套可交付的环境。举个例子加MySQL服务时compose里新增一个服务指定镜像、环境变量、数据卷再把PHP容器挂到同一个webnet网络里。PHP代码里连接数据库的主机地址直接写服务名而不是127.0.0.1因为容器之间不能通过localhost互相访问。这类扩展并不难但需要理解容器网络的基本逻辑。最后分享一个我自己的小习惯每配置完一个服务都会单独验证这个服务是否可用而不是等所有配置写完才启动。比如先只启动PHP容器确认php -m正常再启动Nginx容器确认静态文件可访问最后把两者连起来验证动态解析。分段验证能让问题在最简单的上下文里暴露排查起来轻松很多。这套组合跑通之后后面不管加MySQL还是Redis我心里都有底了。