news 2026/10/17 9:26:25

macOS上用Docker部署nginx+php:从零搭建容器化开发环境

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
macOS上用Docker部署nginx+php:从零搭建容器化开发环境

你在macOS上装过nginx和php吗?如果装过,大概率体会过那种“装起来半小时、调起来一晚上”的滋味。今天这篇我就带你完整走一遍macOS上安装Docker,然后在Docker里部署一套nginx+php环境的过程,整个过程不追求什么高大上的编排技巧,但保证每一步都能落地、都能复现,尤其是那些只有实际踩过坑才知道的细节,我会一一说出来。

这套东西适合谁?如果你是前端开发,想本地联调PHP接口;如果你是刚入门的后端,想在不污染本机的情况下快速起一套LNMP环境;或者你只是想把本地开发环境做得跟线上服务器尽量一致,那这篇内容就是给你准备的。看完之后,你不仅能跑起来一个nginx+php的容器组合,还能理解它们之间是怎么通信的,以后自己加扩展、改配置、排故障,都不至于两眼一抹黑。

1. 先从“为什么”说起:macOS 上为什么要用 Docker 跑 nginx + php

1.1 本地开发环境的一团乱麻

我相信很多人都经历过这样一个阶段:拿到一台新Mac,想着装个nginx、装个php,结果一搜教程,各种brew命令、各种配置文件路径,装完还得改php.ini、改nginx.conf,好不容易跑起来,过几天又要装个扩展,把php又搞崩了,最后干脆重装系统。

这不是你笨,而是传统方式装出来的环境本来就很难维护。nginx、php、MySQL各自依赖不同的动态库,php版本和扩展之间还有兼容性问题,你系统里可能还残留着旧版本的开机启动项。再加上公司项目和自己的项目可能需要不同版本的php,这种“一个系统跑一套固定环境”的模式天然就捉襟见肘。

我之前有段时间就是被这种事情反复折磨。后来换了个思路:本地环境全部容器化,本机只装一个Docker,剩下的事情全部交给容器。系统干净了,项目环境也互不干扰了。这大概是你在macOS上折腾开发环境时,最值得做的一个决定。

1.2 Docker 恰恰解决的是“环境一致性”

Docker的本质用一个生活类比最好懂:它就是一个标准化的集装箱。开发者把应用和它需要的所有依赖(比如php、nginx、扩展、配置)打包进一个镜像,就像货物被装进集装箱,不管运到哪,装上船、卸下船、放到仓库,里面的东西和环境都不变。

以前你说“我这代码在我机器上能跑”,现在你说“这套镜像在任何机器上都能跑”,这就是环境一致性。对应到nginx+php场景里,本机不用装nginx也不用装php,只需要跑两个容器:一个容器负责跑nginx,一个容器负责跑php-fpm。nginx收到请求后,遇到PHP文件就转交给php容器处理,php处理完把结果返回给nginx,再由nginx返回给浏览器。

这个架构本身并不新,但Docker让它变得特别容易在本地复现。你不用关心怎么在不同系统上编译安装软件,只需要维护好一份docker-compose.yml和几个配置文件,以后换电脑、同步给同事,复制过去就能用。

2. 开工前的准备:Docker 安装与基础配置

2.1 检查你的 Mac:芯片架构与系统要求

在装Docker之前,先看一眼你的Mac是Apple Silicon还是Intel芯片。点左上角苹果图标,选“关于本机”,能看到“芯片”这一栏,如果写着Apple M1/M2/M3这类字样,就是Apple Silicon;如果写着Intel,就是传统的Intel芯片。

这个区别在下载Docker Desktop的时候要用到。虽然现在Docker Desktop官网下载页会自动识别你的芯片,但如果你去第三方渠道找安装包,一定要分清楚x86_64和arm64版本。Apple Silicon的机器不要下Intel版本,因为需要通过模拟层运行,性能和兼容性都会打折;反过来,Intel的机器也没法直接跑arm64的镜像。

系统版本方面,建议macOS 12或更高版本。版本太老的话,Docker Desktop新版可能跑不起来。另外提一句,如果你的机器配置比较低,8GB内存以下,跑Docker Desktop会比较吃力,因为Docker本身要占用一部分资源,后面我会讲怎么限制它的资源占用。

2.2 Docker Desktop 的安装细节与首启配置

