一台 VPS 同时跑几个服务时,不要让每个服务各自去处理域名和证书。更省事的做法是:Nginx 监听 80/443 做统一入口,应用只监听本机端口。Nginx 负责 TLS、域名分发和转发,应用只管自己的业务。

浏览器 ──HTTPS──> Nginx :443 ──> 127.0.0.1:3000  (站点 A)
                       └──────> 127.0.0.1:8080  (站点 B)

下面以 Ubuntu 24.04、Nginx 1.24 为例,两个域名分别是 a.example.com 和 b.example.com。

一、先让应用只监听本机

假设站点 A 是 Node 服务,站点 B 是一个监听 8080 的程序。启动时把它们绑定到 127.0.0.1,而不是 0.0.0.0:

# 示例:Node 服务
HOST=127.0.0.1 PORT=3000 node server.js

好处是外网无法直连这些端口,所有流量必须经过 Nginx,少了绕过入口和暴露端口的风险。应用若必须监听 0.0.0.0,至少在防火墙里只放行 80/443。

二、写站点 A 的 server 块

新建 /etc/nginx/sites-available/a.example.com:

server {
    listen 80;
    listen [::]:80;
    server_name a.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        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 $scheme;
    }
}

四个头各有用途:

  • Host $host:把用户访问的域名传给后端,很多应用靠它判断站点;
  • X-Real-IP:客户端真实 IP;
  • X-Forwarded-For:代理链,后端取第一个非内网地址;
  • X-Forwarded-Proto:告诉后端原始请求是 http 还是 https,生成链接时用得上。

Nginx 官方文档对 Host 的说明是:默认不透传原始请求的 Host 和 Connection,需要显式设置;推荐用 $host,因为它在请求没有 Host 头时也有值。

三、站点 B 照抄一份

/etc/nginx/sites-available/b.example.com:

server {
    listen 80;
    listen [::]:80;
    server_name b.example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        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 $scheme;
    }
}

启用两个站点:

ln -s /etc/nginx/sites-available/a.example.com /etc/nginx/sites-enabled/
ln -s /etc/nginx/sites-available/b.example.com /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

此时用 HTTP 访问两个域名,应该分别落到不同后端。

四、proxy_pass 的斜杠别乱加

这是最容易踩的坑。官方原文:

If the proxy_pass directive is specified with a URI, then when a request is passed to the server, the part of a normalized request URI matching the location is replaced by a URI specified in the directive…

If proxy_pass is specified without a URI, the request URI is passed to the server in the same form as sent by a client…

举例:

配置 请求 /api/users 时后端收到
proxy_pass http://127.0.0.1:3000; /api/users
proxy_pass http://127.0.0.1:3000/; /api/users(location 为 / 时替换后相同)
location /api/ { proxy_pass http://127.0.0.1:3000/; } /users

第三种是把 /api/ 前缀剥掉。要保留前缀就别加末尾斜杠,要剥掉就加。改完必须 nginx -t 再 reload。

五、签发证书

Certbot 官方推荐用 snap 安装:

apt install -y snapd
snap install --classic certbot
ln -s /snap/bin/certbot /usr/local/bin/certbot

给两个域名各签一张证书:

certbot --nginx -d a.example.com
certbot --nginx -d b.example.com

certbot --nginx 会自动改 Nginx 配置、加上 443 的 server 块和证书路径。想自己控制配置,就用 certbot certonly --nginx 只拿证书,然后手动写:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name a.example.com;

    ssl_certificate     /etc/letsencrypt/live/a.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/a.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        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 $scheme;
    }
}

多个域名共用一个 IP 靠的是 SNI。Nginx 官方文档写得很清楚:SNI 允许浏览器在握手阶段带上要访问的域名,这是在单 IP 上跑多个 HTTPS 站点的通用方案;但 SNI 只能传域名,用 IP 直接访问时不要依赖它。

证书续期不用手动:

certbot renew --dry-run

能跑通就说明定时续期配置正常。

六、强制跳转到 HTTPS

certbot --nginx 在签发时会问是否重定向,选是就会自动加。要自己写,就在 80 的 server 块里加:

server {
    listen 80;
    server_name a.example.com;
    return 301 https://$host$request_uri;
}

注意跳转后后端拿到的 X-Forwarded-Proto 应该是 https;如果应用生成的回调地址还是 http,检查这个头有没有传。

七、WebSocket 要额外两行

反代普通网页不需要,但如果后端有 WebSocket(实时通知、终端类界面),在 location 里补:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

缺这两行,握手会失败,浏览器控制台一般能看到 WebSocket 连接被断开。

八、验证顺序

  1. nginx -t 通过;
  2. curl -I http://a.example.com 返回 301 或 200;
  3. curl -I https://a.example.com 返回 200,证书域名正确;
  4. 后端日志里能看到真实客户端 IP 和 https;
  5. certbot renew --dry-run 通过。

常见问题对照

现象 优先检查
502 Bad Gateway 后端是否在跑、端口是否对、是否只绑了 127.0.0.1
一直重定向循环 80/443 两个 server 块的 server_name 是否写错
页面能开但样式丢失 应用生成的是 http 链接,检查 X-Forwarded-Proto
后端日志 IP 全是 127.0.0.1 少了 X-Real-IP / X-Forwarded-For,或应用没读这两个头
证书提示域名不匹配 该 server 块用的证书是否属于这个域名
WebSocket 连不上 缺 Upgrade 与 Connection 两个头

一句话总结

入口统一交给 Nginx,应用只监听 127.0.0.1:一个域名一个 server 块,转发时补齐 Host 与三个转发头,证书交给 certbot,改完先 nginx -t 再 reload。