同一台服务器多系统多Nginx整合,一次从端口疑问到统一反向代理的实践
示例环境: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,答案就在端口映射中:
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。它带来三个长期问题:
端口管理分散。 新系统一多,80、443、8081、8443 等映射需要逐个协调。
HTTPS 管理重复。 每套容器 Nginx 都需要证书挂载、更新和 reload;使用宿主机统一入口后可集中管理证书。
排查链路不直观。 同一个域名的 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。
四、解决思路:先验证新入口,最后才切换生产流量
生产环境里不应该“先拆旧的、再试新的”。合理流程是:
先让宿主机能够访问 Java 容器,完成基础连通性验证。
在宿主机上配置 系统 B 的 HTTP 代理,检查静态页面与 API。
直接复用旧 Docker Nginx 已在使用的 SSL 证书,核验有效期与域名。
使用宿主机 Nginx 的临时 HTTPS 端口 8443 加载正式证书,对完整业务进行 HTTPS 验证。
验证通过后,准备生产 443 配置,再停止并移除旧
system-b-nginx,立即启用宿主机的 443。完成 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 稳定承载后,才把旧服务真正移除。
编辑
docker-compose.yml,删除nginx:服务块,保留app、MySQL、Redis 等业务服务。检查是否有其他服务的
depends_on引用了nginx。执行
docker compose config确认 YAML 正常。删除已停止的旧容器:
docker rm system-b-nginx。根据需要执行
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。
