做运维这些年配置虚拟主机绝大多数时候都在跟域名打交道但有一类需求绕不开基于IP的场景客户拿着独立IP的SSL证书要求绑定内网有多套环境要用不同IP做物理隔离服务器上买了多个公网IP想分别承载不同业务。今天把基于IP的Nginx虚拟主机配置整理成第二篇总结重点讲清楚多IP地址怎么分配、listen指令的绑定写法、和域名虚拟主机混用时的匹配规则以及我实测下来最容易踩的几个坑。这篇偏实战命令和配置文件都能直接拿去用。1. 先把概念理清基于IP的虚拟主机到底解决什么问题1.1 三种虚拟主机方式的分工Nginx同一台服务器上跑多个站点主要靠三种方式区分基于IP、基于域名、基于端口。很多教程把它们并列讲实际处理问题时逻辑并不一样。基于域名一组IP上挂多个站点Nginx通过HTTP请求里的Host头或者HTTPS里的SNI扩展来区分。这是最常见的做法也是Nginx官方推荐的默认方式。基于端口同一个IP用8080、8081等不同端口区分服务。适合没有域名、纯内网调试的场景缺点是访问时要带端口号体验差外网发布也不方便。基于IP每个站点独占一个IP地址。访问哪个IP就进哪个站点不需要依赖Host头也不看SNI。这篇聊的就是这种。这三种方式在Nginx里并不是完全互斥的后面会讲到它们经常组合使用。1.2 什么场景才真正需要基于IP的虚拟主机从我接触过的需求来看这么几种情况会逼着你从域名方案转过来第一老客户端不兼容SNI。SNI是TLS握手时用来告知服务器域名的扩展2010年以前出的不少客户端、老版本库都不支持。一台服务器上多个HTTPS证书时没有SNI的客户端连上来服务器根本不知道要给它哪个证书。解决办法就是每个站点独占一个IP443端口上只放一个server块证书按IP来选。第二证书和IP绑定的合规要求。部分金融机构、政企内部系统的安全审计要求一个证书对应一个独立IP不接受SNI方案。这种只能老老实实上IP虚拟主机。第三内网多环境隔离。测试环境经常有两三套相同的系统跑在同一台机器上数据库、缓存、服务端口全都一样用域名区分还得改Hosts。给每套环境单独分配一个内网IPNginx直接按IP分发谁都不用改配置。第四机房或云上为特定业务单独申请的公网IP。有些场景要求对外访问有独立IP比如某些第三方回源要求固定IP白名单独立IP后某个站被攻击也不会殃及同服务器的其他站点。反过来也要说句公道话IPv4地址紧张、公网IP费用不低、后续IP要迁移时牵扯一堆关联配置所以不是非用IP不可的业务尽量还是用域名方案。基于IP虚拟主机适合刚需不适合炫技。2. 动手之前先让服务器拥有多个IP基于IP虚拟主机的前提是服务器上真的存在多个可用的IP地址。这一步最容易被忽略很多人Nginx配置写得没问题结果系统里压根只有一个IP自然看不到预期效果。2.1 多IP的三种来源方式物理多网卡。服务器上插两块网卡每块网卡配一个IP两条物理链路互不干扰可以做网络层面的流量隔离。缺点是要额外占用硬件资源机房加网卡通常还要提工单、重启成本高一般服务器不会常备多块网卡。单网卡多IP别名。一张物理网卡上绑定多个IPLinux里习惯叫IP别名。实现简单不额外占硬件是目前我用的最多的一种方式尤其适合内网测试机。云平台辅助IP。现在的云服务器大多支持在主网卡上额外绑定辅助私网IP前提是底层网络能正常路由。在云控制台配置后还要到操作系统内手动把IP配上。这种方式的好处是弹性好随时在控制台增减。2.2 临时与永久配置多IP的方法先看当前机器有哪些IPip addr show临时添加一个IP重启失效适合调试sudo ip addr add 192.168.1.10/24 dev eth0 sudo ip addr add 192.168.1.11/24 dev eth0验证一下ip addr show eth0这样192.168.1.10和192.168.1.11就同时出现在eth0上了。如果不想临时生效删除命令也顺带记一下sudo ip addr del 192.168.1.10/24 dev eth0临时配置在测试Nginx配置时非常方便确认无误后再去改系统的永久配置。永久配置要看操作系统。CentOS/RHEL系列如果用NetworkManager推荐用nmclisudo nmcli connection modify eth0 ipv4.addresses 192.168.1.10/24 sudo nmcli connection up eth0注意这就是在原有IP基础上叠加一个IP。老一点的CentOS 6或非NetworkManager环境要改ifcfg-eth0在里面追加IPADDR1IPADDR1192.168.1.10 PREFIX124Ubuntu 18.04以后的netplan直接在网卡配置里列多地址network: version: 2 ethernets: eth0: addresses: - 192.168.1.1/24 - 192.168.1.10/24 - 192.168.1.11/24改完执行sudo netplan apply然后ip addr show确认。2.3 公网IP与私网IP的注意点如果是内网私网IP在网卡上随便加、不冲突就行如果是公网IP情况复杂很多。公网IP的路由一般掌握在网络提供商手里你只在服务器网卡上配IP但对方没有把对应网段的二层/三层路由放通流量根本到不了这台服务器。所以云上或机房场景的辅助公网IP必须严格按照云控制台或网络工单给的参数来配子网掩码、网关千万别自己拍脑袋。曾经有人把某云平台辅助公网IP当作私网IP填写掩码写错结果IP在网卡上能看见外网却永远ping不通只能删掉重配。一个小经验配置前先和网络管理员确认这个IP段是否已经路由到本机一般在机房或者云上都是配好的你只需要在系统里加上IP就能通。配置后用ping、ip neigh这些命令先验证网络层通不通再去碰Nginx不然排除问题的时候会绕一大圈。3. Nginx基于IP虚拟主机的核心配置3.1 listen指令的IP绑定方式Nginx的server块通过listen指令决定监听的地址和端口。最常见的listen 80;其实是一个简写它等价于监听所有本地IP的80端口也就是0.0.0.0:80。基于IP的虚拟主机配置核心就是给不同的server块指定不同的IP地址server { listen 192.168.1.10:80; server_name site-a.local; root /var/www/site-a; } server { listen 192.168.1.11:80; server_name site-b.local; root /var/www/site-b; }访问192.168.1.10时进入第一个站点访问192.168.1.11时进入第二个站点。这里的server_name更多是标识作用因为IP已经精确绑定请求到达对应IP时不需要靠Host头来区分就能落进正确的块。一个容易被新手忽略的行为listen写192.168.1.10:80和写80对server块的选中逻辑有本质区别。监听特定IP的socket在Nginx内部优先级高于通配地址。假设你有一个server块listen 80叫默认块另有一个server块listen 192.168.1.10:80叫特定块那么访问192.168.1.10时Nginx会优先选特定块访问其他IP时才落到默认块。这个机制正好支撑了一套配置里混用IP虚拟主机和默认站点的做法。3.2 一套可直接复制的多站点配置实例实际生产我习惯每个站点拆一个配置文件放到conf.d或者sites-available目录再在nginx.conf的http块里include进来。以两个站点为例# /etc/nginx/conf.d/site-a.conf server { listen 192.168.1.10:80; server_name site-a.local; access_log /var/log/nginx/site-a.access.log; error_log /var/log/nginx/site-a.error.log; root /var/www/site-a; index index.html; location / { try_files $uri $uri/ 404; } }# /etc/nginx/conf.d/site-b.conf server { listen 192.168.1.11:80; server_name site-b.local; access_log /var/log/nginx/site-b.access.log; error_log /var/log/nginx/site-b.error.log; root /var/www/site-b; index index.html; location / { try_files $uri $uri/ 404; } }先分别准备好两个目录和首页文件mkdir -p /var/www/site-a /var/www/site-b echo site-a /var/www/site-a/index.html echo site-b /var/www/site-b/index.html检查并重载配置nginx -t nginx -s reload然后分别请求两个IP验证curl http://192.168.1.10/ # 输出 site-a curl http://192.168.1.11/ # 输出 site-b注意这两个IP都必须在同一台机器上生效否则curl只会得到connection refused。3.3 默认站点default_server的设置实际运维中服务器上除了多个IP业务站点外通常会有一个兜底站点用来处理无关请求、内网探测、乱填Host的恶意扫描等。这个兜底站点用default_server标记server { listen 80 default_server; server_name _; root /var/www/default; }这里listen 80不带具体IP表示上面其他server块没匹配到的所有IP的80端口请求都会落到这个默认块。server_name填下划线_是Nginx官方推荐的写法表示这是一个不存在域名的兜底站点凡是Host头与所有已配置server_name都不匹配的请求也会归到这里。一旦设置default_server整个http上下文里同一个端口只能有一个不然nginx -t会直接报错nginx: [emerg] a default server for name host is already defined如果你既想让192.168.1.10作为默认站点又想让192.168.1.11和192.168.1.12各自独立就需要这样server { listen 192.168.1.10:80 default_server; server_name _; root /var/www/default; } server { listen 192.168.1.11:80; server_name site-b.local; root /var/www/site-b; } server { listen 192.168.1.12:80; server_name site-c.local; root /var/www/site-c; }此时访问192.168.1.11会进site-b192.168.1.12进site-c其余所有IP如果没有其他绑定都进default。这个组合非常实用特别适合那种买了一个公网IP跑业务、内网另外几个IP跑测试的环境。3.4 HTTPS与IPv6场景的配置写法基于IP的HTTPS虚拟主机写法和HTTP几乎一样只是listen加上ssl参数同时每个站点要有自己的证书server { listen 192.168.1.10:443 ssl; server_name site-a.local; ssl_certificate /etc/nginx/certs/site-a.crt; ssl_certificate_key /etc/nginx/certs/site-a.key; root /var/www/site-a; } server { listen 192.168.1.11:443 ssl; server_name site-b.local; ssl_certificate /etc/nginx/certs/site-b.crt; ssl_certificate_key /etc/nginx/certs/site-b.key; root /var/www/site-b; }这种写法的核心价值在于客户端握手时几乎没有SNI依赖证书选择和站点分发都在IP层面完成了老客户端也能正常访问。IPv6环境同理只是地址要用方括号括起来server { listen [2001:db8::10]:80; server_name site-a.local; root /var/www/site-a; } server { listen [2001:db8::11]:80; server_name site-b.local; root /var/www/site-b; }从Nginx 1.3.14开始IPv6和IPv4也可以混写在同一个server块里比如listen 192.168.1.10:80; listen [2001:db8::10]:80;这种双栈配置适合一台服务器同时提供IPv4和IPv6访问的场景。4. 基于IP与基于域名两种方式的组合与取舍4.1 同一监听地址上的多域名分发如果两个IP只是入口但你希望在同一个IP上再区分多个域名就要在listen相同IP的前提下依赖server_name做二次分发server { listen 192.168.1.10:80; server_name homepage.local; root /var/www/homepage; } server { listen 192.168.1.10:80; server_name admin.local; root /var/www/admin; }这种情况下访问http://192.168.1.10/时Host头是192.168.1.10两个server_name都匹配不上请求会落到default_server。配置一个兜底server块并标记error或者返回404可以避免奇怪行为。测试时用curl指定Host头curl -H Host: homepage.local http://192.168.1.10/ curl -H Host: admin.local http://192.168.1.10/这里能看到基于IP和基于域名并不是非此即彼IP决定网络层面进哪个socket域名在socket内部做逻辑分发。4.2 独立IP加域名做精确控制还有一种混合模式是把域名也作为限定条件写进独立IP的server块里server { listen 192.168.1.10:80; server_name site-a.local; root /var/www/site-a; }此时如果有人在浏览器里直接敲http://192.168.1.10/访问Host头是192.168.1.10而这个server块的server_name是site-a.local。直观上以为会拒绝或404但Nginx实际的行为是listen已经精确绑定了192.168.1.10只要同一个listen地址上没有其他server块请求不需要Host头匹配server_name也会进入这个块。简单说listen完成了选人server_name只是名字。理解这个机制很重要在排查为什么我用IP访问进错了站点这种问题时会少走很多弯路。如果希望IP直连时必须返回错误可以在该IP的server块里增加判断逻辑或者单独写一个server块处理IP直连server { listen 192.168.1.10:80; server_name 192.168.1.10; return 444; } server { listen 192.168.1.10:80; server_name site-a.local; root /var/www/site-a; }return 444是Nginx一个特殊状态码表示直接断开连接不给任何响应。这样IP裸访问会被掐掉而用域名访问正常。4.3 三种混用方案怎么选我把平时用下来觉得靠谱的选型思路列一下供参考需求场景推荐方案理由内网测试多套环境多IP每个IP一个server块不用改Hosts各环境完全隔离对外提供多个独立站点共享IP 域名区分省IP维护方便IPv4资源紧张老客户端HTTPS强需求每个站点独立IP 443不依赖SNI证书按IP选一台机器既跑业务又做兜底业务IP绑定 default_server兜底防止漏网流量乱进业务站同一业务要IPv4和IPv6同一server块双listen配置集中证书只配一次这个表格是我根据个人排障经验整理的不一定覆盖所有极端场景但大部分常规需求都够用了。5. 验证、重启与常见问题排查5.1 配置检查与平滑重载每次改动配置文件第一件事永远是nginx -t这一步只检查语法和指令冲突不会真正重启Nginx。看到syntax is ok和test is successful两个输出再做平滑重载nginx -s reload重载期间Nginx会优雅地把老连接处理完再切换到新配置不会中断服务这是Nginx非常实用的特性。验证配置是否真的按IP分流除了前面的curl还可以打开日志确认tail -f /var/log/nginx/site-a.access.log curl http://192.168.1.10/访问后日志里会出现一条记录源IP、请求路径、User-Agent都有。如果发现某个IP的请求出现在另一个站点的日志里说明listen绑定出了问题。5.2 踩坑实录访问结果总是不对我调试基于IP虚拟主机时遇到过不少反直觉的问题挑几个典型说一下。案例一配好了所有IP但访问任何IP都进同一个站点。排查后发现这个站点用的listen 80;这种通配写法而其他站点写的是listen 192.168.1.x:80。问题出在于特定IP的server块虽然优先级更高但其中几个IP压根没有在系统网卡上生效请求根本到不了Nginx。所以第一步永远先确认ip addr show里面IP是不是真的存在不要一上来怀疑Nginx配置。案例二nginx -t提示default server冲突。这是因为http块里多个server块都标了default_server或者显式的default_server和旧配置里隐式默认产生了歧义。解决办法是全局搜索default_server同一个端口只保留一个。所有server块都去掉default_server的话Nginx默认会选第一个server块作为默认处理这在多IP场景下很容易造成意外站点被选中所以强烈建议显式写default_server。案例三外网访问不通内网访问正常。多数情况不是Nginx的问题而是云安全组或系统防火墙没有放行对应IP的80端口。排查步骤先用系统防火墙看端口是否放行# CentOS/RHEL sudo firewall-cmd --list-all # Ubuntu sudo ufw status再看监听状态ss -lntp | grep 80看到类似192.168.1.10:80和192.168.1.11:80两条监听记录说明Nginx本身没问题。如果只有*:80说明配置没有按预期加载回第3步检查。5.3 常见问题速查表把平时容易被问题淹没的排查思路整理成速查表遇到问题直接对号入座现象可能原因排查与解决办法某个IP访问不了其他IP正常该IP未在网卡生效、路由不通、防火墙/安全组拦截ip addr show确认IPping测网络检查防火墙和安全组所有IP都进入同一个站点其他站点listen没写IP或写错IPdefault_server指向该站点检查所有listen后的IP是否与ip addr一致查看default_server位置nginx -t报default server重复同一端口多个default_server全局搜索default_server保留一个curl用IP访问但进了域名站点listen未绑定具体IP或同一IP下server_name匹配逻辑发挥作用确认listen写法用curl -H Host: xxx测试域名分发HTTPS访问证书不正确listen未绑定IP或证书路径/域名不匹配或SNI被意外使用检查listen是否绑定IP地址检查证书域名用openssl s_client验证证书页面能打开但日志IP来源看不懂配置了默认server未知请求落到兜底站点确认default_server站点访问日志结合ss -tnp查来源reload时报地址已在使用有连接还没断开或有其他进程占用端口用ss -lntp检查占用稍等重试确认Nginx没有重复启动这张表是我把多次排障经验压缩出来的结果覆盖了基于IP虚拟主机90%以上的问题面。遇到这里没列出的情况建议先跑一遍nginx -t看有没有语法层错误再看error.log大多数线索都能从日志里拉出来。6. 个人实操经验与两点反思做了这么多年Nginx配置我对基于IP虚拟主机的态度是能用域名就别用IP但需要IP时不要犹豫。真到非用不可的场景IP方案带来的确定性和兼容性是域名方案替代不了的尤其是HTTPS证书按IP走、老客户端无障碍访问这两点保住了很多遗留系统。实际操作层面有几个细节值得提。第一IP规划一定用表格记录下来哪个IP对应哪个站点、证书绑定关系、安全组规则全都写清楚。我曾经处理过一个线上事故某个站点要下线操作的人只删了Nginx配置却忘了回收IP结果后来的流量通过默认server全跑到了另一个站点上排查了大半天。IP资源是明确的一份清晰的台账能省下大量时间。第二多IP的Nginx配置切分到独立文件管理每个文件命名和站点一一对应比如site-a.conf、site-b.conf主配置里只留一个include。这样上线、下线、迁移都只要处理一个文件不会误动其他站点。第三测试阶段先用临时IP验证Nginx逻辑确认无误后再把IP固化成系统配置可以降低误操作风险。最后再分享一个小技巧用curl验证基于IP虚拟主机时加一个-I参数看响应头能快速知道当前请求到底进了哪个server块因为每个站点的Server头字段可以自定义成不同值。比如site-a的配置里加server_tokens off后再设置自定义响应头测试时一眼就能区分开。这个办法在接手别人留下来的混乱配置时特别好用。