1
0
0

同一台服务器多系统多Nginx整合,一次从端口疑问到统一反向代理的实践

2024-03-09
2026-10-12
文章摘要
|

示例环境:Ubuntu 22.04、Docker Compose、Spring Boot、Vue、Nginx 说明:文中域名、容器名、路径和业务端口均为脱敏示例,操作时应按实际环境替换。 目标:将同一台服务器上的多个 Nginx 整合为宿主机统一入口;复用旧证书先验证宿主机 HTTPS,再移除 Docker Nginx。

一、背景:一台服务器,两个业务系统,两套 Nginx

服务器上部署了两个系统:

  • system-a.example.com:使用宿主机安装的 Nginx,通过 127.0.0.1:9000 反向代理业务。

  • system-b.example.com:使用 Docker Compose 中单独部署的 Nginx,提供 Vue 前端文件,并将 /api/、/mobile-api/ 代理到 Java 容器 system-b-app:18080。

宿主机 Nginx 的典型配置:

server {
    listen 80;
    server_name system-a.example.com;
​
    location / {
        proxy_pass http://127.0.0.1:9000;
        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;
    }
}

Docker Compose 中的 Nginx:

nginx:
  image: nginx:stable-alpine
  restart: always
  container_name: system-b-nginx
  ports:
    - "8081:80"
    - "443:443"
  volumes:
    - "./dist/nginx/nginx.conf:/etc/nginx/nginx.conf"
    - "./dist/nginx/html/dist:/usr/share/nginx/html"
    - "./dist/nginx/cert:/etc/nginx/cert"
  depends_on:
    - app
  networks:
    - app-network

这种部署最初能够正常使用,但随着服务器承载的系统增多,每个项目各自带一套 Nginx,就开始产生重复配置、端口协调和证书维护的问题。

二、发现问题:两个 Nginx 都监听 80,为什么不冲突?

检查配置时,发现宿主机 Nginx 与容器 Nginx 都写了:

listen 80;

第一反应是:两个进程都监听 80 端口,不应该冲突吗?而且 Docker 的 HTTP 映射明明是 8081:80,为什么平时直接访问 https://system-b.example.com,也不需要写 :8081?

进一步查看 Compose,答案就在端口映射中:

服务

监听位置

宿主机端口

说明

宿主机 Nginx

宿主机 :80

80

接收公网 HTTP

Docker Nginx HTTP

容器 :80

8081

访问时需要显式指定 :8081

Docker Nginx HTTPS

容器 :443

443

浏览器访问 HTTPS 默认走 443

Docker 容器有自己的网络命名空间。容器里的 listen 80 并不等于宿主机占用了 80;真正需要避免冲突的是映射后的宿主机 IP + 端口组合。

因此:

  • http://system-b.example.com 默认进入宿主机的 80 端口,是否能访问 系统 B 取决于宿主机 Nginx 的 server 配置。

  • http://system-b.example.com:8081 进入 Docker Nginx 的 80 端口。

  • https://system-b.example.com 默认进入 Docker Nginx 映射的宿主机 443 端口。

结论:运行不冲突,不代表架构没有优化空间。

可用下面两条命令验证真实情况:

docker ps --format 'table {{.Names}}\t{{.Ports}}'
ss -lntp | grep -E ':(80|443|8081|8080|18080)\b'

三、分析原因:为什么决定只保留宿主机 Nginx?

当前的部署并非错误,而是将“对外统一入口”分散到了两套 Nginx:一套处理 80,另一套处理 443。它带来三个长期问题:

  1. 端口管理分散。 新系统一多,80、443、8081、8443 等映射需要逐个协调。

  2. HTTPS 管理重复。 每套容器 Nginx 都需要证书挂载、更新和 reload;使用宿主机统一入口后可集中管理证书。

  3. 排查链路不直观。 同一个域名的 HTTP 与 HTTPS 可能被不同 Nginx 处理,改错配置或误判验证入口的概率更高。

最终决定采用单入口架构:

                     客户端
                       |
                  公网 80 / 443
                       |
                宿主机 Nginx
                 /          \
     system-a.example.com   system-b.example.com
               |              |         \
       127.0.0.1:9000     Vue 静态文件   /admin-api、/app-api
                                          |
                                  127.0.0.1:18080
                                          |
                                  Docker Spring Boot

两个业务使用不同的 server_name,由同一套 Nginx 处理 TLS 和流量入口。Docker 仍然部署 Java、MySQL、Redis 等业务组件,只移除作为公网入口的容器 Nginx。

四、解决思路:先验证新入口,最后才切换生产流量

