Docker 基础体系 · 第 57/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Compose 反向代理架构:Nginx、TLS、服务发现、路由和零停机
在一个典型的 Compose 部署中,客户端不直接访问应用容器,而是先连接 Nginx。Nginx 负责 TLS 终止、请求路由和反向代理;应用服务只在 Compose 网络中监听,不必直接暴露到宿主机。
一个最小的数据流如下:
flowchart LR
C[客户端] -->|HTTPS :443| E[Nginx edge]
E -->|Compose 网络<br/>HTTP :80| A[backend 容器]
A --> D[(数据库或其他依赖)]
E -.->|Docker Embedded DNS| DNS[127.0.0.11]
DNS -.->|backend -> 容器 IP| A
这里有四个必须区分的地址:
- 客户端访问的是宿主机地址,例如
https://app.example.test。 - Nginx 在宿主机上发布
80和443。 - Nginx 到应用使用 Compose 网络中的服务名和容器端口,例如
http://backend:80。 backend不是宿主机 DNS 名称,而是 Docker 用户定义网络中的服务发现名称。
如果把这些层次混淆,最常见的结果是:容器内访问 localhost 访问到了错误的容器、把容器端口错误地写成宿主机端口,或者应用容器重建后 Nginx 仍然连接旧 IP。
一、反向代理架构中的各个角色
1. 正向代理与反向代理
正向代理代表客户端访问外部服务。客户端知道代理的存在,并把请求交给代理。
反向代理代表服务端接收客户端请求。客户端通常只知道公开域名,不知道后端应用的实际地址:
客户端 -> Nginx -> 应用服务
Nginx 接收客户端的 TCP/TLS 连接,再以另一个连接访问后端。这意味着一次请求至少涉及两个连接:
- 客户端到 Nginx;
- Nginx 到后端容器。
客户端连接使用 HTTPS,并不自动意味着 Nginx 到后端也是 HTTPS。后端协议由 Nginx 的 proxy_pass 决定:
proxy_pass http://backend:80;
表示边缘到后端是 HTTP;而:
proxy_pass https://backend:8443;
才表示 Nginx 到后端也使用 TLS。生产环境是否需要后端 TLS,取决于网络边界和安全要求,不能从“前端使用 HTTPS”推导出来。
2. TLS 终止
TLS 终止是指 Nginx 完成 TLS 握手、证书校验和加密解密,然后把请求以明文 HTTP 转发到后端。
这种架构的优点是:
- 证书只在边缘层管理;
- 应用不必实现证书轮换和 TLS 配置;
- 多个应用可以共享同一个
443入口; - 可以在进入应用前统一执行重定向、限流、请求头处理和路由。
它也产生一个安全边界:从 Nginx 到后端的流量在 Docker 网络中是明文的。若 Docker 网络跨越不可信主机、存在严格的合规要求,或者后端网络并非可信边界,就应使用后端 TLS,形成 TLS re-encryption:
客户端 --HTTPS--> Nginx --HTTPS--> backend
3. 服务发现
服务发现是“根据逻辑名称找到当前服务实例地址”的过程。在 Compose 创建的用户定义网络中,Docker 提供内置 DNS。服务名通常可以直接解析:
backend -> 172.20.0.5
容器重建后,IP 可能变为 172.20.0.8,但服务名仍然是 backend。因此应用之间不应硬编码容器 IP。
Compose 的服务名是网络范围内的逻辑名称,而不是:
- 宿主机的
/etc/hosts条目; - 公共 DNS 记录;
- 可以从互联网访问的域名;
- 宿主机上可直接解析的名称。
4. 路由
路由决定请求应该进入哪个后端。常见维度有:
- 主机名:
api.example.com和web.example.com; - 路径前缀:
/api/、/static/; - 请求方法、请求头或 Cookie;
- 蓝绿版本标记。
Nginx 的 server_name 主要解决主机名路由,location 主要解决路径路由。路由规则是有顺序和匹配语义的,不能简单理解为“从上到下逐字匹配”。
5. 零停机
零停机不是“容器从来不停止”,而是对于满足条件的客户端请求,发布过程中仍有可用且正确的后端实例。
至少要区分三件事:
- 进程存活:容器中的进程还存在;
- 服务就绪:服务已经能正确处理业务请求;
- 连接排空:旧实例不再接收新请求,但已有请求允许完成。
Docker healthcheck 主要表达健康状态,不能自动完成 Nginx 的连接排空,也不能让 Compose 获得完整的滚动发布能力。
二、Compose 网络、端口和 DNS
下面这个 Compose 文件展示一个边缘 Nginx 和一个后端 Nginx:
services:
edge:
image: nginx:1.27-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
backend:
condition: service_healthy
networks:
- edge_net
backend:
image: nginx:1.27-alpine
expose:
- "80"
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/ || exit 1"]
interval: 5s
timeout: 2s
retries: 12
start_period: 5s
networks:
- edge_net
networks:
edge_net:
driver: bridge
这里有几个重要区别。
ports 与 expose
ports:
- "443:443"
表示把容器端口发布到宿主机。外部客户端可以通过宿主机的 443 连接到 Nginx。
expose:
- "80"
主要是声明服务使用的内部端口,不创建宿主机端口映射。edge 可以通过 Compose 网络访问 backend:80,但宿主机不会因为这项配置而开放一个新的端口。
因此,Nginx 应该写:
proxy_pass http://backend:80;
而不是:
proxy_pass http://localhost:80;
在 edge 容器内,localhost 指的是 edge 自己。也不应默认写宿主机映射端口,因为容器间通信走的是容器端口,不经过宿主机发布端口。
服务名和容器名
Compose 默认把服务加入项目网络,并为服务名提供 DNS 记录。应用应依赖服务名:
backend
不建议把 container_name 作为服务发现机制。固定容器名会限制同一服务的水平扩展,因为多个实例不能共享同一个固定名称。
DNS 解析不是永久 IP 租约
Docker DNS 返回的是当前网络状态下的解析结果。容器重建时,服务名不变,IP 可能变化:
第一次:
backend -> 172.20.0.5
重建后:
backend -> 172.20.0.9
客户端库通常会在连接失败后重新解析或建立新连接,但 Nginx 的上游解析行为需要单独配置,不能仅凭“配置里写了服务名”就断定它会始终跟踪新 IP。
三、depends_on、Healthcheck 与真正的就绪状态
1. 启动顺序不是可用性保证
最简单的配置:
depends_on:
- backend
表达的是 Compose 创建 backend 后再创建 edge。它不等价于:
- 后端已经监听端口;
- 后端已经完成数据库迁移;
- 后端已经可以处理业务请求;
- 后端之后永远健康。
如果 Nginx 启动时后端进程还没有监听端口,首次请求可能得到 502 Bad Gateway。
2. 使用健康条件控制初始启动
可以把后端定义为带健康检查的服务:
services:
edge:
image: nginx:1.27-alpine
depends_on:
backend:
condition: service_healthy
backend:
image: nginx:1.27-alpine
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/ || exit 1"]
interval: 5s
timeout: 2s
retries: 12
start_period: 5s
状态变化大致为:
created -> starting -> healthy
\-> unhealthy
Compose 在初始创建 edge 时等待 backend 进入 healthy,但这只是启动阶段的条件。之后 backend 变成 unhealthy 时,Compose 不会因此自动重启 edge,也不会自动修改 Nginx 上游列表。
3. 健康检查必须检查“就绪”,而非仅检查进程
下面的检查价值很低:
test: ["CMD", "pidof", "nginx"]
它只能说明进程存在,不能说明 HTTP 服务可用。
更有意义的检查至少应验证:
- 监听端口可接受连接;
- HTTP 返回预期状态码;
- 必要时检查应用依赖是否已准备好。
例如应用有 /readyz:
healthcheck:
test:
[
"CMD-SHELL",
"wget -q -O - http://127.0.0.1/readyz | grep -q '^ready$'"
]
interval: 5s
timeout: 2s
retries: 12
start_period: 10s
健康检查命令必须存在于镜像中。精简镜像不一定包含 curl,上述示例使用 Alpine 常见的 BusyBox wget,但实际项目仍应通过构建镜像后验证,而不是假设所有基础镜像都相同。
四、Nginx 的服务发现与容器重建
1. 静态上游配置的边界
常见写法是:
upstream backend_pool {
server backend:80;
}
server {
listen 80;
location / {
proxy_pass http://backend_pool;
}
}
Nginx 通常在配置加载时解析 backend。如果后端容器后来被重建并获得新 IP,已加载的 Nginx worker 可能仍使用旧地址,直到 Nginx reload 或重新启动。
这不是 Docker DNS 失效,而是“DNS 解析发生在何时”和“代理是否重新解析”两个问题。
2. 使用 Docker 内置 DNS 的动态解析
可以让 Nginx 使用 Docker DNS,并通过变量形式的 proxy_pass 触发运行时解析:
resolver 127.0.0.11 valid=10s ipv6=off;
server {
listen 80;
location / {
set $backend http://backend:80;
proxy_pass $backend;
}
}
127.0.0.11 是 Docker 用户定义网络中常见的内置 DNS 地址。valid=10s 表示 Nginx 对解析结果使用有限缓存时间;它不是 Docker DNS 的 TTL 修改,也不保证 10 秒内一定完成切换。
这种写法的关键机制是:
- 请求进入
location; - Nginx 需要构造变量形式的上游 URL;
- Nginx 通过
resolver查询backend; - 使用解析到的地址建立连接;
- 缓存结果一段时间,随后重新解析。
动态解析的代价是每次解析结果变化时,连接可能转移到新实例,且配置中的 URI 处理语义要更加谨慎。为了减少歧义,代理路径重写应显式写清楚。
例如:
location /api/ {
set $backend http://backend:80;
proxy_pass $backend;
}
这种写法会把原始请求 URI 传给后端,例如 /api/users 仍然是 /api/users。如果希望去掉 /api/ 前缀,应明确使用 URI:
location /api/ {
proxy_pass http://backend/;
}
这里的 / 会改变转发 URI。不要只因为看到两种写法都能启动,就认为它们路由结果相同。
3. 另一种方案:发布时 reload Nginx
如果不希望使用变量形式的 proxy_pass,也可以使用静态上游配置,并在后端容器更换后 reload Nginx:
docker compose exec edge nginx -t
docker compose exec edge nginx -s reload
nginx -t 先验证配置语法和引用文件;只有验证成功后才执行 reload。reload 通常让旧 worker 处理已有连接,新 worker 使用新配置接收新请求。
这种方案要求发布流程知道“后端地址或容器状态发生了变化”。它的优点是配置语义直观;缺点是需要把 reload 纳入交付流程。
4. Nginx 不会自动读取 Docker health 状态
即使 Compose 显示:
backend ... (unhealthy)
Nginx 也不会自动知道这个状态。开源版 Nginx 的常规 upstream 机制主要依赖被动失败判断,例如连接失败或响应失败;它不是 Docker healthcheck 的订阅者。
因此:
Docker healthcheck = 容器编排层的健康信号
Nginx upstream = 代理层的连接和响应行为
两者需要通过发布流程、服务发现机制或额外的控制组件连接起来,不能混为一谈。
五、TLS 终止的完整配置
1. 生成本地测试证书
生产证书应来自受信任的 CA。下面的命令只适合本地测试:
mkdir -p certs
openssl req -x509 -nodes -newkey rsa:2048 \
-keyout certs/server.key \
-out certs/server.crt \
-days 30 \
-subj "/CN=app.example.test" \
-addext "subjectAltName=DNS:app.example.test"
chmod 600 certs/server.key
subjectAltName 很重要。现代客户端通常依据 SAN 校验证书主机名,仅设置传统 CN 不能替代 SAN。
2. Nginx 配置
nginx/nginx.conf:
events {}
http {
resolver 127.0.0.11 valid=10s ipv6=off;
log_format main
'$remote_addr "$request" $status '
'host="$host" upstream="$upstream_addr" '
'request_time=$request_time upstream_time=$upstream_response_time';
access_log /var/log/nginx/access.log main;
server {
listen 80;
server_name app.example.test;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name app.example.test;
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
location / {
set $backend http://backend:80;
proxy_http_version 1.1;
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 https;
proxy_connect_timeout 3s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
proxy_pass $backend;
}
}
}
这段配置完成了几个因果链:
80收到请求后返回301;$host保留客户端请求的主机名;$request_uri保留原始路径和查询字符串;443完成 TLS 握手;proxy_pass把请求发送到 Compose 服务backend;X-Forwarded-Proto https告诉后端,客户端与边缘之间使用的是 HTTPS。
如果应用根据 X-Forwarded-Proto 判断是否安全,必须只信任来自可信 Nginx 的该请求头。不能让互联网客户端任意伪造后端信任的代理头。Nginx 的 proxy_set_header X-Forwarded-Proto https 会覆盖传入值,这比直接转发客户端提供的同名头更安全。
3. WebSocket 需要升级头
普通 HTTP 代理不自动完成 WebSocket 升级。若应用使用 WebSocket,应增加:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
server_name app.example.test;
# TLS 配置省略重复部分
location /socket/ {
set $backend http://backend:80;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 1h;
proxy_pass $backend;
}
}
proxy_read_timeout 是空闲读取超时,不是 WebSocket 生命周期的硬上限。设置过大可能导致异常连接长期占用资源,实际值应结合心跳和连接数容量确定。
六、基于主机名和路径的路由
1. 主机名路由
可以在同一个 443 监听器上配置多个虚拟主机:
server {
listen 443 ssl;
server_name api.example.test;
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
location / {
set $api_backend http://api:80;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_pass $api_backend;
}
}
server {
listen 443 ssl;
server_name web.example.test;
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
location / {
set $web_backend http://web:80;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_pass $web_backend;
}
}
客户端发送的 TLS ClientHello 还可能包含 SNI,Nginx 可以据此选择证书;TLS 建立后,HTTP Host 又参与虚拟主机选择。证书必须覆盖客户端实际访问的域名,否则客户端会在到达 HTTP 路由前就报告证书错误。
2. 路径路由与前缀处理
server {
listen 443 ssl;
server_name app.example.test;
location /api/ {
set $api_backend http://api:80;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Prefix /api;
proxy_pass $api_backend;
}
location / {
set $web_backend http://web:80;
proxy_set_header Host $host;
proxy_pass $web_backend;
}
}
此时 /api/users 会带原路径转发到 api。若后端只接受 /users,则应写:
location /api/ {
proxy_pass http://api/;
}
Nginx 在 proxy_pass 中是否包含 URI,会影响匹配到的 location 前缀是否被替换。路径重写错误通常表现为:
- 后端返回
404; - 静态资源 URL 失效;
- 应用重定向到错误路径;
- API 文档中的基础路径不正确。
诊断时要同时查看 Nginx access log 中的 $request 和后端实际收到的 URI,不能只看客户端 URL。
3. location 匹配不是简单的最长字符串替换
例如:
location / {
...
}
location /api/ {
...
}
/api/users 会优先匹配更具体的 /api/。但正则 location、精确匹配 =, 以及 ^~ 会改变选择过程。复杂配置应通过实际请求验证,而不是依靠直觉排列配置块。
七、从 Compose 启动到请求失败的状态路径
一次初始启动可以抽象为:
sequenceDiagram
participant C as docker compose
participant D as Docker Engine
participant B as backend
participant E as edge
participant U as 客户端
C->>D: 创建网络
C->>D: 创建 backend
D->>B: 启动容器
B-->>D: healthcheck starting
B-->>D: healthcheck healthy
C->>D: 创建并启动 edge
E->>D: 解析 backend
U->>E: HTTPS 请求
E->>B: HTTP 代理请求
B-->>E: HTTP 响应
E-->>U: HTTPS 响应
后端异常时,路径可能变成:
后端进程退出
-> Docker healthcheck 失败或容器停止
-> Nginx 仍保留原有上游信息
-> 新请求连接失败或超时
-> Nginx 返回 502/504
其中:
502 Bad Gateway常见于无法建立后端连接、后端返回无效响应;504 Gateway Timeout常见于后端连接建立后,在配置的读取或发送超时时间内没有完成。
如果 Nginx 有多个上游实例,proxy_next_upstream 可以在某些失败类型下尝试下一个实例:
upstream api_pool {
server api:80;
}
location /api/ {
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_pass http://api_pool;
}
但这不是事务回滚。请求可能已经被后端部分处理,随后 Nginx 才发现连接失败;对有副作用的 POST 请求自动重试可能造成重复写入。重试策略必须结合幂等键、请求方法和业务语义。
八、Compose 中的扩展与负载分发
可以尝试扩展后端:
docker compose up -d --scale backend=2
docker compose ps
预期会看到两个 backend 实例,但前提是没有配置固定的 container_name。
服务名解析在多实例场景下可能返回多个地址,客户端或代理是否使用多个地址,取决于其 DNS 和连接池行为。不要仅凭 DNS 返回多个 IP,就推导出 Nginx 一定会以理想方式进行负载均衡。
更明确的 Nginx upstream 可以写成:
upstream api_pool {
zone api_pool 64k;
server api_1:80;
server api_2:80;
}
但这要求实例名称稳定,通常不适合直接交给 Compose 自动生成的副本名称。对 Compose 来说,更现实的做法通常是:
- 让 Nginx 通过 Docker DNS 发现服务;
- 或在发布脚本中生成上游配置并 reload;
- 或使用专门的服务代理/编排系统处理动态实例。
Compose 规范描述了服务、网络、卷、健康检查和依赖等应用模型,但 Compose 本身不是 Kubernetes、Nomad 或 Swarm,不应假设它提供完整的调度、滚动升级、自动摘除和跨节点故障转移能力。
九、零停机发布的必要条件
1. 单实例重建为什么不能保证零停机
假设只有一个 backend:
backend-old 正在服务
停止 backend-old
启动 backend-new
等待健康
停止和新实例就绪之间存在空窗。即使 Nginx 容器没有停止,仍可能产生:
502:没有可连接的后端
因此,“docker compose up -d 没有报错”不等价于“用户无感知”。
2. 至少两个实例与就绪门槛
要让旧实例更新期间仍有服务,至少需要:
backend-1 healthy <- 接收流量
backend-2 healthy <- 新版本先启动并验证
backend-1 <- 再排空并移除
但 Compose 的普通 up 命令不定义通用的滚动更新算法。即使使用多个副本,也必须明确:
- 新实例何时算就绪;
- 旧实例何时停止接收新请求;
- 长连接如何排空;
- 数据库迁移是否向前兼容;
- 失败时如何恢复到旧版本。
3. 一个可执行的蓝绿发布思路
蓝绿发布保留两组服务:
blue -> 当前生产版本
green -> 新版本
先启动并验证 green:
docker compose -f compose.green.yml up -d
docker compose -f compose.green.yml ps
docker compose -f compose.green.yml exec app wget -q -O - http://127.0.0.1/readyz
然后修改 Nginx 上游,例如从:
set $app_backend http://app_blue:80;
切换到:
set $app_backend http://app_green:80;
验证配置并 reload:
docker compose exec edge nginx -t
docker compose exec edge nginx -s reload
最后观察错误率、延迟和业务指标,再删除 blue。
这个过程之所以可以减少停机,是因为切换前 green 已经独立运行。它仍然不是绝对零风险:
- green 可能只在简单健康检查上正常;
- 切换后可能暴露真实流量问题;
- 有状态会话可能仍绑定旧实例;
- 数据库 schema 变更可能使旧版本无法继续运行;
- reload 只改变 Nginx 配置,不会修复应用自身的兼容性问题。
4. 连接排空
对于短 HTTP 请求,旧实例通常可以快速停止;对于 WebSocket、SSE 或长轮询,停止容器可能立即断开大量连接。
零停机需要应用支持优雅关闭:
- 标记实例不再就绪;
- 代理不再向它发送新请求;
- 等待已有请求完成;
- 超过最大排空时间后强制终止。
Compose 的 stop_grace_period 可以延长停止等待时间:
services:
backend:
stop_grace_period: 30s
它只控制 Docker 停止容器时等待的时间。它不会自动通知 Nginx 摘除实例,也不会为应用实现排空逻辑。应用需要处理终止信号并停止接受新请求。
十、证书轮换与 Nginx reload
证书不应直接打包进应用镜像。可以将证书以只读方式挂载:
services:
edge:
volumes:
- ./certs:/etc/nginx/certs:ro
轮换流程可以是:
cp new/fullchain.pem certs/server.crt
cp new/privkey.pem certs/server.key
docker compose exec edge nginx -t
docker compose exec edge nginx -s reload
顺序不能反过来:
- 若先 reload 再替换文件,Nginx 可能仍加载旧证书;
- 若新证书或私钥格式错误,
nginx -t应该失败; nginx -t失败时,不应执行 reload。
对于挂载文件的替换,最好先在临时路径验证证书和私钥匹配,再以原子方式替换。验证命令示例:
openssl x509 -in certs/server.crt -noout -subject -issuer -dates
openssl x509 -in certs/server.crt -pubkey -noout | openssl sha256
openssl pkey -in certs/server.key -pubout | openssl sha256
两条 SHA-256 摘要应相同,才能说明证书公钥和私钥匹配。
十一、端到端运行与验证
目录结构:
.
├── compose.yml
├── nginx
│ └── nginx.conf
└── certs
├── server.crt
└── server.key
启动:
docker compose config
docker compose up -d
docker compose ps
docker compose config 会展开并校验 Compose 配置。docker compose ps 应看到:
edge处于运行状态;backend处于healthy,而不是仅仅Up。
测试 HTTP 到 HTTPS 的重定向:
curl -i --resolve app.example.test:80:127.0.0.1 \
http://app.example.test/
预期包含:
HTTP/1.1 301 Moved Permanently
Location: https://app.example.test/
测试 HTTPS:
curl -k -i --resolve app.example.test:443:127.0.0.1 \
https://app.example.test/
--resolve 只在本次请求中把域名映射到 127.0.0.1,同时保留正确的 Host 和 TLS SNI。-k 忽略本地自签名证书错误;生产验证不应使用 -k,而应安装正确的信任链。
查看证书和 TLS 协议:
openssl s_client \
-connect 127.0.0.1:443 \
-servername app.example.test \
-showcerts </dev/null
这里的 -servername 很重要。没有 SNI 时,多虚拟主机配置可能返回默认证书,导致验证结果与真实客户端不同。
检查容器内 DNS:
docker compose exec edge getent hosts backend
如果镜像没有 getent,可以使用:
docker compose exec edge nginx -T
并结合请求日志判断解析和连接情况。也可以从后端反向检查网络:
docker compose exec edge wget -q -O - http://backend/
查看代理错误:
docker compose logs edge
docker compose exec edge tail -f /var/log/nginx/error.log
诊断时按层次排查:
宿主机端口是否监听
-> TLS 是否成功
-> Nginx server_name 是否匹配
-> Docker DNS 是否能解析服务名
-> 后端容器是否监听容器端口
-> 健康检查是否真正通过
-> 后端是否返回有效 HTTP 响应
不要在看到 502 后直接修改 TLS 配置;502 发生在 TLS 成功之后,通常应优先检查 Nginx 到后端的连接。
十二、常见失败配置及其原因
失败一:在 Nginx 中使用 localhost
proxy_pass http://localhost:8080;
如果 Nginx 和应用在不同容器中,这会访问 Nginx 容器自身的 8080。除非 Nginx 和应用确实运行在同一个容器,否则这是错误的服务发现方式。
正确方向是:
proxy_pass http://backend:80;
失败二:把宿主机端口当成容器端口
backend:
ports:
- "8080:80"
然后 Nginx 写:
proxy_pass http://backend:8080;
这里的 8080 是宿主机端口映射,后端容器在 Compose 网络中仍监听 80。Nginx 应写:
proxy_pass http://backend:80;
失败三:只写 depends_on 就认为发布安全
depends_on:
- backend
它只影响初始创建顺序,不能解决:
- 后端启动后仍未就绪;
- 后端运行中变为不健康;
- 后端重建后 IP 变化;
- 发布时旧实例连接排空。
失败四:认为健康检查会自动摘除后端
健康检查失败不会自动改变 Nginx 的配置。若代理必须依据主动健康状态摘除节点,需要额外的控制机制,或者使用支持该能力的代理/编排系统。
失败五:使用自签名证书测试生产行为
curl -k 可以确认请求链路大致可用,但它跳过证书信任校验,不能证明:
- 证书链完整;
- 域名匹配;
- 客户端信任正确;
- 中间证书配置正确。
本地测试和生产 TLS 验证必须分开。
失败六:把 Nginx reload 当成应用滚动发布
Nginx reload 只重新加载 Nginx 配置和证书。它不会:
- 启动新的应用容器;
- 执行数据库迁移;
- 等待应用 readiness;
- 迁移连接;
- 回滚业务状态。
它只是切换代理进程使用的配置,不能替代完整交付流程。
十三、Compose 适用的边界
Compose 很适合描述单机或单个 Linux 主机上的多容器应用:
网络创建
服务创建
卷挂载
环境变量
健康检查
依赖关系
端口发布
但“单机 Compose 反向代理”有明确边界:
- Docker Engine 主机故障时,整套服务可能不可用;
- Compose 不自动跨主机调度;
- 普通
up不提供通用滚动升级语义; - 健康检查不会自动建立全链路流量摘除;
- Nginx 不知道应用层业务是否真的安全;
- 容器删除后,容器内临时状态会丢失;
- 数据库、会话、文件和队列需要独立设计高可用与备份。
因此,在单机上可以通过“多后端实例 + readiness + Nginx 动态解析或 reload + 优雅停止”降低发布中断概率;但若要求跨节点故障转移、自动滚动升级、自动调度和声明式回滚,就需要更强的编排或发布系统。
十四、一个可验证的发布条件
可以把一次后端切换抽象成以下条件。设:
- 是旧实例集合;
- 是新实例集合;
- 是被路由到当前流量的实例集合;
- 表示实例 已通过就绪检查;
- 表示实例 仍能处理已有连接。
要让切换前后都存在可用实例,至少需要:
也就是在切换和旧实例排空期间,路由集合中始终至少有一个已就绪实例。
对于多实例服务,还需要容量条件。设:
- 是时刻 的请求负载;
- 是实例 在目标延迟下的可处理容量;
- 是当前接收流量的健康实例集合。
则至少应满足:
如果发布时先摘除一个实例,使:
即使没有任何容器崩溃,也会因为排队、超时和连接耗尽产生服务中断。由此可见,零停机不仅是“还有一个容器在运行”,还要求剩余容量足以承载流量。
完整发布顺序应接近:
启动新版本
-> 等待 readiness
-> 确认至少一个健康实例接收流量
-> 修改 Nginx 路由或服务发现结果
-> reload 并验证
-> 旧版本停止接收新请求
-> 等待连接排空
-> 停止旧版本
如果跳过“等待 readiness”,切换可能把流量发送到刚启动但尚未完成迁移的实例;如果跳过“连接排空”,长连接会被直接中断;如果跳过 reload 配置验证,错误配置可能让边缘代理整体无法重新加载。
十五、实践中的取舍
对于简单单机服务,可以采用:
客户端 HTTPS
-> Nginx TLS 终止
-> Compose 服务名发现
-> 动态 DNS 或发布后 reload
-> 多实例后端
-> healthcheck + readiness
-> 优雅停止
对于只需要一次性部署、后端重启不频繁的系统,静态 upstream 加发布后 reload 通常更容易理解和排查。
对于容器 IP 经常变化、需要自动发现的系统,Docker DNS 加 Nginx resolver 可以减少人工 reload,但必须验证当前 Nginx 版本和配置语义,并观察 DNS 缓存、失败重试和连接复用行为。
对于严格的零停机要求,不能只依赖 Compose 文件中的 depends_on 和 healthcheck。应把以下步骤作为交付流程的一部分:
构建镜像
-> 启动新版本
-> 健康与就绪验证
-> 切换路由
-> 观测错误率和延迟
-> 连接排空
-> 保留可回滚版本
最终,Nginx、TLS、Docker 服务发现和 Compose 依赖解决的是不同层的问题:
- TLS 解决客户端到入口的身份与加密;
- Nginx 解决入口代理和请求路由;
- Docker DNS 解决服务名到当前容器地址的发现;
- Healthcheck 表达容器健康状态;
- Compose 管理服务生命周期;
- 零停机发布还需要容量、就绪、排空、兼容性和回滚共同成立。
只有把这些边界连接起来,Compose 反向代理架构才不会停留在“容器能启动、页面能打开”的演示状态,而能成为可验证、可诊断、可交付的 Linux 容器部署方案。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Compose 扩容与并发:Replica、端口、负载均衡、共享状态和边界
- 下一篇:Docker CI 构建流水线:缓存、并行、扫描、签名、推送和晋级
- 延伸:Compose 服务发现与依赖:DNS、Healthcheck、启动顺序和重连
- 延伸:Docker 生产交付体系:CI、灰度、回滚、容量和运行手册
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论