安装Docker最主流的方式就是去官网下载Docker Desktop安装包,这里我不推荐去搞什么命令行brew安装,因为桌面版自带图形化管理界面、文件共享配置、资源限制面板,对新手上手最友好。下载完成后,把Docker.app拖进“应用程序”文件夹,然后双击启动。

首次启动会弹窗请求权限,输入你的Mac密码安装辅助组件。之后菜单栏右上角会出现一个小鲸鱼图标,点一下能看到Docker Desktop的启动状态。等鲸鱼图标不再闪动,进入“About”面板能看到版本信息,说明Docker引擎已经就绪。

打开终端,运行下面两个命令确认环境正常:

docker --version docker compose version

能看到类似Docker version 27.x.x和Docker Compose version v2.x.x的输出,说明安装成功。如果你发现docker命令找不到,大概率是Docker Desktop没启动,或者PATH没配置好。Docker Desktop正常安装后会自动把docker命令写进PATH,不需要手动改。

2.3 镜像加速器要不要配?

国内网络环境拉Docker Hub镜像,懂的都懂,经常慢到怀疑人生。Docker Desktop里提供了一个镜像加速配置入口,路径是:Docker Desktop → Settings → Docker Engine,然后在JSON配置里加上registry-mirrors字段。

一个常见的配置长这样:

{ "registry-mirrors": [ "https://docker.mirrors.ustc.edu.cn", "https://hub-mirror.c.163.com" ] }

配置完点“Apply & Restart”,Docker引擎会重启,再拉镜像时就会走加速源了。不过这里要提醒一句:公共加速源有时候也不稳定,你本地多试几个源,哪个快用哪个。如果某个加速源挂了,就把registry-mirrors暂时清空,直接走官方源反而更稳。

这个配置属于“没配置也能用,配置了更舒服”的类型。不配置的话,小镜像还能忍受,像php:8.3-fpm-alpine这种几百MB的镜像,你可能就得泡杯茶等它下完了。

3. 先想清楚再动手:nginx + php 架构与目录规划

3.1 nginx 和 php-fpm 到底是怎么配合的

很多人第一次部署nginx+php,会下意识地以为需要在同一个容器里既装nginx又装php。实际上生产环境和你本地最推荐的做法,都是两个独立容器,各干各的。

nginx本身只处理静态资源和请求转发。当nginx收到一个请求,发现访问的是.php结尾的文件,它自己不会解析,而是通过FastCGI协议把请求扔给php-fpm处理。php-fpm是PHP的进程管理器,它监听一个端口(默认9000),收到nginx扔过来的请求后,找到对应PHP文件,用PHP解释器执行,把结果返回给nginx,nginx再拼上HTTP响应头返回给浏览器。

这个协作过程用大白话讲,nginx像餐厅前台,php-fpm像后厨。前台收下点菜单(HTTP请求),如果是现成的菜(静态文件),直接端给你;如果是需要现做的菜(PHP动态请求),把单子传给后厨,等后厨做好了再端出来。前厅和后厨是两个独立房间,中间通过一个传菜口连接,这个传菜口就是FastCGI协议。

在Docker里,这两个“房间”就是两个容器。它们之间不需要通过端口映射来通信,因为在同一个compose网络里,容器之间直接用服务名就能互相访问。比如nginx容器要访问php容器,配置里写fastcgi_pass php:9000,这里的php就是compose服务名,Docker会自动解析成php容器的IP地址。这是初学者最容易卡住的地方。

3.2 推荐目录结构与镜像选型

动手写文件之前,先建一个项目目录,我习惯放在~/docker-nginx-php下,后续所有文件都放这里。目录结构规划好,后面维护起来非常舒服:

docker-nginx-php/ ├── docker-compose.yml ├── nginx/ │ └── conf.d/ │ └── default.conf ├── php/ │ └── Dockerfile └── src/ └── index.php

src目录是你项目的代码根目录。nginx和php容器都要访问这个目录,所以它会被挂载到两个容器里去。这样本地改代码,容器里立刻同步,不用重新构建镜像。

镜像选型方面,我的建议是:

  • nginx用nginx:1.27-alpine,Alpine版本体积小、内存占用低,适合开发环境;
  • php用php:8.3-fpm-alpine,自带php-fpm,Alpine版本同样小巧。

为什么不直接用官方默认的nginx和php镜像?因为默认镜像基于Debian,体积大不少,而Alpine版精简了大量用不上的组件,两个镜像加起来能小一半。这里刻意选比较新的版本号,是为了让你拿到手就能用上较新的PHP特性和安全补丁。如果你项目有固定的PHP版本要求(比如公司项目强制PHP 7.4),把数字改成对应版本号就好,其他部分不用动。