生产环境里不应该“先拆旧的、再试新的”。合理流程是:

  1. 先让宿主机能够访问 Java 容器,完成基础连通性验证。

  2. 在宿主机上配置 系统 B 的 HTTP 代理,检查静态页面与 API。

  3. 直接复用旧 Docker Nginx 已在使用的 SSL 证书,核验有效期与域名。

  4. 使用宿主机 Nginx 的临时 HTTPS 端口 8443 加载正式证书,对完整业务进行 HTTPS 验证。

  5. 验证通过后,准备生产 443 配置,再停止并移除旧 system-b-nginx,立即启用宿主机的 443。

  6. 完成 HTTPS、自动续期和业务回归验证后,再清理 Compose 中的旧服务定义。

这里有一个容易忽略的限制:Docker Nginx 已经映射宿主机 443:443,所以在它退出之前,宿主机 Nginx 不能同时绑定同一宿主机 IP 的 443。

这并不妨碍我们复用证书和测试 TLS:现有证书已经由旧 Docker Nginx 使用,预演 HTTPS 只需暂时空闲的 8443。这样直到最后切换窗口前,原有 HTTPS 服务都继续由 Docker Nginx 提供。

五、具体改造:复用旧证书验证新入口,再移除容器 Nginx

5.1 备份并检查现状

进入项目目录并备份 Compose 和现有 Nginx 配置:

cd /opt/projects/system-b
cp docker-compose.yml docker-compose.yml.bak
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
sudo cp -a /etc/nginx/conf.d /etc/nginx/conf.d.bak
​
docker ps --format 'table {{.Names}}\t{{.Ports}}'
sudo nginx -t

以下命令以 app 为 Compose 服务名,以 system-b-nginx 为旧 Nginx 容器名;执行前以实际配置为准。

5.2 打通宿主机到 Java 容器的访问

过去 Docker Nginx 可以通过容器网络解析 system-b-app:18080。宿主机 Nginx 不应直接依赖这个 Docker 内部名称,更推荐将 Java 服务只发布到宿主机回环地址。

在原有 app 服务下补充端口映射:

services:
  app:
    # 保留现有 image/build、环境变量、volumes、depends_on、networks 等配置
    ports:
      - "127.0.0.1:18080:18080"

此处是局部示例,不要用它覆盖整个 app 配置。如果 app 已经有 ports,应合并条目并排查端口冲突。Java 应用必须在容器内监听 0.0.0.0:18080。

只更新 app 服务:

docker compose config
docker compose up -d --no-deps app
curl -I http://127.0.0.1:18080/

--no-deps 让 Compose 不主动处理依赖服务;修改 ports 往往会重建 app 容器,可能产生短暂业务中断。curl 返回 404/405 不一定是网络故障,关键是是否收到应用返回的 HTTP 响应;最好再用真实 API 路径测试。

5.3 配置宿主机 HTTP 代理

在 /etc/nginx/conf.d/system-b.conf 添加以下 HTTP 配置:

server {
    listen 80;
    server_name system-b.example.com;
    client_max_body_size 100M;
​
    # Vue 单页应用
    root /opt/projects/system-b/dist/nginx/html/dist;
    index index.html;
    location / {
        try_files $uri $uri/ /index.html;
    }
​
    location /api/ {
        proxy_pass http://127.0.0.1:18080;
        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;
    }
​
    location /mobile-api/ {
        proxy_pass http://127.0.0.1:18080;
        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;
    }
}

两个检查重点:

  • 宿主机 Nginx 的运行用户(Ubuntu 常见为 www-data)必须能穿过 /opt/projects/... 目录并读取 Vue 文件。可用 namei -l /opt/projects/system-b/dist/nginx/html/dist/index.html 检查。

  • 这里 proxy_pass 后没有尾部 URI,会保留 /api/ 和 /mobile-api/ 原始请求路径。上线前仍应测试真实 API、文件上传和 WebSocket(若存在)。

验证 HTTP:

sudo nginx -t && sudo systemctl reload nginx
curl -I -H 'Host: system-b.example.com' http://127.0.0.1/
​

还应从公网验证 http://system-b.example.com 可访问,并检查 DNS 解析和安全组是否放行 80。

5.4 复用 Docker Nginx 现有 SSL 证书

这是本次迁移可以简化的地方:旧 Docker Nginx 已经有正式 SSL 证书,只要还有效,就可以直接供宿主机 Nginx 使用,不需要在切换前使用 Certbot 重新申请。

旧容器配置中使用的证书路径是:

