Nginx多域名多证书多服务配置实践,一台服务器承载多套业务
1. 整体设计思路一台服务器如何承载多套业务很多朋友第一次接触“多域名、多证书、多服务”这几个词觉得是把Nginx配置搞得特别复杂。其实拆开来看Nginx处理多站点的方式和执行多任务差不多——它靠的是“请求头部里带的域名来分流”不是靠多台机器堆出来的。只要把server_name、listen、proxy_pass这几个核心指令理解透一台服务器带十几个域名、十几个SSL证书、运行十几个后端服务完全能安排得明明白白。先理清一个最底层的逻辑Nginx收到一个请求后会经历“选Server块 → 匹配Location → 执行反代或返回静态文件”这条链路。多域名靠多个server块区分多证书靠每个server块里单独配ssl_certificate多服务靠location段里的proxy_pass指向不同后端端口或Unix Socket。这三件事是互相独立的你可以自由组合同一个IP上挂10个域名每个域名用独立证书每个证书背后接不同的服务。这就是Nginx作为反向代理网关最经典的使用方式也是很多生产环境的标配架构。那“多证书”又怎么理解两个层面一是多个域名各自有独立的证书文件配置里每块各写各的二是某些证书体系下一个域名可能同时支持RSA和ECC两套证书Nginx从某个版本开始支持同时配置两套密钥对自动协商。这个后面我会专门讲属于容易踩坑但收益很明显的点。适合谁看这份配置方案如果你手头有一台云服务器上面跑了几个Web应用有的是Java写的、有的是Python写的、有的就是静态页面你想让它们共用80和443端口对外看起来像几个独立网站——那这篇文章就是给你准备的。我会把从目录规划、证书放置、配置编写到重启验证的完整链路全部过一遍配置代码可以直接“抄作业”同时也会解释每一条指令为什么这么写避免你改个路径就翻车。在开始之前先打个预防针。Nginx配置本身不难难的是“你以为配好了其实没生效”。最常见的翻车现场有三种改了配置忘了测试语法直接重启导致服务挂掉、证书路径写错结果浏览器提示不安全、多个server块里server_name写重复导致请求全跑进第一个块。这三类问题我后面都会给出对应的排查套路你先有个印象实操时就不会慌。2. 实操前的准备目录规划与证书约定2.1 一套少走弯路的目录结构我见过很多新手把证书文件、配置文件随意扔在/etc下面最后自己都找不到哪个证书对应哪个域名。我的建议是从一开始就按域名隔离让每个站点的配置、证书、日志都归拢到一个目录下。下面这套结构我在多台服务器上用过稳定、清晰、好维护/etc/nginx/ ├── nginx.conf # 主配置只做全局设置和include ├── conf.d/ │ └── default.conf # 兜底配置处理未匹配域名的请求 ├── sites-available/ # 实际站点配置存放处 │ ├── blog.example.com.conf │ ├── shop.example.com.conf │ └── api.example.com.conf ├── sites-enabled/ # 软链接到sites-available │ ├── blog.example.com.conf - ../sites-available/blog.example.com.conf │ ├── shop.example.com.conf - ../sites-available/shop.example.com.conf │ └── api.example.com.conf - ../sites-available/api.example.com.conf └── ssl/ ├── blog.example.com/ │ ├── fullchain.pem │ └── privkey.pem ├── shop.example.com/ │ ├── fullchain.pem │ └── privkey.pem └── api.example.com/ ├── fullchain.pem └── privkey.pem这套结构里sites-available放“可用配置”sites-enabled放“启用配置”通过软链接关联。想临时下线一个站点直接删掉软链接比改配置文件再重载干净得多。ssl目录按域名再套一层是为了防止多个证书文件重名互相覆盖。做个额外提醒有些系统安装Nginx后默认会在conf.d/下有个default.conf或者没有sites-available目录。你可以按上面的结构自己创建然后在nginx.conf的http块里加上一句include /etc/nginx/sites-enabled/*.conf;这样主配置保持瘦身站点配置全部走include加载。这个步骤务必确认否则你放在sites-enabled里的配置永远不生效。2.2 证书获取与部署前的几个必查项现在做HTTPS证书基本都用ACME协议的自动签发工具免费证书90天续期一次配合自动续期脚本基本做到“装上就忘”。但有几个细节值得提前确认第一证书文件最好下载fullchain.pem注意它包含的是“站点证书 中间证书”很多自动签发工具会同时生成cert.pem、chain.pem、fullchain.pem、privkey.pem。Nginx的ssl_certificate指令必须指向fullchain.pem如果你只填cert.pem浏览器会报“证书链不完整”因为中间证书没有下发。我见过不少人在这里栽跟头症状是PC端访问时好时坏手机端干脆打不开。第二私钥文件的权限要收紧。Nginx主进程以root启动但worker进程通常以nginx用户运行。私钥文件建议设为600所有者root即可这样只有root能读。如果你把私钥放到网站根目录或者设置为644一旦网站有上传漏洞私钥泄露等于整个HTTPS防线崩溃。第三多域名的自动签发工具支持SAN证书也就是一张证书里包含多个域名比如blog.example.com和www.blog.example.com放在一张证书里。这种方式适合同一个主站下的几个子域但如果你有业务隔离的需求比如商城和博客完全独立我个人更推荐每个域名单独签发证书。好处是某个证书过期需要重新申请时不影响其他站点排错也更容易判断是哪个域名出了问题。证书部署到位后可以先用openssl命令快速检查文件内容是否正常openssl x509 -in /etc/nginx/ssl/blog.example.com/fullchain.pem -noout -subject -issuer -dates如果能看到证书的Subject和有效期说明文件是有效的PEM格式。如果提示unable to load certificate大概率是文件下载不完整或者路径写错。2.3 端口规划与后端服务约定多服务场景下后端服务一般跑在内网端口比如Spring Boot跑8080、Node.js跑3000、Python应用跑8000。Nginx作为对外入口只暴露80和443内部服务不直接对公网开放这样既统一了入口又增加了安全隔离。这里有一个值得养成的习惯把后端服务绑定到127.0.0.1而不是0.0.0.0。很多开发框架默认监听所有网卡这意味着如果你在服务器上开了防火墙但没限制端口外部可以直接访问8080等端口绕过了Nginx。把服务绑定到127.0.0.1之后只有本机Nginx能访问到后端外部的包根本到不了这个端口相当于天然加了一道锁。后端服务之间尽量不要端口冲突或者使用某种固定规则。比如约定“每个项目在启动时通过环境变量读取PORT默认值各不相同”这样可以避免后期新增服务时端口号乱套。Nginx侧只需要记住一个端口对应哪个服务即可后面配置里会用到。3. 核心配置拆解一个多站点Nginx是怎么组织起来的3.1 主配置nginx.conf的合理瘦身很多默认安装的nginx.conf非常臃肿里面塞了一堆用不到的配置。我的习惯是保留核心全局设置其他全部丢到站点级配置里。下面是一份基础但够用的主配置user nginx; worker_processes auto; error_log /var/log/nginx/error.log warn; pid /run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for; access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; # 限制上传大小反向代理场景很关键 client_max_body_size 20m; # Gzip压缩 gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xmlrss text/javascript; gzip_min_length 1024; # 站点配置入口 include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*.conf; }这里每个参数都可以说两句。worker_processes auto让Nginx按CPU核数自动启动worker进程比手动写死数字更省心。gzip_min_length 1024意思是小于1KB的文件不压缩因为压缩本身有开销小文件压了反而得不偿失。client_max_body_size 20m要根据你的业务调整如果后面要上传大文件忘了改这行请求会被Nginx直接拒掉状态码413很多人查半天不知道是这里的问题。把站点配置独立出去之后主配置基本就不用动了。增加一个新站点就是往sites-available里放一个文件、建一个软链接然后nginx -t nginx -s reload整个流程几分钟完成。3.2 多服务反代的站点配置模板下面这段是整篇的核心多域名、多证书、多服务全部体现在一个站点配置文件里。我以三个站点为例一个博客静态文件、一个商城Java后端8080、一个API服务Node后端3000三个域名各不相同证书也相互独立。第一个站点博客纯静态不经过后端。server { listen 80; server_name blog.example.com; # 强制跳转HTTPS return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name blog.example.com; ssl_certificate /etc/nginx/ssl/blog.example.com/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/blog.example.com/privkey.pem; root /var/www/blog; index index.html; location / { try_files $uri $uri/ 404; } access_log /var/log/nginx/blog.access.log main; }第二个站点商城Java服务跑在127.0.0.1:8080。server { listen 80; server_name shop.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name shop.example.com; ssl_certificate /etc/nginx/ssl/shop.example.com/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/shop.example.com/privkey.pem; 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/shop.access.log main; }第三个站点API服务Node.js跑在127.0.0.1:3000并且对接口路径做了前缀剥离。server { listen 80; server_name api.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name api.example.com; ssl_certificate /etc/nginx/ssl/api.example.com/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/api.example.com/privkey.pem; location /api/ { proxy_pass http://127.0.0.1:3000/; 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; proxy_connect_timeout 10s; proxy_read_timeout 60s; } access_log /var/log/nginx/api.access.log main; }你可能会注意到每个域名我都写了两个server块一个处理80端口跳转一个处理443端口真正提供服务。这是目前最主流的写法因为用户习惯直接输入域名不带协议头浏览器默认走80端口如果80端口没有响应体验就很糟糕。return 301 https://$host$request_uri这行把所有HTTP流量永久重定向到HTTPS注意$host用的是原始域名$request_uri保留原始路径这样用户从http://shop.example.com/goods/123进来会被带到https://shop.example.com/goods/123路径不丢失。再说说API这块的proxy_pass结尾斜杠。proxy_pass http://127.0.0.1:3000/;和proxy_pass http://127.0.0.1:3000;差别很大带斜杠时location /api/匹配的前缀会被替换成斜杠后的路径比如请求/api/users实际转发到后端的/users不带斜杠时路径原样传递后端收到的还是/api/users。这是一个经典易错点务必根据后端路由决定要不要加斜杠。3.3 多证书并存RSA与ECC双证书配置前面提到多证书还可能指“同一域名下同时部署RSA和ECC两套证书”。为什么要这么干老客户端不认ECC证书新客户端用ECC证书性能更好、握手更快。Nginx支持在同一个server块里配置两套证书根据客户端能力自动选择合适的证书。写法是server { listen 443 ssl http2; server_name blog.example.com; # RSA证书 ssl_certificate /etc/nginx/ssl/blog.example.com/rsa/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/blog.example.com/rsa/privkey.pem; # ECC证书优先级更高兼容优先时会自动选择 ssl_certificate /etc/nginx/ssl/blog.example.com/ecc/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/blog.example.com/ecc/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers on; root /var/www/blog; index index.html; }注意一个细节当同时配置RSA和ECC两套证书后ssl_ciphers里的套件要同时包含ECDSA和RSA两种否则部分客户端协商时会失败。ssl_prefer_server_ciphers on表示优先使用服务端定义的套件顺序而不是客户端偏好这样可以在一定程度上固定加密套件避免老旧弱套件被选中。还有一点很容易被忽略双证书配置下自动续期工具要分别维护两套证书。如果用的ACME客户端请确认它在签发ECC证书时不会覆盖RSA证书一般工具会通过--key-type参数区分或者把两套证书放在不同目录。续期脚本也要分别运行不要混在一起。3.4 泛解析与default_server的兜底策略多域名场景里还有一个实用技巧如果你有多个子域名希望统一走向同一个服务或者想给未匹配的域名一个友好提示就要用到泛解析和默认服务器。泛解析的server_name写法是*.example.com它可以匹配blog.example.com、shop.example.com等任意子域名但注意它不匹配example.com本身。如果你想把主域名也纳入管理可以写两行server_name example.com *.example.com;default_server则是“兜底”当所有server块的server_name都匹配不上时Nginx会使用标记了default_server的监听块。这对于IP直连、未备案域名指向服务器、或者有人把别的域名解析到你IP上这些场景特别有用。配置方式server { listen 80 default_server; listen 443 ssl default_server; server_name _; ssl_certificate /etc/nginx/ssl/default/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/default/privkey.pem; return 444; }server_name _;是一个不匹配任何真实域名的写法return 444是Nginx特有的响应码表示直接断开连接不给任何响应。这样做的好处是扫描器、恶意探测、乱解析的域名访问过来时不会得到任何Web内容连错误页都没有从攻击者的视角看就像端口没开一样。4. 实操全程实录从零到全部站点跑通4.1 步骤一准备证书与目录按前面的规划先建目录并放证书mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled /etc/nginx/ssl/blog.example.com /etc/nginx/ssl/shop.example.com /etc/nginx/ssl/api.example.com # 假设证书文件已经下载到/root/certs目录 cp /root/certs/blog/fullchain.pem /etc/nginx/ssl/blog.example.com/ cp /root/certs/blog/privkey.pem /etc/nginx/ssl/blog.example.com/ chmod 600 /etc/nginx/ssl/blog.example.com/privkey.pem # 其他站点同理如果你用的自动签发工具文件会生成在/etc/letsencrypt/live/域名/目录下Nginx配置里直接引用这个路径也可以。但个人体验是单独复制到ssl目录下更灵活因为后续换证书服务商时不需要动Nginx配置只需要更新文件。4.2 步骤二写入配置并做语法校验将前面的站点配置分别写入对应文件之后执行校验nginx -t如果输出nginx: configuration file /etc/nginx/nginx.conf test is successful说明语法没问题。如果有错误会明确提示哪个文件第几行出了问题。我发现一个高效的做法是先写好一个站点的配置校验通过后再复制扩展成其他站点。一次写三个文件出错时容易混淆逐个来反而更快。这一步还有个值得强调的操作习惯校验前先用编辑器或vscode确认文件编码为UTF-8无BOMBOM会导致Nginx解析第一行时出现不可见字符报错信息指向行号但你在那行看不出任何问题。4.3 步骤三启用站点并重载确认语法无误后建立软链接并重载ln -s /etc/nginx/sites-available/blog.example.com.conf /etc/nginx/sites-enabled/blog.example.com.conf ln -s /etc/nginx/sites-available/shop.example.com.conf /etc/nginx/sites-enabled/shop.example.com.conf ln -s /etc/nginx/sites-available/api.example.com.conf /etc/nginx/sites-enabled/api.example.com.conf nginx -t nginx -s reload这里要再三提醒生产环境不要用nginx -s stop再nginx启动的方式更新配置因为stop瞬间会断开所有连接reload会让Nginx平滑地用新配置启动新worker进程旧worker处理完已有连接后再退出用户基本无感知。这是上线部署的基本素养。4.4 步骤四验证多域名、多证书是否真正生效配置重载后至少要做三个维度的验证第一用curl验证HTTP跳转是否正常curl -I http://blog.example.com预期能看到301 Moved Permanently并且Location头是https://blog.example.com/。第二用curl验证HTTPS证书是否正确下发curl -I https://blog.example.com输出里如果正常显示HTTP状态码200或相应业务码说明证书被接受。如果想更仔细地确认证书内容和域名匹配可以加-v参数输出里会有SSL connection using TLSv1.3和证书信息。第三用浏览器分别打开三个域名确认地址栏锁标记正常、页面内容正确。如果是前后端分离架构顺便按F12打开开发者工具看一下某个接口的请求落到哪个域下是否能正常响应。如果某个域名跳到了另一个站点的内容基本可以确定是server_name写重复或者include顺序导致匹配错误回到配置文件去查。5. 常见坑位与排查实录5.1 证书链不完整与证书过期这个我前面提过再展开一下。如果浏览器报“您的连接不是私密连接”或“NET::ERR_CERT_AUTHORITY_INVALID”同时你用openssl s_client连接时看到verify error:num20:unable to get local issuer certificate那就是证书链不完整。解决办法是使用fullchain.pem而不是cert.pem。另外证书过期是最容易忽视的特别是自动续期失败时。可以写一个简单的cron任务每天检查证书有效期比如0 6 * * * /usr/bin/openssl x509 -enddate -noout -in /etc/nginx/ssl/blog.example.com/fullchain.pem如果快过期系统会通过邮件提醒提前处理总比用户访问报错再排查要好。5.2 server_name匹配优先级与default_server的坑Nginx在匹配server_name时遵循以下优先级精确匹配 通配符前缀*.example.com 通配符后缀www.* 正则匹配。如果都没有匹配到则走default_server。很多新手因为配了两个server_name blog.example.com的块导致请求总是落到第一个块调整半天无果。解决办法全局搜索server_name确认重复。另外listen 443 ssl;和listen 443 ssl default_server;两个块如果server_name不同也偶尔出现“第一个块成为默认块”的行为所以兜底块务必显式加default_server。5.3 后端服务连接被拒与超时如果Nginx返回502 Bad Gateway说明Nginx无法连接后端服务。排查顺序是先看后端进程是否存活再看后端是否只监听了127.0.0.1然后用curl http://127.0.0.1:8080确认本机直连是否正常。如果本机正常但Nginx代理502检查proxy_pass里的IP和端口是否正确以及SELinux或AppArmor是否拦截了Nginx的网络访问。某些系统上强制安全模块会阻止Nginx发起对外连接需要调整策略或放行这个容易忽略。返回504 Gateway Timeout则说明后端处理太慢超过了proxy_read_timeout。可以按业务需要调大这个值或者检查后端是否出现慢SQL、死锁等性能问题。5.4 性能优化从够用到好用上线稳定之后可以做一些微调让Nginx跑得更舒服开启HTTP/2listen 443 ssl http2;多路复用能明显提升并发资源加载效率。Nginx 1.25.1及以上版本支持http2 on;的写法如果你用的版本较老用listen ... http2参数也没问题。开启OCSP Stapling由Nginx代为查询证书状态并缓存减少客户端和CA的交互握手更快。开启keepalive到后端的连接池在upstream块里配置keepalive 32;并在location里设置proxy_http_version 1.1;和proxy_set_header Connection ;减少频繁建立TCP连接的开销。合理配置worker_connections这个值乘以worker进程数决定了最大并发连接数。默认1024对很多站够用但如果你的业务是长连接或WebSocket建议调大。5.5 日志切割与空间清理访问日志增长快是个隐性坑。默认access.log在流量大时一天上百MB几个月不处理磁盘直接爆掉。建议用logrotate做按天切割保留30天同时开启compress压缩历史日志。Nginx自身的错误日志也建议定期检查很多问题在error.log里会留下明确线索比如[warn]级别的“conflicting server name”提示。5.6 防火墙与安全组服务器本地的防火墙如firewalld或ufw只放行80和443其他端口一律拒绝。云服务商的安全组也只需放行80和443如果只使用HTTPS甚至可以只放行443。注意如果后端服务被绑定到127.0.0.1那么即使防火墙没限制8080端口外部也访问不到这是双重保险。另外可以考虑启用Nginx的limit_req模块做接口限流对API域名的未知路径做访问频率限制能挡掉一部分恶意请求。6. 额外心得与几个压箱底的小技巧如果这套配置你已经完全跑通我再分享几个日常维护中真正提升幸福感的小操作。第一给每个站点配置独立的access_log。统一日志虽然省事但一旦某个站点访问量异常或者需要排查某个用户的请求在总日志里grep非常痛苦。独立日志后排查范围缩小到一个文件配合grep加时间范围基本秒出结果。第二写一个简单的部署脚本把“新建站点”这个动作半自动化。每次手动复制配置文件、改域名、建软链接容易在域名上写错。脚本可以这样设计输入域名和后端端口自动生成配置模板、建目录、做语法检查全部完成后提示是否重载。这样批量新增站点时非常高效也能避免手误。第三多域名多证书环境下证书续期务必做一次“演练”。不要等证书自动续期脚本跑失败了才去研究。你可以手动把某个证书备份后删除然后执行续期命令看它能否正确重新生成并触发Nginx重载。如果自动续期完成后没有执行nginx -s reload证书文件虽然更新了但Nginx还在用旧证书直到重启才生效这会导致“证书文件明明有效但网站报错”的诡异问题。第四也是我最想强调的配置完成后把整个/etc/nginx目录打包备份。改配置之前先tar -czf /backup/nginx_backup_$(date %F).tar.gz /etc/nginx然后随便改。出了问题一条命令恢复不用凭记忆反推哪里改错了。这个习惯帮我避免过好几次严重的线上事故。多域名、多证书、多服务在Nginx上的配置归根到底没有高深的技术核心就是结构清晰、文件隔离、先校验再重载。把这三点做到位后续做再多的站点、再多的证书都只是重复劳动而已不会有什么风险。你在实操过程中如果遇到其他诡异问题建议先看error.log再逐条检查server_name、证书路径、proxy_pass三样东西九成问题都出在这三处。