资讯详情

Nginx泛域名反向代理:Docker多服务零改动动态分配方案

📅 2026/10/17 23:50:14 | 华诺云谱 👁 阅读
Nginx泛域名反向代理:Docker多服务零改动动态分配方案
如果你手上同时维护着五六个 Docker 服务每个服务都打算用一个独立的二级域名对外访问那这篇文章就是给你准备的。标题里说的“泛型二级域名分配”拆开讲就两件事泛域名通配符*.example.com和 nginx 反向代理分配把不同子域名的请求自动对到不同的 Docker 容器上。以前我每加一个服务就复制一份server块改server_name、改proxy_pass然后 reload。服务一多配置文件里全是重复内容稍不留神就漏改端口。后来换成一套泛域名加动态转发的配置新增容器完全不用碰网关配置这种“零改动扩服务”的感觉确实舒服。下文包含完整可复现的docker-compose.yml、nginx 正则配置以及我在实际部署中踩过的坑resolver、容器名拼接、DNS 泛解析、正则匹配优先级。适合已经熟悉 Docker 基本操作、想统一管理多服务入口的同学如果只是听说过反向代理这个词也能按步骤跑通我会把原理一并补上。1. 方案选型泛域名分配的三种主流思路1.1 为什么不用“每服务一份配置”多服务共用一台服务器时最直觉的做法是为每个服务写一个独立的server块server { listen 80; server_name blog.example.com; location / { proxy_pass http://app-blog:8080; } } server { listen 80; server_name api.example.com; location / { proxy_pass http://app-api:8080; } }这种写法有两个明显问题。第一配置膨胀。每新增一个服务就要增加一段几乎一模一样的代码改多了容易复制粘贴出错。第二网关配置变更频繁。服务上下线都要动 nginx 配置文件哪怕只是加一个小工具页面也得 reload 一遍。如果团队成员多还会出现“改配置的人不知道别人改过什么”的尴尬局面。更关键的是这种方式把“服务名”和“域名”的对应关系写死在了配置里。一旦你希望同一个服务在不同环境开发、测试、演示下自动换域名配置就彻底失控了。1.2 动态路由的两种常见做法想要一套配置通吃所有二级域名业界常见的做法有两种。第一种是OpenResty / Lua 动态路由。OpenResty 在 nginx 基础上嵌入了 Lua 解释器可以通过 Lua 脚本读取 Redis、数据库、注册中心里的域名映射表实现完全动态的路由分发。这种方案灵活性强适合服务注册和发现关系频繁变化的场景但引入 Lua 脚本、Redis、注册中心这些依赖后运维复杂度明显上升。第二种是nginx 正则匹配 变量转发。利用 nginx 的 PCRE 正则表达式在server_name中捕获子域名再通过set指令拼出上游地址最后用变量形式传给proxy_pass。不需要额外的存储和脚本纯官方 nginx 镜像就能实现。我选的是第二种。理由很直接场景够用、零依赖、配置一眼能看懂。1.3 我最终选的组合方案与适用范围我最终的方案是nginx 正则捕获子域名 容器命名规约 Docker 内置 DNS 动态解析。具体拆成几条约定所有业务容器统一放到同一个 Docker 自定义网络中。业务容器命名遵循app-子域名的规约比如app-blog、app-api。nginx 入口容器用一条正则规则捕获Host中的子域名部分拼接成http://app-${sub}:端口并转发。这套方案的适用边界我实际检验过个人服务器、小团队内部工具、测试环境、演示平台都很顺手即使有十几个服务也能管理得井井有条。但如果你到了几十上百个微服务的规模服务实例会动态扩缩容域名和实例的对应关系变化极快那应该考虑更专业的服务发现方案比如注册中心配合网关或者直接上 Kubernetes 的 Ingress。泛域名正则在这种大规模场景下只能作为兜底入口不适合做唯一的动态路由引擎。2. 泛域名生效的关键前提DNS 与 Docker 网络2.1 先在 DNS 服务商做好泛解析泛域名分配的“泛”字首先要求 DNS 层面就把所有子域名都指向你的服务器。操作很简单在 DNS 服务商的后台添加一条A记录主机记录填*记录值填你的公网 IP。以示例域名example.com为例添加*.example.com A 203.0.113.5之后blog.example.com、api.example.com、任意不存在的子域名都会解析到这台服务器。这里有个容易忽略的点DNS 泛解析只管“解析到哪台机器”至于这个子域名对应哪个容器是 nginx 内部的事情。DNS 负责把用户带到你家门口nginx 负责把人领进正确的房间。本地测试时如果不想走公网 DNS修改本机/etc/hosts逐条添加对照即可。注意 hosts 文件不支持通配符只能手动加127.0.0.1 blog.example.com 127.0.0.1 api.example.com 127.0.0.1 demo.example.com2.2 Docker 自定义网络与内置 DNSDocker 有两种最常用的网络模式默认 bridge 网络和自定义 bridge 网络。默认 bridge 网络下容器之间只能通过 IP 访问不推荐用于多容器协作。自定义 bridge 网络则支持容器名自动解析这是 Docker 内置 DNS 的功劳在同一个自定义网络里的容器可以用容器名或网络别名互相访问。因此nginx 入口容器和所有业务容器必须加入同一个自定义网络。在 docker-compose 中定义一个webnet网络所有服务都挂进去nginx 内就可以用http://app-blog:8080访问到博客容器。有一点要特别提醒不要把业务容器的端口暴露到宿主机。既然都在同一个 Docker 网络里nginx 完全可以通过内部网络、内部端口访问到它们没必要让8080、9090这些端口出现在公网上。这样能少开一个攻击面也更符合“入口唯一化”的部署习惯。2.3 为什么proxy_pass不能直接写容器名很多人第一次配置时会在 nginx 里写proxy_pass http://app-blog:8080;如果容器一直在运行、IP 不变这种写法没问题。但存在两个隐患的触发条件一是 nginx 在启动时对静态proxy_pass的上游域名做一次解析之后长期复用这个 IP。如果业务容器因升级、重启导致 Docker 网络里 IP 变了nginx 依然访问旧 IP直接 502。二是容器是“按需启停”的网关一启动就解析域名当时容器还没起来nginx 直接[emerg] host not found in upstream连启动都失败。所以动态场景下proxy_pass必须使用变量形式强制 nginx 在每个请求时去 DNS 重新解析。而要让变量形式的转发能解析nginx 里必须配置resolver把 DNS 指向 Docker 内置的127.0.0.11。3. 完整实操docker-compose nginx 泛域名分配3.1 目录结构与配置文件清单我先给出完整的目录结构你照着建就行。project/ ├── docker-compose.yml ├── nginx/ │ └── conf.d/ │ └── gateway.conf └── html/ ├── blog/ │ └── index.html ├── api/ │ └── index.html └── demo/ └── index.html三个 html 目录分别放三个演示页面的内容内容随意只要能区分来源就行。比如 blog 页面写一句This is BLOG serviceapi 页面写This is API servicedemo 页面写This is DEMO service。3.2 docker-compose.yml 编排细节services: gateway: image: nginx:1.27-alpine container_name: gateway ports: - 80:80 volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro networks: - webnet depends_on: - app-blog - app-api - app-demo app-blog: image: python:3.12-alpine container_name: app-blog volumes: - ./html/blog:/var/www/html:ro command: python -m http.server 8080 --directory /var/www/html networks: - webnet app-api: image: python:3.12-alpine container_name: app-api volumes: - ./html/api:/var/www/html:ro command: python -m http.server 8080 --directory /var/www/html networks: - webnet app-demo: image: python:3.12-alpine container_name: app-demo volumes: - ./html/demo:/var/www/html:ro command: python -m http.server 8080 --directory /var/www/html networks: - webnet networks: webnet: driver: bridge注意几个细节业务容器我用的是python:3.12-alpine跑一个简单的 HTTP 服务仅仅为了演示。真实项目中这里会替换成各种业务容器只要它们监听在8080端口且加入webnet网络即可。container_name我特意固定成了app-blog这种格式这是为了配合后面的动态转发约定。如果不用固定容器名compose 会自动生成带序号的容器名反而容易乱。业务容器没有ports映射外部无法直连只能通过网关访问。3.3 nginx 核心配置与逐行解读下面是nginx/conf.d/gateway.conf的完整内容# 默认入口不认识的 Host 直接断开 server { listen 80 default_server; server_name _; return 444; } # 泛域名分配核心入口 server { listen 80; server_name ~^(?sub[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)\.example\.com$; resolver 127.0.0.11 valid10s ipv6off; location / { set $backend_url http://app-${sub}:8080; proxy_pass $backend_url; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }逐行拆解一下。第一个server块是default_serverserver_name _;表示匹配任何不在其他 server 块中的 Host。这里直接return 444;。444 是 nginx 特有的非标准状态码会直接断开连接适合拿来拦截乱访问的域名扫描和无效请求。这块的作用是兜底所有既不是example.com也不是*.example.com的请求一律在这里终止。第二个server块才是核心。server_name ~^(?sub[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)\.example\.com$;是 PCRE 正则~开头表示这是一个正则表达式。(?sub...)是命名捕获把匹配到的子域名部分存到变量$sub中。这个正则要求子域名由字母、数字、中划线组成且中划线不能出现在首位或末尾基本符合域名规范。resolver 127.0.0.11 valid10s ipv6off;这一行必不可少。127.0.0.11是 Docker 内置 DNS 的固定地址告诉 nginx 遇到动态变量形式的proxy_pass时向这个地址解析域名。valid10s表示 DNS 解析结果缓存 10 秒避免每个请求都触发解析。ipv6off则不查询 AAAA 记录减少不必要的 DNS 请求。set $backend_url http://app-${sub}:8080;是动态拼接的核心。请求blog.example.com时$sub是blog拼出来的地址就是http://app-blog:8080。Docker 内置 DNS 再把app-blog解析成对应容器的内部 IP。这套“命名规约 字符串拼接 DNS 动态解析”的思路让新建服务变成了一件纯增量的工作。3.4 启动验证与请求实测配置写完先做语法检查然后启动docker compose up -d docker exec gateway nginx -t如果输出syntax is ok说明配置没问题。接着用 curl 模拟不同二级域名的请求curl -H Host: blog.example.com http://127.0.0.1/ curl -H Host: api.example.com http://127.0.0.1/ curl -H Host: demo.example.com http://127.0.0.1/三种请求应该分别返回对应 html 目录里的页面内容。如果已经在 DNS 或 hosts 里配好了解析直接用curl http://blog.example.com/结果一样。更关键的一步是验证“新增服务零改动”。临时再起一个容器docker run -d --name app-test \ -v $(pwd)/html/demo:/var/www/html:ro \ --network project_webnet \ python:3.12-alpine \ python -m http.server 8080 --directory /var/www/html注意网络名compose 默认创建的网卡叫项目名_webnet如果项目目录是project网络名就是project_webnet。也可以用docker network ls查看。容器起好后直接请求curl -H Host: test.example.com http://127.0.0.1/不需要 reload nginx不需要改任何配置文件新服务已经可以访问了。这里注意 DNS 泛解析已经覆盖了test.example.com所以只要 DNS 生效整条链路就是通的。这就是泛域名分配最大的价值配置一次后续只按照命名规约加容器。4. 核心机制拆解nginx 匹配顺序与正则变量4.1 server_name 的匹配优先级nginx 在收到请求时会根据Host头选择一个server块来处理。这个选择是有顺序的精确匹配server_name blog.example.com;这种写法优先级最高。通配符前缀匹配server_name *.example.com;匹配以.example.com结尾的域名。通配符后缀匹配server_name www.*;匹配以www.开头的域名。正则表达式匹配server_name ~^...;按配置文件中的书写顺序匹配到第一个就停止。如果都没有匹配走listen对应的default_server。理解这一点很重要。如果你同时写了精确的blog.example.com和*.example.com那么blog.example.com会命中精确配置不会进入泛域名规则。正则通配的规则可以被当作用户配置的“特例覆盖”手段。4.2 为什么正则 server_name 要与业务配置分开正则server_name匹配范围大放的位置不合适容易把其他服务的请求“吞掉”。最稳妥的做法是把泛域名正则写在独立的配置文件中并且把default_server放在同一个文件里做兜底。其它服务的精确域名配置放在其他文件反正精确匹配的优先级永远高于正则不需要担心被抢。我这里把default_server放在正则配置上方它负责“什么都不匹配”的情况。如果你自己在调试时发现某些请求走到了 444大概率是 Host 里的子域名不满足正则的字符限制或者域名后缀不是example.com。4.3 变量 proxy_pass 与 resolver 的绑定关系普通proxy_pass http://app-blog:8080;写法下nginx 在启动阶段解析app-blog之后一直用解析到的 IP 转发。容器 IP 变化后nginx 依然去访问旧 IP表现就是 502。变量写法proxy_pass http://app-${sub}:8080;把转发目标推迟到了请求阶段。nginx 每次都需要拿着域名去 DNS 查询所以必须配置resolver否则启动时会报nginx: [emerg] host not found in upstream app-blog:8080在 Docker 环境里resolver固定写127.0.0.11。这个地址是 Docker 内置 DNS 服务能解析容器名、网络别名、服务名。valid10s则是告诉 nginx解析结果最多缓存 10 秒。这样既不会让 IP 变化后长时间失效也不会每个请求都触发一次 DNS 查询。一个容易踩的坑是容器重启后 IP 变了但因为 resolver 缓存还没过期nginx 在 10 秒内仍会向旧 IP 转发导致请求间歇性 502。这个从现象上很像“nginx 偶发故障”实际上只是缓存窗口内正常现象等valid时间过了自然恢复。如果对恢复时间敏感可以把valid调成1s代价是 DNS 查询频率变高。5. 常见问题与排查实录5.1 502 Bad Gateway先查 resolver 和容器名这是我遇到最多的一类问题。第一反应进 nginx 容器里确认解析是否正常docker exec gateway getent hosts app-blog如果返回了容器 IP说明 Docker 网络和 DNS 没问题问题大概率在 nginx 配置里少了resolver或者proxy_pass的 URL 拼写有误。如果getent hosts app-blog查不到说明业务容器和 nginx 不在同一个网络。用docker network inspect 网络名查看网关容器和业务容器的网络成员确认两者在同一个webnet下。5.2 路径不对、404 频发变量 proxy_pass 的 URI 传递规则用变量形式proxy_pass和静态写法的 URI 传递规则不同。静态写法下nginx 会把location匹配到的部分替换成proxy_pass里的 URI变量写法下nginx 直接把原始请求 URI 原样传给上游不做 location 前缀替换。这就带来两个实际影响。第一如果业务容器监听根路径请求/api/user传过去还是/api/user没问题。第二如果你想在某个location /foo/下转发到/bar/用变量形式时需要在proxy_pass后手动拼接或者干脆在转发前用rewrite显式改写 URI。我自己的习惯是只要用到变量proxy_pass就在 location 里保持清晰的路径规则避免依赖隐式替换。还有一个相关细节业务容器如果校验Host头需要在 nginx 里传proxy_set_header Host $host;否则上游看到的是127.0.0.1之类内部地址可能导致基于域名的虚拟主机路由失效。5.3 新增服务不生效命名规约和网络没对齐新容器起了、请求还是 502或者走了 444优先查三件事容器名是不是app-子域名的格式。我遇到过因为容器名多写了一个横线导致拼接结果对不上的情况。容器有没有加入webnet网络。很容易在docker run时漏掉--network参数容器起在默认 bridge 网络上nginx 自然访问不到。子域名是否满足正则限制。正则要求子域名不能有下划线如果你的子域名带下划线会匹配失败被default_server直接 444 断开。5.4 本地测试 hosts 不支持通配符DNS 泛解析在公网没问题但本地调试验证时往往不走 DNS而是改/etc/hosts。hosts 文件不支持*.example.com这种通配写法只能逐条添加。我调试时只加两三个测试子域名就够了测试完记得清理免得影响其他工作。如果确实需要在本地模拟“任意子域名”可以用本地 DNS 工具比如dnsmasq做泛解析把*.example.com解析到127.0.0.1然后把系统 DNS 指向本地。这个方案适合经常需要本地验证泛域名场景的同学但日常开发用 hosts 足够。5.5 HTTPS 下怎么配泛域名到了 HTTPS 环节泛域名的优势依然存在。最理想的情况是准备一张通配符证书*.example.com它同时覆盖主域名和所有二级域名。证书签发可以用 ACME 协议配合 DNS API 验证的方式在 DNS 服务商后台添加一条_acme-challenge的 TXT 记录不需要在每台服务器上分别验证。证书拿到后挂载到 nginx 容器在原来的监听上增加 443server { listen 443 ssl; server_name ~^(?sub[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)\.example\.com$; ssl_certificate /etc/nginx/certs/example.com.pem; ssl_certificate_key /etc/nginx/certs/example.com.key; location / { set $backend_url http://app-${sub}:8080; proxy_pass $backend_url; } }HTTP 请求建议在泛域名的 80 入口上统一做跳转把$host$request_uri转成对应 HTTPS 地址即可。泛域名证书要注意有效期和自动续期ACME 客户端可以配置定时续期任务每次续期后需要 reload nginx 加载新证书。最后再说几点我自己的操作习惯。第一nginx 配置全都用只读方式挂载进容器改配置不会绕过 compose 管理直接改动容器内文件第二每次改完先nginx -t再 reload顺序不能反语法错了直接 reload 容易把线上的配置替换成坏的第三容器命名规约写进团队的规范文档里比写在 nginx 注释里有用得多因为后加入项目的同事第一眼看到的是 docker-compose而不是 nginx 配置。这套方案我用下来最大的感受是新增服务从“改配置 reload”变成了“起一个容器”心智负担小了很多。如果你也有一堆 Docker 服务要分域名直接按这个思路搭一套用起来就知道省事了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