讲个真实的场景:你手上有一台云服务器,或者一台虚拟机,上面同时跑着博客、接口服务、文件管理、还有一两个内部工具。它们监听在不同的端口,每个都想用独立的域名访问,每个域名都希望有对应的SSL证书,而且最好统一从Nginx这一层进,谁要访问就由谁转发,不再给每个服务单独开端口、单独开证书。这就是典型的Nginx多域名、多证书、多服务配置需求。
这套配置解决的痛点很直接:第一,不用记一长串IP加端口号,域名简洁好记;第二,证书统一维护,不用每个服务各自装一套HTTPS;第三,服务内部架构可以随意调整,只要Nginx入口不变,前端用户完全无感知。适合个人开发者、小团队运维,也适合本地开发环境模拟多站点。我自己就是从一台裸机、三个服务、四个域名开始踩坑,最后整理出一套能直接复制粘贴、改改就能用的配置方案,这篇文章就是把最终版本和里面关键的坑全部摊开讲。
1. 为什么要做多域名多证书多服务
1.1 一台Nginx面对的真实场景
先假设一个最常见的组合,这也是我早期项目的拓扑:一台服务器上跑一个Java写的数据接口,监听在127.0.0.1:8080;一个Node.js的小工具,监听在127.0.0.1:3000;还有一个文件上传下载服务,监听在127.0.0.1:9000。这三个服务如果直接裸奔在公网上,端口号完全暴露,安全性和美观度都不行。
于是你规划了三个域名:api.example.com走Java接口,tools.example.com走Node工具,files.example.com走文件服务。用户访问的时候,输入域名就想直接到达对应服务。Nginx的强项就在这:它可以根据域名把请求路由到不同的虚拟主机,再根据路径把请求转发到不同的后端端口。这整个过程对外只暴露80和443两个端口,其他端口全部锁在内网或者本机回环地址上,这才是正经的多服务入口方案。
还有一层好处是证书统一。三个服务如果各自实现HTTPS,等于要写三套TLS逻辑,处理三套证书续期。但让Nginx统一接HTTPS后,服务后端只需要监听普通HTTP,证书和加密全部由Nginx这层完成。这样后端代码里不需要处理证书,开发环境也可以直接跑HTTP,减少很多不必要的复杂度。
1.2 这套方案的适用人群
往大了说,这套方案适合四类人:一是个人站长,手头好几个站点,服务器只有一台,想省成本;二是小团队开发,前后端分离,前端需要联调多个子域名;三是运维入门者,想搞清楚虚拟主机、反向代理、证书加载这几件事之间的关系;四是本地开发场景,想在电脑上用自定义域名访问虚拟机里的多个端口服务,就像热词里提到的“本地+虚拟机多端口Nginx开发环境多站点自定义域名配置”。
说实话,这些需求在本质上完全一样,区别只是证书来源和域名解析方式。公网环境用云厂商免费证书或者Let's Encrypt,本地环境用自签名证书加hosts映射。明白了这个区别,后面所有配置都能一套逻辑走通。
2. 配置前的总体规划:目录、域名与证书
2.1 先规划目录结构别急着写代码
很多人上来就把所有server块堆在nginx.conf一个文件里,短时间没问题,等域名一多就乱了。我现在的习惯是,每个域名独立一个配置文件,证书统一放在一个目录里,用域名做子目录隔开。
推荐目录结构:
/etc/nginx/ ├── nginx.conf ├── conf.d/ │ ├── api.example.com.conf │ ├── tools.example.com.conf │ └── files.example.com.conf └── certs/ ├── api.example.com/ │ ├── fullchain.pem │ └── privkey.pem ├── tools.example.com/ └── files.example.com/这种结构的好处是,以后要调整某个域名,只动一个文件;要排查问题,直接看对应conf文件就可以了。main配置里用include /etc/nginx/conf.d/*.conf;把全部虚拟主机加载进来。证书目录统一也好管理,备份的时候把整个certs目录拷走就行。
2.2 证书从哪来、怎么放
证书获取有三大主流渠道,按使用场景自己选:
第一种,云厂商免费证书。如果你用了云服务器,阿里云、腾讯云这些控制台里都能申请免费证书,申请的时候填要绑定域名,下载时选Nginx格式。这种证书一般有效期一年,到期后在控制台重新申请、替换文件、reload一下Nginx即可。优点是操作门槛低,web界面点点就行。
第二种,Let's Encrypt的certbot。服务器上安装certbot后,一条命令就能申请并自动配置证书,例如certbot --nginx -d api.example.com。这方式适合喜欢自动化的场景,证书续期也能用cron自动执行。需要注意,申请时要求该域名已解析到这台服务器,并且80端口可以被访问验证。
第三种,自签名证书。本地开发或者内网测试时用,用openssl自己生成,比如:
openssl req -x509 -nodes -newkey rsa:2048 \ -keyout privkey.pem \ -out fullchain.pem \ -days 365 \ -subj "/CN=dev.local"自签名的关键是生成时把Common Name和可选的SAN(Subject Alternative Name)填对,否则浏览器会报域名不匹配,也就是你经常遇到的那种NET::ERR_CERT_COMMON_NAME_INVALID。
2.3 证书格式与路径核对
Nginx下证书文件主要认PEM格式。申请下来以后,一般会得到两个文件,一个证书链(fullchain.pem或叫bundle),一个私钥(privkey.pem或key文件)。有少数情况会遇到pfx格式,需要先转换:
openssl pkcs12 -in cert.pfx -nocerts -out privkey.pem -nodes openssl pkcs12 -in cert.pfx -clcerts -nokeys -out fullchain.pem放好文件后,建议顺手做一次格式核对,避免后面加载失败:
openssl x509 -in /etc/nginx/certs/api.example.com/fullchain.pem -noout -text | grep -A1 "Subject Alternative Name"这一个命令就能看到证书到底绑定了哪些域名。很多证书配了但不生效的问题,就是在这个步骤发现的,明明给tools.example.com配了另一张证书,结果文件里写的是api.example.com的。
还要注意私钥权限,建议设置成600或者640,属主是nginx运行用户或者root。权限过大Nginx在启动阶段会直接报Permission denied。
3. 多域名虚拟主机:server_name的正确用法
3.1 server_name怎么匹配,别再搞混优先级
Nginx里区分多域名,靠的就是每个server块里的server_name指令。它的职责是对应HTTP请求头里的Host字段。比如用户访问http://api.example.com,请求头里带着Host: api.example.com,Nginx就去找有没有这个server_name,找到了就用这个server块处理,找不到就走默认规则。
匹配优先级从高到低依次是:精确匹配、通配符前缀匹配(*.example.com)、通配符后缀匹配(example.*)、正则匹配(~^api\.)。同一个域名的请求,优先走精确匹配的server块,这一点在配置多域名时非常重要。如果你写了一个server_name example.com *.example.com;,然后又单独写了一个server_name api.example.com;,那么访问api.example.com时,会优先命中精确匹配的那个块,而不是通配符那个。
另外,server_name是不允许出现下划线做正常域名使用的,虽然Nginx允许你写server_name _;,但这个下划线通常只用来作为“非正常请求”的兜底,不能当真实域名访问。很多新手拿server_name local_test;去访问,结果死活匹配不上,就是这个问题。
3.2 一定要设置default_server兜底
有了多域名,就一定会出现“请求的域名不在我预期列表里”的情况,比如有人直接用IP访问,或者你的域名到期被重新解析到别的服务。这时如果没有任何server块能匹配,Nginx的行为是选取默认server块来处理。默认server块可以自己指定,写法是在listen指令后面加default_server参数:
server { listen 80 default_server; listen 443 ssl default_server; server_name _; ssl_certificate /etc/nginx/certs/default/fullchain.pem; ssl_certificate_key /etc/nginx/certs/default/privkey.pem; return 444; }如果不主动配置default_server,Nginx会拿配置文件里第一个加载的server块来兜底。那就很容易出现一个尴尬场面:访问https://1.2.3.4时,浏览器提示证书不匹配,因为Nginx把A域名的证书返回给了没有域名的IP请求。设置default_server并返回444,是一个干脆又安全的做法,直接断开连接,不给任何响应。
3.3 本地开发环境:自定义域名加到hosts
说回本地开发场景。你在虚拟机里装了Nginx,宿主机想用多个自定义域名访问虚拟机里的多个服务,第一步是编辑宿主机hosts文件:
192.168.10.10 api.dev.com 192.168.10.10 tools.dev.com 192.168.10.10 files.dev.com再把虚拟机的Nginx配置成对应的server_name。这样浏览器输入http://api.dev.com就能到虚拟机,Nginx再把请求转发到本地对应端口。如果需要HTTPS,就用自签名证书,并在浏览器里手动信任。这个流程和在公网配置几乎一致,唯一区别是域名解析从DNS换成了hosts,证书从正规CA换成了自签名,其他全部通用。
4. 多证书加载与HTTPS跳转的完整写法
4.1 每个域名一套独立server块,最稳的配置方式
证书加载的核心是:每个域名对应的443 server块里,指向各自的证书文件。这句话听起来简单,实际配置中却经常被写乱。来看一个标准写法:
server { listen 443 ssl http2; server_name api.example.com; ssl_certificate /etc/nginx/certs/api.example.com/fullchain.pem; ssl_certificate_key /etc/nginx/certs/api.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; location / { proxy_pass http://127.0.0.1:8080; 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; } access_log /var/log/nginx/api.example.com.access.log; }多域名的时候,把这个server块整个复制一份,改掉server_name、证书路径、代理地址、日志路径。复制改配置是个好习惯,比在同一个server里塞多个location去处理多个域名清晰得多。这里的核心判断依据是:如果一个域名对应一个后端服务,那就是一对一的server块;如果一个域名下多个服务,那才需要考虑location拆分,这是两种不同的负载场景,不要混在一起设计。
4.2 HTTP自动跳转HTTPS的标准写法
加了HTTPS之后,旧链接访问http://api.example.com时,如果不处理会让用户踩到一半加密一半不加密的尴尬。最简单可靠的跳转,就是单独写一个80端口server块,统一301:
server { listen 80; server_name api.example.com tools.example.com files.example.com; return 301 https://$host$request_uri; }注意这里可以把多个域名写在一起,因为它们跳转逻辑相同,而且80端口不涉及证书问题。$host取自原始请求的Host字段,这样不管访问哪个域名,跳转后都会保持域名不变,不会串地址。如果后端服务还需要接收“用户原始请求是否是HTTPS”这个信息,就要靠前面配置里的X-Forwarded-Proto $scheme头传递,后端程序读到这个头就知道当前请求已经由Nginx完成HTTPS解密,可以放行,不用再担心重定向循环。
4.3 证书替换后为什么经常不生效
热词里有一条“nginx替换ssl证书不生效”,这个我太有体会了。第一次换证书的时候,我替换完文件愣是刷新半天都还是旧证书,后来发现原因五花八门,但核心就几类。
第一类是Nginx根本没有reload。替换证书文件后,必须执行nginx -s reload或systemctl reload nginx。很多人改完文件忘了这一步,或者以为nginx会自动监听文件变化,实际上不会。第二类是改了文件但改错了server块,尤其在统一管理多个配置文件时,可能在api的conf里改了tools的证书路径。第三类是nginx -t虽然通过了,但Nginx启动时用的还是旧进程,因为reload失败的情况下旧配置不会退出,这时候要去看错误日志。
第四类是浏览器缓存和HSTS。Chrome和Firefox对证书信息有缓存,如果之前访问过某个域名并记住了HTTPS状态,即使Nginx换了证书,浏览器在短时间内可能还是用旧缓存。解决方式是换一个无痕窗口测试,或者等缓存过期。HSTS更麻烦,如果之前下发过Strict-Transport-Security头,浏览器会强制HTTPS,这阶段证书有问题的话会直接打不开,不要被吓到,先在无痕模式里排查。
第五类才是真正的证书文件问题:证书链不完整、私钥和证书不匹配、文件权限不对。判断方法很简单,用openssl测试一遍即可:
openssl s_client -connect api.example.com:443 -servername api.example.com这条命令返回的信息里会明确显示证书链是否完整、证书是否在有效期内、域名是否匹配。配合nginx -t和/var/log/nginx/error.log,几乎能覆盖所有证书不生效的排查方向。
5. 多服务反向代理:location与proxy_pass实战
5.1 按子域名还是按路径区分服务,怎么选
多服务的暴露方式有两种主流方案,一个子域名一个服务,或者一个域名下不同路径对应不同服务。
子域名方案的优势是证书和server块天然隔离,每个服务互不影响,这适合服务边界清晰的场景,比如api、tools、files三兄弟。路径方案的典型场景是只有一个主域名,比如example.com下/api走Java,/admin走Node,/files走文件服务。路径方案只需要一张证书,但location规则复杂,而且容易和后端自己的路由冲突,比如后端本身就用/api作为接口前缀,那location和proxy_pass的配合就要非常小心。
我的建议是:能上子域名就上子域名,实在只有一个域名时才用路径拆分。子域名在证书续期和故障排查上都有天然优势,你不需要在同一个server块里去猜哪条location对应哪个服务。
5.2 location的匹配优先级,别再凭感觉写
location匹配是反向代理里最核心也最容易写错的地方。Nginx内部执行顺序,按优先级从高到低是:精确匹配(=)最高,然后是前缀匹配且带^~修饰符的,再到正则匹配(~或~*),最后才是普通前缀匹配。
举几个实际例子。location = /health只匹配/health这一个路径;location ^~ /static/匹配以/static/开头的路径,而且匹配后不再检查正则;location ~ \.php$会匹配以.php结尾的URL;普通写法的location /api/则配广泛前缀。
有一个很经典的坑,是前端项目里常见的:配置了location /api/ { proxy_pass http://127.0.0.1:8080; },然后又写了一个location / { proxy_pass http://127.0.0.1:3000; }用来承接纯静态页面。这时候请求/api/login会命中/api/,请求根路径会命中/,看起来没问题,但如果你把正则加进去,比如location ~* \.(js|css)$ { root /data/static; },那就要仔细想想正则与普通前缀谁会先执行,Nginx在这个坑里翻车的人不在少数。记住一个原则:正则匹配永远在普通前缀匹配之后执行,但^~能阻止正则继续搜索。
5.3 proxy_pass的斜杠陷阱,404的真正来源
反向代理最隐蔽的坑,是proxy_pass目标地址末尾有没有斜杠。这直接影响转发给后端服务时的URI。看两个典型写法:
location /api/ { proxy_pass http://127.0.0.1:8080; }这个写法中,proxy_pass末尾不带斜杠,转发给后端时保留完整URI,后端看到的是/api/login。适合后端本身就是按/api前缀开发的接口服务。
location /api/ { proxy_pass http://127.0.0.1:8080/; }这个写法中,proxy_pass末尾带了斜杠,Nginx会把location匹配到的那一段去掉,后端看到的是/login。适合后端本身没有/api这个前缀、只是前端对外用/api来区分服务的场景。
这两种写法没有绝对的对错,取决于你后端的路由设计。但如果你配完之后发现部分接口404,优先检查这里,大概率是斜杠问题。我早期调试时,前端/api/login到后端变成了//login或者/login,各种莫名其妙的路径错位,都是这个细节造成的。
还要注意代理时头信息的传递。后端服务如果想获取用户的真实IP、真实协议,你得把下面这一段配上:
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;尤其是X-Forwarded-Proto,如果后端是Spring这类框架并且自己做了HTTPS重定向,没有这个头的话,它会认为原始请求是HTTP,然后强制跳转,结果就是你反复体验过的301循环。
5.4 几个高频服务的代理写法实战
代理Node.js服务,常规写法:
location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; }代理WebSocket服务,比如在线聊天、实时通知:
location /ws { proxy_pass http://127.0.0.1:3001; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; }WebSocket的关键是Upgrade和Connection两个头,不写的话连接会一直断。proxy_read_timeout也要调大,默认60秒很容易导致长连接被切断。
代理内网AI服务,比如Ollama这类本地推理服务:只需要把Nginx配置成统一入口,转发到本机11434端口即可:
location / { proxy_pass http://127.0.0.1:11434; 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_read_timeout 600s; }本地模型推理耗时长,proxy_read_timeout一定要拉到几百秒以上,否则Nginx会在模型还没返回结果时就主动断开,给用户一个504。
6. 一份可直接抄作业的完整配置模板
6.1 全局配置段先看一眼
先说主配置文件/etc/nginx/nginx.conf里需要关注的部分。系统默认的nginx.conf整体够用,重点留意下面几个关键点:
user nginx; worker_processes auto; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; gzip on; include /etc/nginx/conf.d/*.conf; }很多人会忽略worker_connections和keepalive_timeout。连接数按需调,常规小项目worker_connections 1024足够;keepalive_timeout保持默认或者调到65秒都比较稳妥。关键在最后一行include /etc/nginx/conf.d/*.conf;,确保你新建的conf文件都放在这个目录里。
6.2 一个域名的完整server组合,直接复制改
下面是一套完整的、能直接抄走的域名配置,我加了详细注释。
# 域名: api.example.com # 后端: http://127.0.0.1:8080 # 80端口统一跳转HTTPS server { listen 80; server_name api.example.com; return 301 https://$host$request_uri; } # 443端口入口,加载证书,代理后端 server { listen 443 ssl http2; server_name api.example.com; ssl_certificate /etc/nginx/certs/api.example.com/fullchain.pem; ssl_certificate_key /etc/nginx/certs/api.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; access_log /var/log/nginx/api.example.com.access.log; error_log /var/log/nginx/api.example.com.error.log; location / { proxy_pass http://127.0.0.1:8080; 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; } }第二个域名tools.example.com就复制这个文件,把server_name、证书路径、代理端口、日志路径全部替换。每个域名独立文件的好处是:以后换证书时,只需要改对应文件里的证书路径再reload即可,绝不会误伤其他站点。
6.3 reload与验证流程,这条流程必须在换配置后走一遍
配置文件写好后,完整流程我建议按这个来:
nginx -t这个命令会检测所有配置语法是否正确,显示syntax is ok和test is successful才能继续。配置有错时Nginx会明确指出是哪一行。
然后重新加载配置:
nginx -s reloadreload和restart区别很大。reload会先检查配置,成功则平滑重载,不中断当前连接;失败则继续用旧配置,这很安全。所以我已经很久不用nginx -s restart了,reload就够。
接着验证跳转和证书:
curl -I http://api.example.com这能看到是否返回301,Location头是否正确指向HTTPS。
curl -I https://api.example.com确认返回正常状态码,比如200。
openssl s_client -connect api.example.com:443 -servername api.example.com确认证书链和域名匹配。这套流程走完,一个域名的配置就算真正落地了。
7. 常见问题与排查技巧实录
7.1 高频问题速查表
这么多年的配置经验,我把最常踩的问题整理成一张速查表,适合打印出来贴显示器上。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
访问域名报NET::ERR_CERT_COMMON_NAME_INVALID | 证书域名与访问域名不匹配 | 核对证书SAN,重新申请匹配域名的证书 |
| 替换证书后还是显示旧证书 | 未reload、浏览器缓存 | 执行nginx -s reload,用无痕窗口测试 |
| 配置了HTTPS但HTTP不跳转 | 缺少80端口server块 | 添加return 301 https://$host$request_uri; |
| 301重定向循环 | 后端自身强制HTTPS且Nginx未传X-Forwarded-Proto | 给location加proxy_set_header X-Forwarded-Proto $scheme; |
| 502 Bad Gateway | 后端服务未启动或端口错误 | ss -lntp检查端口,确认后端进程 |
| 504 Gateway Timeout | 后端处理超时 | 调大proxy_read_timeout |
| 404 Not Found | proxy_pass末尾斜杠错误或location匹配错了 | 检查proxy_pass是否带斜杠,确认location规则 |
| 静态文件403 | 目录权限不足 | 给目录加r权限,或检查index配置 |
| WebSocket连不上 | 缺少Upgrade头 | 配置proxy_set_header Upgrade $http_upgrade; |
| IP直接访问返回错误证书 | 未配置default_server | 配置default_server,返回444或跳转 |
7.2 我的独家排查套路
最后分享一个我用了很久的排查套路。遇到多域名代理问题,我永远先做三件事:看错误日志,跑curl,确认server_name命中情况。
第一步是tail -f /var/log/nginx/error.log,这里会记录几乎所有底层错误,包括证书权限、上游连接失败。第二步是curl -v http://api.example.com,看返回头里的Server和Location字段,快速判断是否被Nginx处理。第三步是最容易被忽略的:确认当前请求到底进入了哪个server块。方法是临时在一个server块的location里加一行add_header X-Debug-Server api;,然后用curl -I看响应头,立刻就知道是哪个server块在处理请求,这个技巧在处理多域名串证书、跳转异常时极其有用。
再补充一个小经验:给每个域名配置独立的access_log,比如access_log /var/log/nginx/api.example.com.access.log;。日志一分开,哪个域名有异常流量、哪个域名出现了大量4xx,一眼就能看出来。而且以后做日志分析的时候,分文件收集数据也方便得多。很多小问题看着玄乎,实际把日志分开后,线索立刻就清晰了。