4. 写出你的第一份 Docker Compose 编排文件

4.1 compose 文件逐行解读

现在开始写核心文件。第一个是docker-compose.yml,我用的是Compose V2的写法,不需要version字段,直接声明services就行:

services: nginx: image: nginx:1.27-alpine container_name: nginx-web ports: - "8080:80" volumes: - ./src:/var/www/html - ./nginx/conf.d:/etc/nginx/conf.d depends_on: - php networks: - lnmp php: image: php:8.3-fpm-alpine container_name: php-fpm volumes: - ./src:/var/www/html networks: - lnmp networks: lnmp: driver: bridge

逐行解释一下关键配置。ports: "8080:80"的意思是宿主机8080端口映射到容器内80端口,这样浏览器访问http://localhost:8080时,请求会进入nginx容器。为什么用8080而非80?因为macOS上80端口经常被系统自带的Apache或者其他服务占用,一上来就绑80容易报错,先用8080跑通再说。

volumes挂载需要特别注意:nginx容器挂载了./src到/var/www/html,也挂载了./nginx/conf.d到/etc/nginx/conf.d。后者是nginx容器读取站点配置的地方,我们把宿主机上的default.conf挂进去,就不用进入容器内部改配置了,改完重启一下nginx容器就能生效。

depends_on: php只是告诉compose先启动php容器再启动nginx容器,这是为了防一手nginx启动时php还没就绪,导致第一次连接失败。

4.2 nginx 站点配置:fastcgi_pass 是关键

接下来写nginx/conf.d/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; } }

最重要的地方是fastcgi_pass php:9000。php是docker-compose.yml里的服务名,前面说过,同一个自定义网络里,容器可以直接通过服务名互相访问。如果你不用compose、改用docker run手动起容器,这个位置就得写容器的IP地址,但那很不方便,重启容器IP可能就变了,所以这里用compose网络是省心做法。

fastcgi_param SCRIPT_FILENAME这行同样容易踩坑,它告诉php-fpm要执行哪个PHP文件。$document_root对应上面的root /var/www/html,$fastcgi_script_name是请求的PHP文件名。如果漏掉这个参数,或者路径跟php容器里的挂载路径对不上,php-fpm就会报文件找不到,nginx返回404。

4.3 单独为 php 加一个 Dockerfile

php/Dockerfile里目前只做一件事:安装PHP扩展。如果你不需要连MySQL,这个文件甚至可以不用,直接依赖compose里的php:8.3-fpm-alpine镜像就能跑。但一个典型的PHP项目基本都要连MySQL,所以我提前把扩展装上:

FROM php:8.3-fpm-alpine RUN docker-php-ext-install pdo_mysql mysqli

基础镜像里自带了一个docker-php-ext-install脚本,专门用来编译安装PHP扩展。比如pdo_mysql是PDO连接MySQL的驱动,mysqli是老式MySQL扩展,按需安装。如果你还要装redis、zip这类扩展,推荐先查一下对应的安装方式,有些需要额外依赖库,不能一句命令搞定。

写完之后先别急着启动,先把src/index.php建好,写个探针页面验证环境:

<?php phpinfo();

这个页面会展示PHP版本、已加载的扩展、配置路径等信息。等整个环境跑起来后,访问这个页面就能确认php-fpm是否正常解析了PHP文件。

5. 启动、验证与第一次踩坑

5.1 一键启动与环境验证

来到项目目录,打开终端执行:

docker compose up -d

-d表示后台运行。第一次执行会先拉取两个镜像,时间取决于网络。镜像拉完之后,终端会出现两个容器的启动状态。执行docker ps就能看到两个容器都在运行,状态为Up。

然后打开浏览器访问http://localhost:8080,如果看到phpinfo页面,说明整条链路已经通了。这里可能有人会问,为什么访问的是index.php而不是phpinfo?因为nginx配置里index指令设置了默认查找index.php,而src/index.php正好命中。

再看一眼容器日志,确认没有异常:

docker compose logs php docker compose logs nginx

如果只看最后几条,可以加--tail参数:docker compose logs --tail=20 nginx。日志是排查问题最重要的信息来源,养成看日志的习惯比记住任何配置都管用。

5.2 常见错误与排查技巧实录

我在第一次跑这套环境时,几乎把新手能踩的坑都踩了一遍。下面按出现频率整理出来:

现象原因解法
访问页面一直转圈或报502 Bad Gatewaynginx连不上php-fpm检查default.conf里fastcgi_pass写的服务名是否正确,容器是否都处于Up状态
浏览器直接下载PHP文件或显示源码nginx没有把.php请求交给php-fpm检查default.conf的location匹配规则,确认fastcgi_pass配置存在
返回404 Not FoundSCRIPT_FILENAME路径不对default.conf里fastcgi_param SCRIPT_FILENAME要写成$document_root$fastcgi_script_name
宿主机8080端口已被占用起了其他服务换端口映射,比如8081:80,重新docker compose up -d
镜像一直拉不下来网络问题配置registry-mirrors,或检查Docker引擎是否重启成功
修改代码不生效没清opcache或没挂载正确确认./src映射到/var/www/html,容器内没有旧的缓存残留
php容器里看到代码目录为空挂载路径不对检查compose文件里相对路径,确保在项目根目录执行命令

这里重点说一个很多人翻车的问题:改完nginx配置后,一定要重启nginx容器,而不是restart php容器。命令是:

docker compose restart nginx

nginx的配置是启动时加载的,你改宿主机上的default.conf,不会热更新,必须重启容器让它重新读配置。

还有权限问题。macOS的文件系统跟Linux容器有读取权限映射,如果src目录下有文件写入不了,一般是因为php容器里运行的用户是www-data,而宿主机目录权限限制了写入。简单粗暴的解法是在容器里给/var/www/html提权,但最优雅的解法是在Dockerfile里指定运行用户或者调整目录owner,这个后面单独说。

6. 从能跑到好用:后续必做的三件事

6.1 挂载 php.ini,把配置从镜像里解放出来

基础镜像自带一份默认php.ini,但你项目的upload_max_filesize、memory_limit、display_errors这些参数很可能跟默认值不一样。直接在容器里改php.ini,容器一删就全没了,所以正规做法也是挂载到宿主机。

先启动一个临时容器把默认php.ini复制出来:

docker compose exec php php -i | grep "Loaded Configuration File"

会显示php.ini路径,比如/usr/local/etc/php/php.ini。然后在项目vendor目录下新建php/php.ini,向容器里复制默认内容,再按需修改。改完之后在compose文件php服务里添加挂载:

php: image: php:8.3-fpm-alpine container_name: php-fpm volumes: - ./src:/var/www/html - ./php/php.ini:/usr/local/etc/php/php.ini

这样以后调参不用进容器,而且整个项目环境配置都在git里,同事clone下来直接就能用。

6.2 结构调整:方便调试,而不是只求“能跑”

很多教程到这里就结束了,但我建议你再加两个小文件:一个.env文件存放可变的端口号,一个README.md记录启动步骤。.env文件可以这样写:

NGINX_PORT=8080

compose文件里对应改成"${NGINX_PORT}:80",这样不同同事的机器上如果端口冲突,不需要改compose文件,只改.env就行。这个小习惯特别适合团队协作场景。

另外强烈建议把php-fpm的日志也挂载出来。线上排障时,nginx的错误日志只能看到上游连接失败,具体PHP执行了什么错误,必须看php-fpm的日志。在compose文件php服务里再加一段:

volumes: - ./src:/var/www/html - ./php/php.ini:/usr/local/etc/php/php.ini - ./logs/php:/var/log/php

然后在php容器里确认日志目录的写权限。你现在把日志挂出来,早晚会用上。

6.3 性能与调试:顺手把 Xdebug 配上

本地开发怎么能少得了调试工具。Xdebug对php-fpm来说就像一个调试代理,它监听9000端口的调试请求,IDE(比如VS Code)连上之后就能打断点、看变量。

在php的Dockerfile里加一层安装逻辑:

RUN pecl install xdebug \ && docker-php-ext-enable xdebug

然后在php.ini里写上:

[xdebug] xdebug.mode = debug xdebug.start_with_request = yes xdebug.client_host = host.docker.internal xdebug.client_port = 9003

为什么client_host要写host.docker.internal?因为容器里的PHP执行时,要回连宿主机上的IDE。host.docker.internal是Docker Desktop自动帮你解析到宿主机地址的固定域名,容器里直接用就行,前提是Docker Desktop正常安装。

配好之后重启php容器,IDE里设置好监听端口9003,打开调试开关,就能像以前本地环境一样打断点了。这一步做完,你本地这套容器环境的使用体验就已经超过很多所谓“专业环境”了。