ssl_certificate     /etc/nginx/cert/system-b.example.com_bundle.crt;
ssl_certificate_key /etc/nginx/cert/system-b.example.com.key;

而 Docker Compose 已经将宿主机目录挂载到上述路径:

volumes:
  - "./dist/nginx/cert:/etc/nginx/cert"

因此宿主机上的真实文件就在:

/opt/projects/system-b/dist/nginx/cert/system-b.example.com_bundle.crt
/opt/projects/system-b/dist/nginx/cert/system-b.example.com.key

先检查证书的域名和有效期:

CERT_DIR=/opt/projects/system-b/dist/nginx/cert
​
openssl x509 -in "$CERT_DIR/system-b.example.com_bundle.crt" \
  -noout -subject -issuer -dates -ext subjectAltName
​
# 检查证书是否至少还剩 7 天有效期(成功返回 0)
openssl x509 -checkend 604800 -noout \
  -in "$CERT_DIR/system-b.example.com_bundle.crt"

再确认私钥与证书匹配:

openssl x509 -in "$CERT_DIR/system-b.example.com_bundle.crt" \
  -pubkey -noout | openssl pkey -pubin -outform DER | sha256sum
​
openssl pkey -in "$CERT_DIR/system-b.example.com.key" \
  -pubout -outform DER | sha256sum

两条命令的 SHA-256 值应相同。如果不一致,不要继续迁移。还要确认 subjectAltName 包含 system-b.example.com,证书未过期。

为避免未来清理旧项目目录时误删证书,建议先将证书复制到宿主机 Nginx 专用目录(不要公开私钥):

sudo install -d -m 700 /etc/nginx/ssl/system-b.example.com
sudo install -m 644 "$CERT_DIR/system-b.example.com_bundle.crt" \
  /etc/nginx/ssl/system-b.example.com/fullchain.crt
sudo install -m 600 "$CERT_DIR/system-b.example.com.key" \
  /etc/nginx/ssl/system-b.example.com/privkey.key

这样宿主机 Nginx 可以独立使用旧证书,而 Docker Nginx 此时仍正常对外提供 443 服务。

复用证书解决的是本次迁移,不等于实现自动续期。旧证书如果快到期,应优先安排续期或重新签发;后续可以单独接入 Certbot,并将 Nginx 的证书路径改成 Certbot 管理的路径。

5.5 利用临时 8443 端口完整验证 HTTPS

现在已经复用了旧 Docker Nginx 的有效证书,但 443 仍由 Docker Nginx 使用。为了在切换前检验宿主机 Nginx 的 TLS、前端页面和 API,先创建一份临时 HTTPS 配置。

文件:/etc/nginx/conf.d/system-b-https-test.conf

server {
    listen 8443 ssl;
    server_name system-b.example.com;
    client_max_body_size 100M;
​
    ssl_certificate /etc/nginx/ssl/system-b.example.com/fullchain.crt;
    ssl_certificate_key /etc/nginx/ssl/system-b.example.com/privkey.key;
    ssl_protocols TLSv1.2 TLSv1.3;
​
    root /opt/projects/system-b/dist/nginx/html/dist;
    index index.html;
    location / {
        try_files $uri $uri/ /index.html;
    }
​
    location /api/ {
        proxy_pass http://127.0.0.1:18080;
        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;
    }
​
    location /mobile-api/ {
        proxy_pass http://127.0.0.1:18080;
        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;
    }
}

检查是否有端口冲突、语法问题,再启动新配置:

sudo ss -lntp | grep ':8443 ' || true
sudo nginx -t && sudo systemctl reload nginx

在服务器本机测试 TLS,--resolve 保留正确域名/SNI,并把连接指向本机:

curl -Iv --resolve system-b.example.com:8443:127.0.0.1 \
  https://system-b.example.com:8443/

如果临时 8443 不允许从公网访问,仍可以在服务器本机做上述测试;如需浏览器端完整回归,可临时在安全组仅对运维 IP 开放 8443,测试完立即关闭。

此阶段必须确认:

  • TLS 证书的域名和有效期正确,浏览器无证书告警。

  • Vue 页面刷新、静态资源加载正常。

  • /api/、/mobile-api/ 的真实请求正常,尤其是登录、文件上传、长连接等。

  • system-a.example.com 原业务不受影响。

注意:8443 预演不能完全替代最终 443 的验证;如果前端存在写死的域名、端口、跨域策略或回调地址,仍需在正式切换后做一次全量检查。

5.6 验证完成后,再执行生产 443 切换

到这里为止,旧 Docker Nginx 仍在运行,正式 HTTPS 入口尚未改变。 只有全部预检通过,才进入短暂切换窗口。

