Docker部署PHP与Nginx:内网绑定与Compose配置详解

发布时间:2026/10/3 14:33:33
Docker部署PHP与Nginx:内网绑定与Compose配置详解 前几天帮研发组把一套老旧的PHP项目从本机迁移到Docker容器里折腾了半天踩了不少坑。这篇文章就围绕docker部署php和nginx把内网绑定命令讲透同时给出完整的Nginx配置、Docker Compose编排和常见问题排查。内容适用的场景是你有一台内网服务器或者一台办公电脑希望让局域网内的同事通过浏览器访问到PHP站点但又不想让服务暴露到所有网卡。适合刚接触容器化的PHP开发也适合运维想快速搭一套隔离环境。1. 为什么用Docker部署PHP和Nginx还要执着于内网绑定1.1 这组技术栈到底解决什么问题先捋一下三个角色的关系。Nginx是一个高性能Web服务器擅长处理静态文件、HTTP路由和反向代理PHP本身不是常驻服务得靠PHP-FPM这个进程管理器去处理PHP脚本请求。两者之间通过FastCGI协议通信。传统安装方式是把Nginx和PHP-FPM都装在同一台机器上Nginx收到以.php结尾的请求后通过127.0.0.1:9000或Unix Socket转发给PHP-FPM。用Docker之后这两个进程被拆到不同容器里通过容器网络互相访问。因为容器是隔离的你可以给A项目用PHP 7.4给B项目用PHP 8.3互不干扰。同时整个环境可以用docker-compose.yml描述成代码放到任何一台装了Docker的机器上就能一键启动不再需要手动编译PHP扩展、调整Nginx配置。那内网绑定命令又是什么简单说就是在启动容器或者配置Nginx时明确指定服务监听哪个IP地址的哪个端口。比如-p 192.168.31.200:80:80意思是只有访问宿主机192.168.31.200这个内网IP的80端口流量才会进入Nginx容器。如果写成-p 80:80Docker默认绑定的是0.0.0.0:80所有网卡上的80端口都会暴露。在有公网IP或虚拟网卡的机器上后者意味着外部设备也可能扫描到这个端口安全隐患很大。1.2 为什么不直接用集成环境或宿主机安装很多人第一反应是用XAMPP、宝塔面板或者直接apt install nginx php。这些方案确实快但有几个问题第一版本锁死。XAMPP自带PHP版本固定项目如果要求7.2你很难在同一台机器上跑多个版本。第二环境依赖宿主机卸载或升级容易把系统搞乱。第三配置不透明别人拿到你的项目不知道用了哪些依赖和参数。Docker化之后每个项目一个目录里面有一个docker-compose.yml明确写着用哪个镜像、映射哪个端口、挂载哪些目录、设置什么环境变量。团队协作时同事只需要把项目目录拷过去docker-compose up -d就完事了。再说安全层面。宿主机直接装Nginx默认就监听80端口大家往往不会注意它到底绑定在哪个网卡上。Docker使用端口映射时很多人习惯只写端口不写IP结果变成了所有网卡都监听。内网绑定命令其实就是一种“最小暴露”习惯——只让内网访问不给其他接口留机会。2. 环境准备选对镜像装好Docker2.1 先装好Docker不同系统的关键坑Windows用户装Docker Desktop最典型的坑就是启动时提示virtualization support not detected。这通常不是Docker的问题而是底层虚拟化没开启。先去BIOS把Intel VT-x或AMD SVM打开然后在Windows功能里勾选“适用于Linux的Windows子系统”和“虚拟机平台”装好后在终端执行wsl --update。如果之前装了VMware可能会与Docker Desktop冲突因为二者都依赖虚拟化层。我一般建议如果你的主要工作负载在Docker就别同时跑VMware或者把VMware的网络模式改成仅主机。Linux下安装相对直接。Ubuntu/Debian用官方源装一套docker-ce和compose插件sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin sudo systemctl enable --now docker装完把当前用户加入docker组不然每次都要sudosudo usermod -aG docker $USER重新登录后生效。macOS用户下载Docker Desktop安装即可唯一建议是把内存调到4GB以上因为后面还要跑PHP和Nginx以及可能存在的MySQL内存太小容器重启会很频繁。2.2 镜像选型为什么是php-fpm和nginx-alpinePHP官方镜像有很多标签这里推荐php:8.2-fpm-alpine或者你项目需要的具体版本。不要选php:8.2-apache因为我们用Nginx作为Web服务器只需要FPM进程不需要Apache。alpine后缀代表基于Alpine Linux体积很小适合作为基础镜像。缺点是有些PHP扩展需要用docker-php-ext-install编译安装但官方镜像已经集成了这个工具并不复杂。Nginx官方镜像我一般选nginx:stable-alpine。稳定版本加上Alpine基础运行时占用资源很少。标签不要用latest它不稳定每次拉取可能产生行为差异。生产或内部公用环境固定到一个明确的版本号比如nginx:1.26.2-alpine。如果你不确定PHP需要哪些扩展建议先用基础镜像跑起来后面需要哪个扩展再写Dockerfile增加不要一开始就装一堆镜像会越来越臃肿。一个典型的Dockerfile示例如下FROM php:8.2-fpm-alpine RUN docker-php-ext-install pdo_mysql mysqli exif这样生成的镜像只多出你需要的扩展。3. 内网绑定命令从端口映射到Nginx监听3.1 docker run的-p参数详解宿主机IP:端口:容器端口这一节是整个部署的核心搞懂-p的语法内网绑定就理解了一半。Docker端口映射的写法是-p [宿主机IP]:[宿主机端口]:[容器端口]其中宿主机IP可以省略省略后默认是0.0.0.0。我列一张表对比一下docker run参数实际监听谁能访问-p 80:800.0.0.0:80所有能到宿主机网卡的设备-p 127.0.0.1:8088:80127.0.0.1:8088只有本机能访问-p 192.168.31.200:80:80192.168.31.200:80只有内网同网段设备可访问注意如果宿主机有多个网卡比如一个局域网网卡、一个虚拟机的虚拟网卡、一个Docker网桥0.0.0.0会把所有网卡的对应端口都暴露。绑定具体内网IP后Docker会在iptables里生成精确的DNAT规则只有目标IP等于这个内网IP的流量才会转发到容器端口。换句话说其他网卡上的同样端口是没人监听的。一个典型的内网绑定启动命令docker run -d \ --name nginx \ -p 192.168.31.200:80:80 \ -v /data/www:/usr/share/nginx/html \ nginx:stable-alpine这样运行之后内网同事访问http://192.168.31.200就能看到Nginx页面。如果直接写-p 80:80在同一台拥有多网卡的服务器上风险面会大很多。3.2 Nginx配置里的listen绑定Docker端口映射解决了“宿主机哪个端口转给容器”的问题Nginx自己还可以再绑定一次IP。如果你直接使用Nginx的host网络模式或者容器内Nginx监听的是宿主机某个固定IP就要在配置里写清楚server { listen 192.168.31.200:80; server_name myapp.local; root /usr/share/nginx/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; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }listen 192.168.31.200:80;的意思非常明确Nginx只接收目标IP为192.168.31.200的80端口请求。如果这个IP不是宿主机上的IPNginx启动时会报错。所以这个配置里的IP必须提前确认好最快的方式是ip addrLinux或ipconfigWindows查一下本机内网地址。结合Docker端口映射通常不需要在Nginx里再绑一遍IP因为流量已经被Docker精确转发到容器了。但在host网络模式下Nginx直接占用宿主机端口这时listen绑定是唯一的保护手段。我自己的习惯是大多数场景用Docker映射做内网绑定少数需要跨容器共享配置时再在Nginx层面加一道保险两层绑定会让人更放心。3.3 用docker-compose串联起来内网绑定更清晰虽然docker run也可以但PHP容器和Nginx容器需要通信还要挂载多个目录用Compose管理更清晰。下面是一个最小可用的docker-compose.ymlversion: 3.8 services: php: image: php:8.2-fpm-alpine container_name: php-fpm volumes: - ./www:/var/www/html networks: - appnet nginx: image: nginx:stable-alpine container_name: nginx ports: - 192.168.31.200:80:80 volumes: - ./www:/usr/share/nginx/html - ./nginx/default.conf:/etc/nginx/conf.d/default.conf depends_on: - php networks: - appnet networks: appnet: driver: bridge这里的重点一是ports字段写法与docker run -p完全一致192.168.31.200:80:80就是内网绑定命令在Compose里的体现。二是PHP和Nginx都被放进了同一个自定义网络appnet在这个网络里Nginx可以直接用服务名php去访问容器不需要关心IP是否变化。如果你重启了容器容器的IP可能会变但服务名php始终指向那个PHP容器。这比手动写容器IP要可靠得多。3.4 验证端口绑定是否生效启动后先看端口映射是否成功docker ps --format table {{.Names}}\t{{.Ports}}输出里应该看到类似192.168.31.200:80-80/tcp。如果看到的只有80/tcp说明端口没有绑定到具体IP。再在宿主机上执行netstat -tlnp | grep :80观察监听地址是192.168.31.200:80还是0.0.0.0:80。在另一台内网机器上用客户端工具访问curl -I http://192.168.31.200如果返回HTTP/1.1 200说明部署成功。如果长时间连接不上先执行telnet 192.168.31.200 80看端口通不通不通的话直接跳到第五节排查。4. 实操全过程从目录到跑起来4.1 构建一套可运行的项目目录结构我建议每个项目保留一套独立目录不要把所有网站的根目录揉在一起。一个常见结构长这样myapp/ ├── docker-compose.yml ├── nginx/ │ └── default.conf └── www/ ├── index.php └── test.phpdocker-compose.yml负责编排容器nginx/default.conf是Nginx站点配置www就是网页根目录。为什么要这样分因为把配置文件和数据分开迁移时只需要复制整个myapp目录到新机器上直接启动即可。如果数据和配置文件混在系统目录里很容易漏掉某个关键配置。4.2 编写PHP页面和Nginx配置在www/index.php里写最简单的探针文件验证PHP-FPM是否正常?php phpinfo();nginx/default.conf内容如下注意注释标出了每个关键节点server { listen 80; server_name _; root /usr/share/nginx/html; index index.php index.html; # 静态文件直接找磁盘路径 location / { try_files $uri $uri/ /index.php?$query_string; } # PHP请求交给FastCGI处理 location ~ \.php$ { fastcgi_pass php:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }这里有两个容易踩坑的点。第一fastcgi_pass后面跟的是php:9000不是127.0.0.1:9000。因为在Docker Compose网络里服务名php就是容器的主机名Nginx容器会解析它并连接到PHP-FPM。第二SCRIPT_FILENAME必须用$document_root$fastcgi_script_name如果你把root改成了其他目录这里会连带着生效应该保持一致。4.3 启动容器并用其他机器验证内网访问在myapp目录下执行docker-compose up -d第一次会拉取镜像等一两分钟。接着查看状态docker-compose ps两个容器都应该是Up状态。如果某个容器退出了用docker-compose logs php或docker-compose logs nginx查看日志。验证内网访问最简单的方式是打开另一台和设备同网段的机器浏览器输入http://192.168.31.200能看到phpinfo()页面就说明整个链路通了。如果你写的是真实项目建议先把框架入口文件放到www目录再调整Nginx的try_files规则。5. 常见问题与排查技巧实录5.1 内网其他机器无法访问先查这三层这个问题出现频率最高。我按顺序梳理一下排查路径表现可能原因解决办法内网机器ping不通宿主机不在同一网段或宿主机防火墙禁ping先确认两台机器的IP前三位一致再检查防火墙能ping通但端口不通宿主机防火墙拦截80端口Linux开放80端口firewall-cmd --add-port80/tcpWindows增加入站规则端口通但页面打不开Nginx配置错误或容器没起来查看容器状态和日志有一类很隐蔽的问题是端口占用。你在宿主机上本来就跑着一个Nginx或ApacheDocker映射80端口不会报错因为Docker只是创建DNAT规则但外面访问时发现返回的不是Docker容器的页面而是宿主机旧服务的内容。排查方式是用netstat -tlnp | grep :80看谁在监听的端口。遇到这种情况把宿主机上的旧服务停掉或者把Docker映射改成其他端口比如-p 8080:80绕过冲突。5.2 PHP页面502 Bad Gateway多半是fastcgi_pass错了页面能打开静态内容但所有.php请求都返回502说明Nginx找到了但没有连上PHP-FPM。最常见的错误就是把fastcgi_pass写成了127.0.0.1:9000。如果你用了docker-compose且两个容器在不同的容器中Nginx容器里的127.0.0.1指向的是它自己不是PHP容器。正确做法是在Compose网络里使用服务名php:9000。如果你就是用docker run方式启动的PHP容器可以先拿到容器IPdocker inspect php-fpm | grep IPAddress但这样不持久容器重启IP可能变。我更推荐的做法是创建一个docker network把两个容器都连进去docker network create mynet docker run --network mynet --name php-fpm -d php:8.2-fpm-alpine docker run --network mynet --name nginx -v ... -d nginx:stable-alpine然后Nginx就能用php-fpm:9000解析到那个PHP容器。另一个可能原因是PHP容器里的FPM没监听TCP端口而改成了Unix Socket。部分基础镜像默认监听9000 TCP但如果你自定义过配置要确保php-fpm.conf里的listen还是9000而不是/var/run/php-fpm.sock。因为另一个容器无法直接访问宿主机上的socket文件除非做了特殊挂载。5.3 静态文件403基本都是路径和权限问题访问http://192.168.31.200/index.html没问题但访问根目录时返回403通常有三个原因。一是Nginx配置的root路径和实际挂载路径不一致比如容器里挂载到/usr/share/nginx/html但配置写成了/var/www/html。二是宿主机目录权限不够Nginx容器里的进程以www-data用户运行如果父目录权限是700宿主机都没有给其他用户读权限容器自然读不了。解决办法是chmod -R 755 www并把文件权限设为644。三是index指令没有包含index.php如果你没有静态首页文件Nginx默认找不到可列出的目录就会403增加index index.php;即可。如果是开发环境给挂载目录chmod -R 777 www能快速解决但不建议照搬到生产环境。5.4 多站点自定义域名内网开发的高频需求很多人问怎么在同一台宿主机上用Docker部署多个PHP项目还想通过不同域名访问。核心思路是让Nginx容器暴露一个端口在容器里配置多个server块每个server块对应一个server_name。server { listen 80; server_name project1.local; root /usr/share/nginx/html/project1; index index.php; location ~ \.php$ { fastcgi_pass php1:9000; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } } server { listen 80; server_name project2.local; root /usr/share/nginx/html/project2; index index.php; location ~ \.php$ { fastcgi_pass php2:9000; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }然后在内网自己的笔记本或办公机上修改hosts文件添加两行192.168.31.200 project1.local 192.168.31.200 project2.local这样访问http://project1.local就能进入第一个项目http://project2.local进入第二个。注意需要为每个项目准备一个PHP-FPM容器或者把所有PHP项目集中到一个FPM里通过server_name区分脚本路径。如果你是在虚拟机里跑Docker同时希望宿主机之外的内网机器访问到站点需要把虚拟机的网络模式从NAT改为桥接否则虚拟机只有一个宿主机能访问的虚拟IP外网设备无法直接连接。这也是热词里“本地虚拟机 多端口nginx 开发环境多站点自定义域名配置”和“docker网络不通”最常见的根源之一。5.5 Docker Desktop启动失败常见环境问题速查Windows下Docker Desktop无法启动报virtualization support not detected前面2.1已经提到过。补充几个我能想到的细节如果你的Windows功能里同时启用了旧版Hyper-V和WSL2Docker Desktop可能会因为内核冲突而失败这时可以到“启用或关闭Windows功能”里把“Hyper-V”暂时关掉仅保留“虚拟机平台”和“适用于Linux的Windows子系统”。另外在BIOS里确认虚拟化开启后还要检查主板厂商的某些安全功能是否把虚拟化锁定了。做完这些重启系统再试。Mac环境相对少但如果你遇到Docker Desktop一直卡在starting可能是资源不够到Preferences里调大内存和CPU。6. 进阶把内网绑定扩展到完整环境6.1 MySQL和Redis只暴露该暴露的一个真实的PHP项目基本都要用到MySQL和Redis。在docker-compose里加上MySQL容器之后要不要给它做内网绑定我的建议是如果团队同事需要直接用Navicat连到数据库可以绑定到内网IP的3306端口如果只有PHP容器需要访问数据库那完全不要映射端口PHP在内部网络里用服务名mysql:3306连接即可。mysql: image: mysql:8.0 container_name: mysql environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: myapp volumes: - ./mysql-data:/var/lib/mysql ports: - 192.168.31.200:3306:3306 networks: - appnet这样MySQL既被内网绑定到了3306又加入了appnet网络。php服务可以直接使用mysql作为主机名不需要知道宿主机的内网IP。Redis同理除非要单独调试Redis否则连端口映射都不必写。只暴露“需要被外部访问”的服务是Docker网络的一个基本安全原则。内网绑定命令不是把所有服务都绑一遍而是有选择地绑定。6.2 Nginx反向代理和SSL证书注意reload内网环境经常用一台Nginx容器做反向代理把不同域名转发到同一台宿主机上不同端口的服务。示例配置server { listen 80; server_name api.local; location / { proxy_pass http://192.168.31.200: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; } }如果应用到需要对HTTPS的场景把listen 80;改为listen 443 ssl;并挂载证书文件server { listen 443 ssl; server_name secure.local; ssl_certificate /etc/nginx/cert/server.crt; ssl_certificate_key /etc/nginx/cert/server.key; location / { proxy_pass http://192.168.31.200:8080; } }很多人替换了证书文件后页面仍然显示旧证书这是因为Nginx配置没重载。需要执行docker exec nginx nginx -s reload只改文件不reloadNginx不会自动感知证书变化。还有一个容易忽略的坑证书文件挂载进容器后权限必须是644目录权限不能过严否则Nginx读取时会报错甚至容器启动直接失败。6.3 环境迁移和备份Docker部署的另一个好处是迁移容易。你只需要把项目目录包含docker-compose.yml、nginx配置、www源码以及MySQL的数据目录打包到新机器上安装Docker然后docker-compose up -d整个环境就能运行起来。需要注意MySQL、Redis以及用户上传的图片都属于数据文件必须挂在宿主机目录并纳入备份。而PHP和Nginx的容器本身是无状态的删掉重建都不影响业务。每次修改docker-compose.yml时最好执行docker-compose down docker-compose up -d而不是用docker-compose restart因为down才会删除旧网络设置让新增的端口映射和网络配置生效。我个人的体会是docker部署php和nginx内网绑定命令只解决了最外层暴露的问题真正的隔离要靠Docker网络和防火墙配合。别在配置里写死IP除非你的内网IP确实一成不变你可以把IP抽到.env文件里这样换环境时改一处就好。另外Nginx和PHP容器重启后IP可能变化所以尽量用Compose的service name而不是容器的IP地址。踩过坑之后我现在会把所有项目的配置都做成模板固定一套目录结构新项目复制出来只改域名和内网IP能省下不少时间。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询