6.4 清理与备份:别让容器堆积成山

开发过程中免不了反复构建、反复折腾。几个实用命令先记下来:

# 停止并删除容器(不会删除镜像和挂载数据) docker compose down # 同时删除网络和匿名卷 docker compose down --volumes # 清理所有悬空镜像 docker image prune # 查看某个容器的资源占用 docker stats

有一点很重要:docker compose down不会删除你挂载到容器里的宿主机文件,所以代码和配置都不会丢。但如果你把数据写在容器内部(比如MySQL的数据文件不挂载),down之后数据就没了。这个观念要建立起来:容器是无状态的,状态全部留在挂载目录或者外部数据卷里。

有不少人开发到一半想“重置一下环境”,我通常不建议直接docker system prune -a,那会把所有镜像都删掉,下次还得重新拉。精确一点的做法是,删掉当前项目的容器和网络,保留镜像,按需重建,这样既干净又不用重新下载镜像。

我在实际维护这套环境时最深的体会是:docker-compose让环境定义变成了代码,这意味着你不需要记住任何“当初是怎么配置的”这种问题。所有配置都在项目目录里,看着compose文件就知道整个架构长什么样。即使这台Mac哪天坏了,只要项目目录还在,换台机器装好Docker,进去敲一句docker compose up -d,环境立马原地复活。这个收益,远比刚开始多花半小时学Docker来得值。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 9:25:43

RAVEN:面向事件密度与语义关联的自适应窗口引擎

1. 为什么“固定窗口”成了时序数据处理的隐形枷锁我第一次在金融风控系统里看到“每15分钟跑一次特征计算”的调度脚本时&#xff0c;没觉得有问题。直到某天凌晨三点&#xff0c;一笔异常交易在14分58秒发生&#xff0c;而模型要等到15分00秒才触发窗口聚合——那两秒&#x…

作者头像 李华
网站建设 2026/10/17 9:25:12

智能体能力评估实战:从Harness Engineering到Agent评测体系搭建

做Agent应用开发和做传统软件有一个特别不一样的地方&#xff1a;传统软件你测的是“逻辑对不对”&#xff0c;而Agent你测的是“能力的边界在哪里”。我在不少项目里见过同一个Agent&#xff0c;换一种Prompt表述效果就天差地别&#xff0c;换一套运行环境直接罢工&#xff0c…

作者头像 李华
网站建设 2026/10/17 9:24:30

机器人与AI交汇:具身智能的技术脉络与工程落地指南

机器人搞了七十年的“身体”&#xff0c;AI搞了七十年的“脑子”&#xff0c;到了这两年&#xff0c;两条线突然开始往一起凑。搞机器人的发现光有精度不够&#xff0c;得让系统学会“看情况”&#xff1b;搞AI的发现光有模型不行&#xff0c;模型得有个身体去接触真实世界&…

作者头像 李华
网站建设 2026/10/17 9:24:27

记忆增强LLM安全边界:ER-MIA注入攻击与防御实战解析

1. 记忆增强LLM的安全边界&#xff1a;为什么"会记住"反而成了新隐患最近在整理长期记忆增强大语言模型的安全测试时&#xff0c;发现一个很容易被忽视的事实&#xff1a;当模型从"每次对话都从零开始"变成"能记住用户偏好和历史上下文"之后&…

作者头像 李华
网站建设 2026/10/17 9:24:26

BP神经网络结合Adaboost集成学习原理与Python实现

我第一次把BP神经网络塞进Adaboost框架当弱分类器用&#xff0c;是处理某工业设备故障数据集的时候。单一BP网络不管怎么调隐层节点和训练轮数&#xff0c;测试精度都卡在86%左右&#xff0c;换个随机种子结果还抖得厉害。后来改用BP-Adaboost模型&#xff0c;把一个个“弱”BP…

作者头像 李华
网站建设 2026/10/17 9:24:24

VS2010 集成 CEF3 90.6.0 实战:MFC 嵌入与 JS 双向通信避坑指南

简介&#xff1a;这份资源面向需要在VS2010环境下做桌面端Web集成的C开发者&#xff0c;重点解决将CEF3 90.6.0嵌入MFC窗口并打通JavaScript与C双向调用的问题。包内以头文件、静态库与动态库、预编译头及工程配置为主&#xff0c;包含269个h、14个lib、28个dll、9个cpp以及sln…

作者头像 李华