用 Docker Compose 做多服务反向代理时,最常碰到的问题不是 Nginx 配置语法,而是服务名、网络和路径重写各自为政。先确认一点:如果你手头已经有多个后端服务,且它们都跑在同一个 Compose 项目里,那么反向代理的配置重点不是端口映射,而是让 Nginx 通过服务名找到后端。
先别急着改配置,确认 Compose 网络和服务名
在Docker Compose中,所有服务默认加入同一个用户自定义网络,服务名会作为该网络的DNS记录被自动解析。Nginx反向代理时,upstream后的地址应直接使用compose服务名,而不是容器ID或IP。需要确认服务名与docker-compose.yml中的key完全一致,大小写敏感,因为DNS解析基于这个名称。如果服务定义的name与key不同,可能引起解析失败。实际操作中,可以先用docker-compose exec nginx getent hosts 服务名 验证DNS是否解析到预期容器IP。
这里说的“服务名”不是容器名,而是 compose 文件里 services 下的顶层 key。比如你在 compose 里写 backend1:,那么 Nginx 的 proxy_pass 就用 http://backend1:端口。如果你额外给服务起了 name: myapp,那 DNS 记录不一定跟着 name 走,不同版本的 Compose 行为有差异,所以建议统一用 key 作为服务名。验证方法就是上面那条 getent 命令,确认返回的 IP 是 Compose 创建的容器网段地址,而不是宿主机的 127.0.0.1。
按“后端一个文件”的方式组织 Nginx 配置
在docker-compose.yml中,为每个后端服务定义一个服务名,例如backend1、backend2,然后创建一个公共网络让Nginx和它们共享。Nginx的配置文件可以放在conf.d目录下,每个后端单独一个文件。典型写法是location /api1/ { proxy_pass http://backend1:3000; }。注意后端服务不需要映射到宿主机端口,只需在容器网络内可访问,因此ports可以只保留给Nginx使用。
这种组织方式的好处是排查问题时能快速定位某个后端的配置。实际部署时,后端容器可以只暴露内部端口,比如 EXPOSE 3000,但不在 compose 里写 ports。这样宿主机上无法直接通过端口访问后端,必须经过 Nginx,提高了网络安全性。Nginx 的 conf.d 目录最好挂载一个本地目录,方便修改后直接 reload。
路径斜杠和 proxy_pass 的写法,直接影响路由结果
proxy_pass的URL结尾斜杠是常见坑。例如location /app/ { proxy_pass http://backend1:8080; } 时,请求路径会保留/app/前缀并传给后端;而proxy_pass http://backend1:8080/; 会把/app/前缀替换为/。如果后端需要接收完整路径,就不要加尾斜杠;如果需要重写,就用带尾斜杠并注意原有路径的存在。这个细节容易造成404或路径错误,建议分环境测试。
理解这个规则不需要背表格,只要记住:不带尾斜杠是“原样转发”,带尾斜杠是“替换匹配前缀”。如果你不确定后端实际接收路径,可以在后端容器里打印一下 request.url 或访问日志,对照看一下即可。另外,如果 location 用了正则表达式,proxy_pass 里的尾斜杠行为会略有不同,正则 location 下 Nginx 通常不支持带 URI 的 proxy_pass,配置时要更谨慎。
改完后看这几个信号:语法检查、reload、健康检查
修改配置后,先执行docker-compose exec nginx nginx -t检查语法,再执行docker-compose exec nginx nginx -s reload使配置生效。不要直接重启容器,因为重启会造成连接中断。如果Nginx容器内没有curl,可以执行docker-compose exec nginx wget -qO- http://backend1/healthz 或使用docker-compose run --rm --service-ports backend1 curl 从外部验证。检查网络连通性时,注意服务名解析应有多个IP时,Nginx会自行处理。
nginx -t 只能验证语法,不能验证路由逻辑。所以语法通过后,重点看两个地方:一是 Nginx 的 error.log,二是后端服务的访问日志。如果后端出现 404,先检查路径是否被改写;如果出现 502,多半是网络不通或服务名解析失败,回到第一步抓 getent hosts。reload 不是万能的,如果改了 compose 网络或服务名,必须重新创建相关容器,因为容器的 DNS 记录不会在 reload 时自动更新。
多个实例做负载均衡,要确认服务名能解析到多个 IP
如果需要对某个后端服务做负载均衡,可以在Nginx中定义upstream组,并在Compose中启动多个实例。例如定义upstream backend1_pool { server backend1:8080; },然后在docker-compose.yml中将backend1服务的scale设为2或使用replicas。Nginx解析服务名时会获得多个容器IP,并自动做轮询。但需注意,当upstream中只有一个server时,Nginx不会尝试解析多个IP;多个实例时需要确认服务名能解析到多个IP,否则轮询失效。
这里有一个很隐蔽的坑:如果你在 upstream 里写的是服务名 backend1,而 compose 用 scale: 2 启动了两个容器,服务名通常能解析到两个 IP。但如果你在 upstream 里写的是容器 ID 或固定 IP,那两个实例就完全不可用了。验证方法很简单:启动多个实例后,在 Nginx 容器里执行 getent hosts backend1,看返回几个 IP。另外,如果后端服务本身没有做会话共享,轮询可能导致登录态丢失,需要结合 sticky 或者后端 session 共享方案,这不在 Nginx 配置层解决。
容器重建后 IP 会变,固定 IP 的路子不要走
容器重建或服务重新创建后,IP地址会变化,但服务名保持不变。如果依赖固定IP进行代理配置,后续维护风险很高,因此必须使用服务名。另外,Compose默认网络支持DNS,但如果自定义了网络subnet或使用external网络,可能覆盖默认DNS行为。要点在于确保所有服务在同一个网络中,且没有与宿主机或网关冲突的网段。若服务分布在不同Compose项目中,还需手动连接共享网络。
这条同样适用于开发环境和测试环境。如果你用 docker-compose pull 更新了镜像,容器重建后 IP 大概率会变,但服务名不变,Nginx 无需改动。如果出现突发 502,先看看是不是某个后端容器被重建后,Nginx 仍然缓存了旧的 DNS 记录。Nginx 默认解析 DNS 的时机是在启动或 reload 时,如果后端容器在 Nginx 运行期间重建,Nginx 可能不会自动重新解析服务名,需要手动 reload 一次。这一点在不同版本的 Nginx 和 Docker DNS 下行为有差异,建议在发布脚本里加上重新 reload Nginx 的步骤。
最后的边界:跨项目共享网络怎么办
如果你不是把 Nginx 和后端放在同一个 compose 项目里,比如 Nginx 单独跑,后端在另一个项目,那就需要创建一个外部网络,并在两个 compose 文件中都声明使用它。创建网络的命令是 docker network create shared-net,然后在两个 compose 文件中用 networks: default: external: name: shared-net 这种方式引入。注意 external 网络不会自动管理 DNS 记录,所以跨项目时服务名解析取决于你的 DNS 插件,有时需要手动添加 extra_hosts。这种做法更适合已有 Nginx 实例需要接入新服务的场景,如果你是从头搭建,直接一个 Compose 项目更省事。
综上,Compose 反向代理的优先级是:先确认服务名在同一个网络内,再写 Nginx 配置,然后验证路径规则,最后考虑多实例和跨项目网络。每一步都可以用容器内的命令验证,不要靠猜。