如果你手头恰好有一台云服务器、一个已备案的域名,又不想每次上线新服务都去手动改Nginx配置,那“Docker部署Nginx,做泛型二级域名自动分配”这套方案值得看一下。我最近刚把个人项目从“一个服务一个端口”切到“一个域名通吃所有子服务”,体验完全不一样:新增一个演示站,不用碰服务器上的任何配置文件,只是起一个容器,把域名按规则指给Nginx,剩下的事情它自己分发。这篇就把完整过程、配置思路、坑位和排查方法都写清楚,适合正在用Docker跑多个小项目、又对域名路由方式有点纠结的人参考。
整个方案的核心就两句话:DNS那边把*.example.com泛解析到你的服务器IP,Nginx这边通过泛域名规则把不同二级域名反向代理到不同容器。听起来简单,真正落地时涉及的细节其实不少,从证书签发到容器网络,每步都有自己的脾气。下面按实际部署顺序拆开讲。
1. 泛型二级域名分配是什么,为什么要用 Nginx + Docker
1.1 一个域名通吃所有子服务的泛解析逻辑
泛型二级域名分配,简单说就是让a.example.com、b.example.com、c.example.com这类二级域名自动落到不同的服务上,而你不需要为每个服务单独申请域名、单独配置DNS记录。DNS侧只要加一条*.example.com的A记录,指向服务器IP;HTTP服务侧再根据请求里的Host字段去匹配具体该交给哪个后端处理。
这里的关键点在于:域名解析只解决“这个域名指向哪台机器”,解决不了“这个域名对应哪个服务”。所以真正做分配的是HTTP反向代理层。它收到请求后读取域名,按预先定义的规则把流量转发到指定的容器或端口。这套机制在多服务场景下特别实用,我举个例子:你有一个博客系统、一个API服务、一个文件预览服务,如果不用泛域名,你得维护三个域名、三条DNS记录、三个证书;用了泛域名之后,全部收敛到一个域名下的二级域名里,看起来整齐,管理成本也低。
DNS泛解析也不是新鲜东西,但以前大家不愿意用,一是因为证书签发麻烦,二是动态增加服务时,网关配置仍然要手工改。直到容器化普及、自动签发证书工具成熟之后,泛域名分配才变成了一套很顺手的方案。你需要意识到,DNS和Nginx是分工协作的关系,DNS负责把流量引导到服务器,Nginx负责把流量引导到具体服务,两者缺一不可。
1.2 为什么不用“每个服务一个端口”或“手动加站点配置”
很多人在小规模阶段会用端口区分服务,比如博客跑在8080,API跑在8081,文件服务跑在8082。这么做在一两台服务器、三五个服务的时候是能用的,但问题会慢慢浮现:浏览器默认只对80和443友好,输入example.com:8080总显得不够正规;有的网络环境会封非标准端口;团队协作时,记住每个服务的端口号本身就是额外负担。更重要的是,端口方案没法在HTTPS证书上做到统一,每个端口对应一个独立证书就很难看了。
另一种常见的思路是每个新服务都手动写一个Nginx server块,再reload。这也不算错,但当一个项目组内大家都要频繁上线演示环境或分支预览时,手动操作的效率就很低。我自己的实践感受是:如果一个月只加一个新服务,手写配置无所谓;如果一周内可能反复创建、销毁临时环境,就必须把“分配”这件事做简化。泛域名分配正好把“二级域名到容器”的映射收敛到一两行配置,并且可以利用同一套正则规则去覆盖一类服务。
其实也不一定非要用Nginx做网关,像其他几个开源网关也能做类似的事。但Nginx的配置模型最简单直接,容器生态下的镜像也成熟,学习成本和调试成本比较低,所以我个人更推荐用Nginx来承担泛域名分发这一层。
1.3 Nginx 在整套容器化方案里的定位
在这套方案里,Nginx是作为请求入口存在的,它承担三件事:终止TLS、按Host路由、反向代理到内部容器。你可能会问,为什么不让业务容器直接暴露端口,非要绕一层Nginx?因为入口统一之后,无论是日志、访问控制、请求大小限制还是跨服务复用,都能在同一个地方处理。
从容器网络角度看,Nginx容器和业务容器最好处于同一个Docker网络中,这样Nginx可以用服务名或容器IP访问后端。我习惯的做法是让Nginx容器单独暴露80和443给宿主机,其他业务容器一律不映射端口到宿主机,只加入同一网络。这样做最大的好处是避免端口冲突,也不会因为业务容器重启导致宿主机网络配置残留。
还要强调的是,Nginx处理泛域名分配时,不是靠多个server_name一条条去写的,而是利用正则匹配把一组域名归拢到同一个 server 块里。比如所有带-demo.前缀的三级场景,或者不同二级域名指向同一组后端,都可以用正则简化配置。这就是泛型分配和普通虚拟主机的本质区别。
2. 部署前的核心准备:域名、DNS 与服务器
2.1 DNS 侧必须配置好的两条记录
这里先说一条容易踩的坑:只有泛解析记录,没有根域名的记录,很多服务和证书签发工具会不工作。我建议至少配两条A记录,都是指向你的服务器IP:一条是example.com本身,另一条是*.example.com。前者用来访问主站,后者用来匹配所有二级域名。
如果你的域名是在常见的DNS面板里管理,操作路径通常是在解析设置里添加记录,记录类型选A,主机记录填*或@,记录值填服务器公网IP,TTL可以先设 600 秒,调试期间方便生效,稳定后再调高到 3600 甚至更高。注意,*.example.com这种泛解析记录并不包含example.com本身,所以根域名那条别省。
TTL这个参数容易被忽略。改DNS解析后,不是立刻全局生效的,老的递归服务器会按原有TTL缓存。如果你调试时发现域名怎么都不解析到新IP,先看TTL,把值调低可以缩短等待时间。泛解析记录生效后,可以用本机nslookup或dig命令验证一下,例如dig a.demo.example.com,如果返回你的服务器IP,就说明DNS侧已经通了。
2.2 服务器侧需要开放的端口与安全组策略
服务器上的防火墙和安全组同样不能忘。你至少要确保80端口和443端口对外可达。80用于HTTP重定向到HTTPS以及一些验证场景,443用于正式流量。如果云厂商有安全组概念,记得在安全组入方向也放行这两个端口,否则服务器内部防火墙开了也没用。
除了入方向,Nginx容器要访问业务容器,通常走Docker内部网络,不经过宿主机端口映射,因此不需要额外开放业务端口。这条可以帮你减少暴露面,也让整套方案在安全审计上更说得过去。我的建议是:宿主机除22(SSH)、80、443外,其他端口一律不放行。业务容器若需要调试,可以临时用docker exec进入容器查看,不需要把调试端口暴露出去。
这里顺便说一句,如果打算后续用容器方式自动签发HTTPS证书,80端口在证书验证阶段会被用来响应挑战请求,所以不要把80端口占给其他Web服务。我见过有人把80端口先给了一个静态站点,结果证书签发失败,排查了半天才发现是端口冲突。
2.3 请求从浏览器到业务容器的完整路径
理清数据流有助于后面排查问题。整套链路是这样的:浏览器访问a.example.com,先做DNS解析,拿到服务器IP;请求到达服务器80或443端口,由Nginx容器接收;Nginx根据$host变量判断是哪个二级域名,查找匹配的 location 或 server 块,然后把请求反向代理到业务容器的内部端口;业务容器处理完成后把响应原路返回。
知道这条链路之后,遇到问题就能按段定位:域名解析不通先查DNS记录;HTTP能访问但HTTPS失败,去查证书和TLS配置;Nginx返回502,多半是后端容器连不上或者容器名写错了。后面排查章节会详细展开,这里只需要记住一句话:流量是逐层转发的,每一层都有日志,顺着日志找断点。
我在实际部署时还会在Nginx配置里加一个默认拒绝站点,把未匹配到的域名直接返回444。这样做一方面可以快速发现异常流量,另一方面避免误配置导致请求落到不该落的地方。这条细节虽然不影响正常功能,但对线上环境很有价值。
3. Docker 部署 Nginx 并启用泛域名路由
3.1 用 Docker 跑 Nginx 而不是直接在宿主机安装的原因
有朋友问我,直接用apt install nginx不也能做泛域名转发吗?为什么非要套一层Docker。我的理由有三个。第一,隔离性:宿主机上的Nginx如果配置失误,可能会影响同一台机器上的其他组件;容器化之后,问题出在容器内,删掉重建即可。第二,可迁移性:这套配置目录、容器编排文件可以整体带到另一台服务器,不用重新装环境和处理依赖。第三,和业务容器一致:既然后端都已经容器化了,网关也容器化,网络模式、资源限制、日志收集都可以用同一套Docker工具链管理。
当然,容器化也有代价,比如性能上会有极少量的损耗,但在一台普通的云服务器上,处理几千并发完全感受不到差别。我更看重的是运维体验的一致性。至少我现在新增一个服务,不需要SSH到机器上再敲一遍nginx的安装命令,所有东西都在Docker Compose项目里维护。
3.2 目录挂载、网络规划与 Compose 文件骨架
我的Nginx容器目录结构大概长这样:宿主机上建一个nginx/目录,下面放conf.d/存放各个server配置文件,certbot/或ssl/存放证书,html/放默认页面。用Docker挂载进去之后,改配置只需要编辑宿主机文件,再执行reload即可,不需要进入容器改文件。
Docker Compose是管理这套方案最舒服的方式。下面给一个骨架示例:
version: "3" services: nginx: image: nginx:stable-alpine container_name: gateway-nginx restart: always ports: - "80:80" - "443:443" volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./nginx/ssl:/etc/nginx/ssl:ro - ./nginx/html:/usr/share/nginx/html:ro networks: - app_net networks: app_net: external: true我特意把网络声明成external: true,也就是预先创建一个名为app_net的Docker网络,让Nginx和所有业务容器都加入这个网络。这样后续每个业务容器启动时只要用--network app_net或者Compose里声明同一个外部网络,Nginx就能直接通过容器名访问它们。这种做法比每次docker run默认网络更可控,也省去手动查IP的麻烦。
创建外部网络只需一条命令:
docker network create app_net注意,容器名就是Nginx访问后端的主机名。比如一个容器叫blog-api,Nginx配置里写proxy_pass http://blog-api:8080;就可以了,Docker内置DNS会自动解析。这个机制是整套方案能“自动分配”的基础。
3.3 泛域名匹配的 Nginx 核心配置写法
Nginx的泛域名匹配最核心的就是server_name支持前缀通配符、后缀通配符和正则。最简单的泛二级域名写法是:
server { listen 80; server_name *.example.com; ... }这样a.example.com和b.example.com都会进入这个块。那如何区分不同二级域名呢?一种方式是写多个server块,每个对应一个固定二级域名,这适合服务数量很少的情况。另一种方式是只保留一个泛域名入口,在 location 里用正则捕获域名前缀,再动态选择后端,这种方式适合同一类服务的批量分发。
举例说明:我想让blog.example.com转发到blog-container:8080,让api.example.com转发到api-container:3000,可以分别写两个server块,每个块内部做反向代理。这种做法直观好读,推荐给大多数场景。
server { server_name blog.example.com; listen 443 ssl; # ssl 配置略 location / { proxy_pass http://blog-container: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; } } server { server_name api.example.com; listen 443 ssl; # ssl 配置略 location / { proxy_pass http://api-container:3000; } }如果服务太多,或者希望用一个配置模板覆盖所有同类服务,可以改用带正则的server_name。比如统一使用~^(?<sub>demo-\w+)\.example\.com$,然后在配置里通过变量构造后端地址:
server { server_name ~^(?<sub>[a-z0-9-]+)\.example\.com$; listen 443 ssl; location / { proxy_pass http://${sub}-service:8080; } }这里有个容易出错的地方:proxy_pass如果使用变量作为目标地址,Nginx要求目标地址至少包含域名和端口,而且如果域名变量解析不了,会在请求时直接报502。我一开始就被这个坑折磨过,因为每次改完配置都要确认${sub}-service这个容器名真实存在。如果前缀和后缀对不上,整个请求就会失败。因此用正则动态构造后端时,命名规范必须统一,比如所有后端容器都叫xxx-service,并且这里的xxx必须和二级域名的前缀一致。
如果服务数量不多,我还是建议用显式server块,虽然多几行配置,但出错概率低,日志也更好读。动态正则方案适合“一定范围的服务可以按规则枚举出来”的场景。
3.4 容器启动与配置热加载的正确姿势
配置文件写好之后,先用docker compose config校验一下Compose文件格式,然后启动Nginx:
docker compose up -d nginx如果后续改了Nginx配置,不需要整个容器重启,执行:
docker exec gateway-nginx nginx -t docker exec gateway-nginx nginx -s reloadnginx -t这步必须先做,它检查语法错误,如果不过,reload会失败并可能导致旧的配置继续运行。我习惯把这两条命令写成一个shell脚本叫做reload-nginx.sh,放到项目根目录,每次改配置就执行一下。
这里要特别提醒:Docker挂载配置目录用了:ro,这是有意为之,防止容器内部被意外篡改,也提醒你在宿主机侧维护配置。但如果配置目录里的文件很多,reload时会逐个解析,文件太多会导致reload延迟,尽量把无关的历史配置文件清理掉。
我还会给Nginx容器配置restart: always,这样即使服务器意外重启,Nginx也会自动跟随Docker守护进程启动。如果你还加了证书自动续期的定时任务,重启后要确认容器依赖顺序正常,否则可能出现Nginx已经启动但证书还没挂上导致启动失败。个人经验是,证书相关的问题后面第5章再展开,这里先把容器跑起来再说。
4. 泛域名到业务容器的实际分发实现
4.1 固定二级域名到单一容器的标准配置
最常见的场景就是“一个二级域名对应一个容器”。你只需要准备两个东西:业务容器名和它的内部端口。然后在Nginx配置里新增一个server块,监听80或443,server_name写成固定的二级域名,location里做proxy_pass。
以博客系统为例,假如容器名是my-blog,内部端口是 8000,配置可以是这样:
server { server_name blog.example.com; listen 80; location / { proxy_pass http://my-blog:8000; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; } }这种配置的优点是清晰,一个server块只服务于一个二级域名,出现问题单独排查。但需要注意一点:如果后端容器不在同一个Docker网络里,proxy_pass http://my-blog:8000会解析不到主机名。遇到这个问题时,不要急着改成IP,先检查容器是否加入了app_net网络。把业务容器加入网络之后,同一网络内部的容器名DNS解析立刻生效。
4.2 用通配符统一入口并分流转发的进阶玩法
如果业务模式是按用户或项目创建临时环境,比如team-a.demo.example.com、team-b.demo.example.com,写死server块就不现实了。这时候要用通配符或正则在入口层把二级域名接收下来,再往后端分发。
一个可行的设计是:所有临时环境共用同一个后端网关容器,这个容器接收到请求后,从域名前缀里解析出项目名或用户ID,再分发到对应的子服务。Nginx这层只负责把泛域名流量转交给网关,不做具体业务判断。
或者,让Nginx把${sub}提取出来,直接转发到对应容器。示例:
server { listen 80; server_name ~^(?<sub>[a-z0-9-]+)\.example\.com$; location / { proxy_pass http://${sub}:8080; } }这个写法要求你为每个项目启动一个以该项目名命名的容器,容器内部服务监听8080。这样新增项目的步骤就变成了两步:启动一个新容器,命名和二级域名前缀一致。不需要改任何Nginx配置。这正是“泛型二级域名自动分配”最有吸引力的地方。
当然,这种动态模式也有限制:如果二级域名前缀对应的容器不存在,Nginx会返回502。所以你至少得写一个兜底的server块,让没有对应容器的域名返回一个友好的提示页,或者直接返回444。我个人会在默认server块里返回一个占位提示,然后通过监控告警来发现是否有非预期域名访问。
4.3 泛解析、默认 server 和正则之间怎么配合
Nginx匹配server_name的顺序有讲究:精确匹配优先,以*开头的通配符其次,以*结尾的通配符再次,最后才是正则匹配。所以你可以同时配置几个固定的精确域名,再配置一个正则泛域名,Nginx会优先处理精确命中的域名。
这个优先级在实际项目中很实用。比如你想让api.example.com固定走API网关,其他所有二级域名走临时环境入口。那么api.example.com用精确server块,临时环境用正则server块,二者互不干扰。
还要注意,当域名没有匹配到任何server块时,Nginx会使用默认的default_server。如果你没有显式配置,它会把第一个监听的server块当作默认。这里我强烈建议你显式定义默认server块,避免未匹配的域名把流量意外喂给某个业务容器。一个简单的默认块如下:
server { listen 80 default_server; listen 443 ssl default_server; server_name _; ssl_certificate /etc/nginx/ssl/default.crt; ssl_certificate_key /etc/nginx/ssl/default.key; return 444; }return 444是Nginx特有的非标准状态码,含义是直接关闭连接,不返回任何内容。对扫描工具来说,这种响应方式很干净,不暴露服务器指纹。不过我提醒一句,HTTPS的默认server块需要至少一个合法证书,否则SSL握手阶段就可能失败,导致服务异常。
4.4 后端容器容量与安全限制
泛域名分配让新增服务变得很容易,但也容易让人忽视安全策略。比如某个后端容器仅限内网访问,却在配置里被泛域名规则意外暴露。为了减少这类风险,我给每个业务容器起名时遵循统一约定的同时,还会在Nginx层加一层访问控制,比如仅允许特定IP段的请求访问某些敏感二级域名。
如果你的服务分类很明确,可以在Nginx里分开写多个泛域名匹配块,比如*.public.example.com走公开入口,*.internal.example.com走带有IP白名单的入口。虽然DNS泛解析是所有子域名都指向服务器,但Nginx可以根据域名正则做更细粒度的分流。这其实也是一种基于Host的精细路由。
容器侧资源限制别忘了加。泛域名分配的副作用是,只要给一个二级域名起了一个容器,它就在你的服务器上占资源。如果某个临时环境容器消耗资源过高,会影响网关和其他服务。我用Docker Compose的deploy.resources.limits或者直接在docker run里限制内存和CPU,至少保证单个容器不会把整个宿主机的资源吃满。这一步不要偷懒,等真的出了故障再补就晚了。
5. HTTPS 证书与泛域名证书自动续期
5.1 为什么泛域名证书比单域名证书麻烦一点
二级域名数量多,不可能每个域名都手动申请一张证书。虽然Nginx支持配置多个证书文件,但每新增一个二级域名就要签一张新证书,维护成本立刻上来了。更合适的做法是使用泛域名证书,它覆盖*.example.com以及example.com本身。一张证书就能让你所有二级域名都走HTTPS。
泛域名证书的申请验证方式通常有两种:HTTP-01和DNS-01。HTTP-01要求你有一个可访问的地址,在域名指向的服务器上放置一个临时文件,CA通过HTTP请求验证你对域名的控制权。这种方式对单个域名很方便,但对泛域名来说有个问题:HTTP-01挑战是针对具体域名的,比如a.example.com,虽然泛解析能让它指向服务器,但你没法预知每个新二级域名会来验证,配置起来比较别扭。
所以泛域名证书一般用DNS-01验证,也就是要求在DNS记录里添加一个指定的TXT记录,证明你控制整个域名。你只需要给_acme-challenge.example.com添加一条TXT记录,就能签出覆盖所有二级域名的证书。这也是目前最主流的泛域名证书申请方式。
5.2 用 Certbot 容器处理泛域名申请与续期
我习惯用某个开源证书客户端工具配合DNS插件来完成DNS-01验证,前提是你的DNS服务商支持API操作TXT记录。流程大致如下:启动一个证书客户端容器,让它调用DNS服务商API创建TXT记录,等验证通过后签发证书,最后证书文件输出到宿主机目录,Nginx里指向这个目录。
如果你用的DNS厂商插件不是现成的,也有变通办法:手动添加TXT记录完成首次签发,随后续期时再用定时脚本更新TXT记录。手动方式虽然繁琐,但胜在不受插件限制,适合域名解析在自建或小众平台上的场景,只需要在每次续期前留出足够时间让TXT记录生效。
证书签下来之后,Nginx容器需要挂载证书目录并定期reload。具体到Nginx配置,把ssl_certificate和ssl_certificate_key指向同一个泛域名证书文件路径即可。泛域名证书可以覆盖所有二级域名,所以在每个server块里,你其实只需要写同一组证书路径,不用为每个域名单独准备证书。这极大简化了配置。
5.3 证书续期的坑位清单
第一个坑:证书续期脚本运行的时间点要避开业务高峰,因为DNS-01验证期间,TXT记录会短暂存在,如果脚本失败,可能残留旧记录,影响下一次验证。我一般把续期任务定在凌晨2点到4点之间,并配置失败重试3次。
第二个坑:续期成功之后一定要让Nginx重新加载证书,否则Nginx仍会继续使用旧证书直到进程重启或reload。这步没有自动做的话,证书到期前你可能会看到客户端报警。解决方案很简单,在续期脚本末尾加一条docker exec gateway-nginx nginx -s reload。
第三个坑:不要把证书文件放在容器内部直接改。因为容器可能被重建,内部文件会丢失。正确做法是让证书目录挂载自宿主机,续期脚本在宿主机上执行并把新证书写到该目录,保证Nginx容器挂载的文件始终是最新的。如果挂载方式不当,Nginx容器重建后证书丢失,HTTPS直接瘫痪,属于我最不想遇到的事故类型。
5.4 有泛域名证书后,Nginx 配置如何统一
拿到泛域名证书后,一个比较巧妙的做法是,把SSL证书配置直接写到默认server块里,因为Nginx的配置继承特性,在同一个监听端口端口上,子server块可以不重复写证书,但这需要你整体设计一致。不过我为了减少配置耦合,还是会每个业务server块都写上证书路径,多几行但清晰。
还有一点,HTTP访问需要无条件跳转到HTTPS。我通常会在80端口的每个server块里写一个return 301 https://$host$request_uri;,或者单独用一个只监听80的server块统一做跳转。因为80和443是同一个Nginx容器在监听,不需要额外开其他服务。如果你某个二级域名暂时不想开放HTTPS,可以保留80服务,但建议至少把没有证书的域名严格限定在测试环境。
注意,使用return 301做强制跳转时,$host保留了原始的二级域名,这样跳到HTTPS后Nginx还能继续按域名路由。不会因为跳转后域名丢失导致Nginx匹配不到正确的server块。
6. 踩坑排查与运维小技巧
6.1 泛域名分配常见问题速查表
下面整理了我实际操作中遇到的问题和解决思路,按症状排查会比较快:
| 问题现象 | 可能原因 | 排查方法 | 解决动作 |
|---|---|---|---|
| 二级域名解析不到服务器 | DNS泛解析记录未生效 | dig 任意二级域名 | 检查A记录,等待TTL或调低TTL |
| 访问HTTP不通 | 安全组或防火墙未放行80 | 本地telnet 服务器IP 80 | 放行安全组和防火墙,重启Nginx |
| 访问HTTPS提示证书错误 | 证书与域名不匹配 | 浏览器查看证书详情 | 确认泛域名证书覆盖*.example.com |
| Nginx返回502 | 后端容器未加入同一网络 | docker network inspect | 将业务容器加入app_net |
| Nginx返回404 | 未匹配到server块 | curl -H "Host: xxx.example.com" | 检查默认server与server_name |
| reload失败 | 配置文件语法错误 | nginx -t | 修正语法后重载 |
| 证书自动续期后还是旧证书 | 未执行reload | 查看证书签发时间 | 续期脚本末尾加reload命令 |
| 部分二级域名访问慢 | DNS解析跨运营商慢 | dig +trace | 更换DNS服务商或调高TTL |
这张表解决90%的常规问题。关键在于,不管哪个环节出了问题,都要先确认流量走到哪一层了。如果连IP都ping不通,就从防火墙和安全组查起;如果IP通但HTTP打不开,就查Nginx容器是否监听;如果HTTP通但HTTPS拒绝,就查证书和443端口。
6.2 我个人验证过有效的几个配置习惯
在把所有服务都纳入泛域名分配模式后,我慢慢养成了一套操作习惯,这里分享三条。
第一,每个泛解析二级域名默认都能访问到服务器,但绝不代表每个域名都应该能访问到某个业务容器。我会在所有正则匹配的server块里加上请求头校验或拒绝无Host请求的规则,把异常流量挡在外面。Nginx默认情况下,如果请求没有带Host头,会走默认server;默认server我配置成444,所以那些直接在服务器IP上发来的请求会被直接断开。
第二,给每个业务容器打上命名标签,并且Nginx的server块和容器命名规则保持一致。这个看起来很小,但能省很多事。比如我用blog-前缀命名所有和博客相关的容器,用demo-前缀命名临时演示容器。这样配置里的正则表达式一目了然,新同事接手的时候也容易理解。
第三,容器日志集中保留。Nginx容器默认日志输出到stdout,如果你把所有容器的日志都交给某个日志收集工具统一采集,排查问题时就不用挨个进容器看。我在宿主机上用一个简单的脚本把所有容器的日志按天归档,至少保证问题发生后能回溯到当时的请求记录。
6.3 一个容易被忽略的细节:内部服务地址别用IP
泛域名配置初期,最容易犯的错就是在proxy_pass里写了一个固定IP,比如proxy_pass http://172.20.0.5:8080;。当时能访问,但容器一旦重建,IP可能变成另一个,整个服务就断了。换成容器名之后,Docker的DNS自动解析新IP,重建后无需修改Nginx配置。这一点在我早期的部署里吃亏不少,所以现在只要看到proxy_pass里出现IP,我就会敲响警钟。
另外,proxy_set_header里至少要设置上面提到的那三个头,尤其Host头。很多后端框架会根据Host生成绝对链接,如果这个头没有转发,网站里生成的URL可能全部指向错误域名。我记得有一次后端生成了localhost链接,差点以为是代码bug,排查半天发现是Nginx里漏了Host头。
6.4 关于自动化分配的一点扩展思路
如果你已经掌握了手动泛域名配置,可以继续往下做一个自动化管理入口。做法是给Nginx容器开发一个管理脚本,这个脚本能接收参数,自动生成server配置块并reload。比如输入test-project,脚本就自动创建一个test-project.conf,里面server_name是test-project.example.com,proxy_pass指向test-project-container:8080。这样连打开编辑器都不需要,完成整个“分配”动作。
更进一步,可以结合容器管理API,在创建业务容器的同时自动调用这个脚本。我目前没有做这么重,因为小项目手动执行脚本已经足够。如果你的环境里容器数量很多,或需要频繁给外部用户开演示环境,建议把配置生成做成一个小工具,省下的时间远比写工具的时间多。
这套方案的好用之处,在于它把“域名 -> 服务”的映射关系收敛到了一个可预期、可编排的模型里。我现在的服务器上,新增一个演示站的时间和创建容器的时间几乎相等,而且不会打扰正在运行的其他服务。如果你也经常在一台服务器上折腾多个小项目,不妨按这个思路把Nginx的泛域名分配搭起来,后续维护会轻松不少。