先准备生产 HTTPS 配置,最简单的方式是复制临时文件,并将其 listen 8443 ssl; 改为 listen 443 ssl;,保存为:

/etc/nginx/conf.d/system-b-https-prod.conf

不要在 Docker 仍占用 443 时重载该生产配置。 可以提前生成文件并先使用 nginx -t 检查语法,但 nginx -t 不等于端口真正可用,是否能接管 443 必须在切换时验证。

为了避免临时与生产配置被同一次 reload 加载,切换时可按以下顺序进行(建议安排维护窗口):

# 1. 先将生产文件放在 conf.d 外,避免它被当前 nginx 自动加载
sudo mkdir -p /etc/nginx/staged
sudo mv /etc/nginx/conf.d/system-b-https-prod.conf /etc/nginx/staged/system-b-https-prod.conf
​
# 2. 先确保旧 Docker Nginx 可用于回滚,记录旧容器配置和 Compose 原文件
cd /opt/projects/system-b
cp docker-compose.yml docker-compose.yml.bak
​
# 3. 正式切换:停止 Docker Nginx,释放宿主机 443
docker stop system-b-nginx
​
# 4. 启用宿主机生产 HTTPS 配置,撤掉测试 HTTPS 配置
sudo mv /etc/nginx/conf.d/system-b-https-test.conf /etc/nginx/staged/system-b-https-test.conf
sudo mv /etc/nginx/staged/system-b-https-prod.conf /etc/nginx/conf.d/system-b-https-prod.conf
​
# 5. 配置验证通过后重载
sudo nginx -t && sudo systemctl reload nginx
​
# 6. 校验正式 443
curl -Iv --resolve system-b.example.com:443:127.0.0.1 \
  https://system-b.example.com/

回滚预案:如果宿主机 443 未成功启动,先将生产配置移出 conf.d,恢复临时测试配置(如有需要),再执行 sudo nginx -t && sudo systemctl reload nginx;然后 docker start system-b-nginx 让旧 Docker Nginx 重新接管 443。回滚前务必确认宿主机不再占用 443。

生产切换后,再用浏览器和真实域名检查 HTTPS 页面、API、登录、上传、静态资源、WebSocket 等关键功能。如果使用 HSTS 或 HTTP 跳转 HTTPS,也应在确认生产 HTTPS 正常后再启用相应规则。

5.7 确认稳定后,从 Compose 中清理 Docker Nginx

当生产 443 已由宿主机 Nginx 稳定承载后,才把旧服务真正移除。

  1. 编辑 docker-compose.yml,删除 nginx: 服务块,保留 app、MySQL、Redis 等业务服务。

  2. 检查是否有其他服务的 depends_on 引用了 nginx。

  3. 执行 docker compose config 确认 YAML 正常。

  4. 删除已停止的旧容器:docker rm system-b-nginx。

  5. 根据需要执行 docker compose ps,确认业务容器仍正常。

不需要执行 docker compose down。 down 会把整个 Compose 项目停掉,不符合“只替换入口 Nginx”的目标。除非有明确维护窗口且接受全站停机,否则避免这样操作。

旧 Docker Nginx 配置和原证书文件建议先归档备份,不要在当天直接删除。宿主机 Nginx 当前使用的 /etc/nginx/ssl/system-b.example.com/ 目录必须保留;等后续启用 Certbot 自动续期后,再有计划地清理不再使用的证书副本。

六、总结:从“端口为什么没冲突”到“统一管理入口”

这次改造起源于一个简单疑问:两个 Nginx 都写着 listen 80,为什么能一起运行?

排查发现,关键在于 Docker 的网络命名空间和宿主机端口映射:容器内监听 80,但宿主机实际发布的是 8081;容器 HTTPS 发布的是宿主机 443,因此浏览器直接访问 HTTPS 时并不会经过宿主机 Nginx 的 80。

进一步思考才发现,更值得优化的不是某一个端口,而是整个服务器的流量入口管理方式。最终决定把多套 Nginx 合并成一套宿主机 Nginx,统一处理域名、HTTPS 和代理,Docker 只负责运行应用。

这次实践最重要的经验不是某条命令,而是迁移顺序:

先打通业务 → 复用并校验旧证书 → 使用 8443 验证宿主机 HTTPS → 再释放旧容器 443 → 切换生产流量 → 最后清理旧服务。

这样做能够把不确定性前移,把真正需要中断业务的操作压缩到最后切换阶段。后续新增系统,也只需新增一个宿主机 Nginx server,并为新域名申请证书,而不必继续堆叠多套容器 Nginx。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论