本文中的域名、主机名、IP 地址、端口、文件路径、脚本名称和服务名称均为虚构示例,已与实际环境完全分离。
一、需求背景
在家庭服务器、小型实验室或自托管环境中,常见需求包括:
- 只有一个公网 IP;
- 多个域名需要提供 HTTPS 服务;
- 局域网中存在多台服务器;
- 每台服务器运行多个 Web 应用;
- 不希望每个应用都直接开放一个公网端口;
- 不同域名可能使用不同的 TLS 证书;
- 后续还需要持续增加新的子域名和服务。
例如,存在两套独立域名:
primary-lab.example
cloud.primary-lab.example
panel.primary-lab.example
secondary-lab.example
www.secondary-lab.example
admin.secondary-lab.example
api.secondary-lab.example
portal.secondary-lab.example
这些域名可以全部使用同一个公网 IP,并共同通过标准 HTTPS 端口 443 访问。
核心技术包括:
- DNS;
- TLS SNI;
- Nginx 虚拟主机;
- 反向代理;
- 多证书管理;
- Certbot 自动续期;
- 路由器端口转发。
二、整体架构
示例网络结构如下:
互联网
│
│ 公网 IPv4
▼
边界路由器
│
│ TCP 443
▼
Nginx 反向代理节点
│
├── www.secondary-lab.example
│ └── 10.88.0.21:18081
│
├── admin.secondary-lab.example
│ └── 10.88.0.21:18082
│
├── api.secondary-lab.example
│ └── 10.88.0.21:18083
│
└── portal.secondary-lab.example
└── 10.88.0.21:18084
路由器只需保留一条主要的 HTTPS 转发:
公网 TCP 443
→ 反向代理节点 TCP 443
外部访问地址保持为标准形式:
https://www.secondary-lab.example
https://admin.secondary-lab.example
https://api.secondary-lab.example
https://portal.secondary-lab.example
无需在 URL 后附加内部应用端口。
三、为什么多个域名能够共用 443
HTTPS 客户端在 TLS 握手阶段通常会携带目标域名,这项机制称为:
SNI
Server Name Indication
Nginx 接收到连接后,可以根据客户端请求的域名选择对应的:
server_name;- TLS 证书;
- 反向代理目标;
- 日志文件;
- 上传限制;
- WebSocket 设置;
- 安全策略。
因此,以下请求虽然全部进入同一个地址:
198.51.100.20:443
仍可被 Nginx 分别处理:
www.secondary-lab.example
→ 10.88.0.21:18081
admin.secondary-lab.example
→ 10.88.0.21:18082
api.secondary-lab.example
→ 10.88.0.21:18083
portal.secondary-lab.example
→ 10.88.0.21:18084
从公网看,所有服务都使用标准 HTTPS;从局域网看,每个域名可以对应不同的主机、端口和应用。
四、Nginx 配置示例
假设域名和后端关系如下:
secondary-lab.example
www.secondary-lab.example
→ 10.88.0.21:18081
admin.secondary-lab.example
→ 10.88.0.21:18082
api.secondary-lab.example
→ 10.88.0.21:18083
portal.secondary-lab.example
→ 10.88.0.21:18084
可以配置四个独立的 server 块。
主域名和 www 子域名
server {
listen 443 ssl;
http2 on;
server_name secondary-lab.example www.secondary-lab.example;
client_max_body_size 200M;
access_log /var/log/nginx/secondary-main.access.log;
error_log /var/log/nginx/secondary-main.error.log;
ssl_certificate /srv/tls/secondary/fullchain.pem;
ssl_certificate_key /srv/tls/secondary/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://10.88.0.21:18081;
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_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
}
}
管理子域名
server {
listen 443 ssl;
http2 on;
server_name admin.secondary-lab.example;
client_max_body_size 200M;
access_log /var/log/nginx/secondary-admin.access.log;
error_log /var/log/nginx/secondary-admin.error.log;
ssl_certificate /srv/tls/secondary/fullchain.pem;
ssl_certificate_key /srv/tls/secondary/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://10.88.0.21:18082;
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_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
}
}
API 子域名
server {
listen 443 ssl;
http2 on;
server_name api.secondary-lab.example;
client_max_body_size 200M;
access_log /var/log/nginx/secondary-api.access.log;
error_log /var/log/nginx/secondary-api.error.log;
ssl_certificate /srv/tls/secondary/fullchain.pem;
ssl_certificate_key /srv/tls/secondary/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://10.88.0.21:18083;
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_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
}
}
其他应用子域名
server {
listen 443 ssl;
http2 on;
server_name portal.secondary-lab.example;
client_max_body_size 200M;
access_log /var/log/nginx/secondary-portal.access.log;
error_log /var/log/nginx/secondary-portal.error.log;
ssl_certificate /srv/tls/secondary/fullchain.pem;
ssl_certificate_key /srv/tls/secondary/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://10.88.0.21:18084;
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_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
}
}
五、不同域名可以使用不同证书
多个域名共用同一个 443 端口,不代表必须使用同一张证书。
例如,第一套域名使用:
/srv/tls/primary/
第二套域名使用:
/srv/tls/secondary/
第一套 Nginx 配置引用:
ssl_certificate /srv/tls/primary/fullchain.pem;
ssl_certificate_key /srv/tls/primary/privkey.pem;
第二套配置引用:
ssl_certificate /srv/tls/secondary/fullchain.pem;
ssl_certificate_key /srv/tls/secondary/privkey.pem;
Nginx 会根据客户端发送的 SNI 域名返回相应证书。
因此可以实现:
同一个公网 IP
同一个 TCP 443
不同域名
不同证书
不同后端
六、申请包含多个子域名的证书
可以一次申请覆盖主域名和多个子域名的证书:
certbot certonly --standalone \
-d secondary-lab.example \
-d www.secondary-lab.example \
-d admin.secondary-lab.example \
-d api.secondary-lab.example \
-d portal.secondary-lab.example \
--agree-tos \
-m administrator@example.invalid \
--cert-name secondary-lab.example
签发后,Certbot 通常会建立:
/etc/letsencrypt/live/secondary-lab.example/fullchain.pem
/etc/letsencrypt/live/secondary-lab.example/privkey.pem
这些路径通常是符号链接。证书续期后,链接会指向新版本文件,因此 Nginx 配置中的路径无需随续期修改。
使用 standalone 模式时需要注意:
- 公网 TCP 80 必须能够到达证书申请主机;
- 本机 TCP 80 在验证期间必须可用;
- 每个申请域名都必须正确解析;
- 错误的 IPv6 AAAA 记录可能导致验证失败。
七、多张证书的自动部署
同一台服务器上存在多张证书时,部署钩子不能把所有续期事件都当作同一张证书处理。
Certbot 在 deploy hook 中通常会提供:
RENEWED_LINEAGE
RENEWED_DOMAINS
例如:
RENEWED_LINEAGE=/etc/letsencrypt/live/secondary-lab.example
可以根据证书名称执行不同分支。
以下脚本名称、目录和服务均为虚构示例:
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
RENEWED_LINEAGE="${RENEWED_LINEAGE:-}"
if [[ -z "$RENEWED_LINEAGE" ]]; then
echo "缺少 RENEWED_LINEAGE" >&2
exit 1
fi
CERT_NAME="$(basename -- "$RENEWED_LINEAGE")"
FULLCHAIN="$RENEWED_LINEAGE/fullchain.pem"
PRIVKEY="$RENEWED_LINEAGE/privkey.pem"
case "$CERT_NAME" in
primary-lab.example)
install -d -m 0755 /srv/tls/primary
install -m 0644 \
"$FULLCHAIN" \
/srv/tls/primary/fullchain.pem
install -m 0600 \
"$PRIVKEY" \
/srv/tls/primary/privkey.pem
;;
secondary-lab.example)
install -d -m 0755 /srv/tls/secondary
install -m 0644 \
"$FULLCHAIN" \
/srv/tls/secondary/fullchain.pem
install -m 0600 \
"$PRIVKEY" \
/srv/tls/secondary/privkey.pem
;;
*)
echo "未配置部署规则,跳过:$CERT_NAME"
exit 0
;;
esac
nginx -t
systemctl reload nginx
示例脚本可命名为:
tls-routing-hook.sh
这个名称只是演示,并不对应任何实际服务器文件。
分流部署可以避免:
- 第二张证书覆盖第一张证书;
- 某张证书续期时错误重启无关服务;
- 多套域名共用同一部署目录;
- 新增证书后被旧逻辑错误处理。
八、从公网多端口暴露迁移到统一 443
在没有反向代理时,应用可能使用多个独立的公网端口。
以下端口均为虚构示例:
公网 31081 → 内网应用端口 18081
公网 32742 → 内网应用端口 18082
公网 34693 → 内网应用端口 18083
公网 35918 → 内网应用端口 18084
这种结构存在几个问题:
- 每新增一个应用,都需要增加一条 NAT 规则;
- URL 中需要附加端口;
- 内部应用可能直接暴露到公网;
- 可以绕过反向代理的 TLS、安全头和访问限制;
- 防火墙规则会不断增加;
- 公网暴露面难以统一管理。
迁移到 Nginx 后,Web 应用统一通过:
公网 TCP 443
→ Nginx 反向代理
Nginx 在局域网内部访问:
10.88.0.21:18081
10.88.0.21:18082
10.88.0.21:18083
10.88.0.21:18084
这些应用端口不再需要直接映射到公网。
最终访问形式变为:
https://www.secondary-lab.example
https://admin.secondary-lab.example
https://api.secondary-lab.example
https://portal.secondary-lab.example
九、后续增加子域名的方法
以后增加一个新应用,例如:
notes.secondary-lab.example
后端运行在:
10.88.0.21:18120
只需完成以下步骤:
- 新增 DNS 记录;
- 确保证书包含新域名;
- 增加一个 Nginx
server块; - 将
proxy_pass指向新的内网端口; - 执行配置检查;
- 平滑加载 Nginx。
示例:
server {
listen 443 ssl;
http2 on;
server_name notes.secondary-lab.example;
ssl_certificate /srv/tls/secondary/fullchain.pem;
ssl_certificate_key /srv/tls/secondary/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://10.88.0.21:18120;
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_redirect off;
}
}
整个过程不需要新增公网端口转发。
十、SSH 仍需单独处理
SSH 不是普通 HTTP 流量,不能直接使用 Nginx HTTP 虚拟主机的 server_name 进行分流。
因此,Web 应用统一通过 443 后,仍可单独保留一个高位 SSH 映射。
以下端口为虚构示例:
公网 TCP 45222
→ 内网服务器 TCP 22
建议同时使用:
- SSH 公钥认证;
- 禁用密码登录;
- 限制允许登录的用户;
- 防火墙来源限制;
- 登录失败防护;
- 日志审计。
SSH 端口与 Web 反向代理属于两类不同入口,不应混为一谈。
十一、同一个 443 不能混用 PROXY Protocol
同一个监听地址和端口上的 Nginx 虚拟主机,必须对 PROXY Protocol 保持一致。
不应采用:
server {
listen 443 ssl;
server_name site-a.example;
}
server {
listen 443 ssl proxy_protocol;
server_name site-b.example;
}
原因在于:
Nginx 必须先判断连接开头是否存在 PROXY Protocol 头,之后才能继续读取 TLS 握手和 SNI 域名。
因此,不能先根据域名决定是否启用 PROXY Protocol。
正确选择只有三种:
全部不使用
listen 443 ssl;
全部统一使用
listen 443 ssl proxy_protocol;
前提是所有进入该监听端口的连接都带有合法的 PROXY Protocol 头。
使用不同监听端口
443 → 普通 HTTPS
另一个内部端口 → 带 PROXY Protocol 的上游
但普通 NAT 无法根据 TLS 内部的域名,将同一个公网 443 自动分流到两个不同内部端口。
十二、WebSocket 连接头的处理
很多配置会直接使用:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
这对 WebSocket 应用通常有效,但更规范的方式是在 http 配置范围中定义:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
然后在反向代理中使用:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
这样普通 HTTP 请求不会无条件携带:
Connection: upgrade
是否需要配置 WebSocket,应根据后端应用的实际需求决定。
十三、配置检查与平滑加载
每次修改 Nginx 配置后,都应先执行:
nginx -t
只有在出现类似结果时才继续:
syntax is ok
test is successful
随后使用:
systemctl reload nginx
通常不需要直接重启 Nginx。
reload 会让新工作进程加载新配置,同时允许旧连接继续完成,更适合生产环境中的普通配置变更。
十四、后端端口连通性检查
在反向代理节点上,可以检查内部应用端口。
以下地址和端口均为虚构示例:
for port in 18081 18082 18083 18084; do
printf '10.88.0.21:%s -> ' "$port"
timeout 3 bash -c "</dev/tcp/10.88.0.21/$port" \
&& echo OK \
|| echo FAILED
done
结果含义如下。
OK
说明反向代理节点可以建立 TCP 连接。
Connection refused
通常表示:
- 目标主机可以到达;
- 但对应端口没有应用监听;
- 或应用只监听其他地址。
超时
可能表示:
- 防火墙丢弃;
- 路由错误;
- 目标主机离线;
- 网络不可达。
后端服务器还可以检查监听状态:
ss -lntp | grep -E ':(18081|18082|18083|18084)\b'
若应用只绑定:
127.0.0.1:18081
则反向代理节点无法连接。
通常需要绑定:
0.0.0.0:18081
或者后端服务器的局域网地址:
10.88.0.21:18081
然后通过防火墙仅允许反向代理节点访问。
十五、推荐的最终公网入口
经过整理后,公网入口可以简化为:
公网 TCP 80
→ ACME HTTP 验证或 HTTP 跳转
公网 TCP 443
→ Nginx 反向代理
公网一个高位 TCP 端口
→ SSH
其他 Web 应用端口
→ 不直接暴露公网
Web 请求统一经过:
客户端
→ 公网 443
→ 边界路由器
→ Nginx
→ 内部应用端口
这种架构具有以下优势:
- 多个域名共用一个公网 IP;
- 所有网站使用标准 443;
- URL 无需附加应用端口;
- TLS 证书集中管理;
- 后端应用端口不直接暴露;
- 新增应用不需要新增公网 NAT;
- 日志和访问控制集中;
- WebSocket、SSE、上传限制可以统一配置;
- 不同域名仍可使用不同证书;
- 不同域名仍可转发到不同服务器。
十六、总结
多个域名共用同一台反向代理服务器的 443 端口,是 Nginx 的标准应用方式。
核心条件包括:
- 所有域名解析到同一个公网入口;
- 公网 443 转发到 Nginx;
- 每组域名配置正确的
server_name; - 每个虚拟主机引用正确证书;
proxy_pass指向正确的局域网后端;- 同一个监听端口的 PROXY Protocol 设置保持一致;
- 后端 Web 应用端口不再直接映射到公网;
- 多张证书通过 deploy hook 分流部署;
- 修改配置后先执行
nginx -t; - 配置通过后使用
systemctl reload nginx。
完成迁移后,公网结构会从“多个端口分别暴露”变为“统一 HTTPS 入口、按域名分流”,更容易维护,也更适合持续增加新的子域名和内部应用。