做运维这些年,配置虚拟主机绝大多数时候都在跟域名打交道,但有一类需求绕不开基于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的客户端连上来,服务器根本不知道要给它哪个证书。解决办法就是每个站点独占一个IP,443端口上只放一个server块,证书按IP来选。
第二,证书和IP绑定的合规要求。部分金融机构、政企内部系统的安全审计要求一个证书对应一个独立IP,不接受SNI方案。这种只能老老实实上IP虚拟主机。
第三,内网多环境隔离。测试环境经常有两三套相同的系统跑在同一台机器上,数据库、缓存、服务端口全都一样,用域名区分还得改Hosts。给每套环境单独分配一个内网IP,Nginx直接按IP分发,谁都不用改配置。
第四,机房或云上为特定业务单独申请的公网IP。有些场景要求对外访问有独立IP,比如某些第三方回源要求固定IP白名单,独立IP后某个站被攻击也不会殃及同服务器的其他站点。
反过来也要说句公道话:IPv4地址紧张、公网IP费用不低、后续IP要迁移时牵扯一堆关联配置,所以不是非用IP不可的业务,尽量还是用域名方案。基于IP虚拟主机适合"刚需",不适合"炫技"。
2. 动手之前:先让服务器拥有多个IP
基于IP虚拟主机的前提是,服务器上真的存在多个可用的IP地址。这一步最容易被忽略,很多人Nginx配置写得没问题,结果系统里压根只有一个IP,自然看不到预期效果。
2.1 多IP的三种来源方式
物理多网卡。服务器上插两块网卡,每块网卡配一个IP,两条物理链路互不干扰,可以做网络层面的流量隔离。缺点是要额外占用硬件资源,机房加网卡通常还要提工单、重启,成本高,一般服务器不会常备多块网卡。
单网卡多IP别名。一张物理网卡上绑定多个IP,Linux里习惯叫IP别名。实现简单,不额外占硬件,是目前我用的最多的一种方式,尤其适合内网测试机。
云平台辅助IP。现在的云服务器大多支持在主网卡上额外绑定辅助私网IP,前提是底层网络能正常路由。在云控制台配置后,还要到操作系统内手动把IP配上。这种方式的好处是弹性好,随时在控制台增减。
2.2 临时与永久配置多IP的方法
先看当前机器有哪些IP:
ip 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,推荐用nmcli:
sudo nmcli connection modify eth0 +ipv4.addresses 192.168.1.10/24 sudo nmcli connection up eth0注意这就是在原有IP基础上叠加一个IP。老一点的CentOS 6或非NetworkManager环境,要改ifcfg-eth0,在里面追加IPADDR1:
IPADDR1=192.168.1.10 PREFIX1=24Ubuntu 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-b,192.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确认IP;ping测网络;检查防火墙和安全组 |
| 所有IP都进入同一个站点 | 其他站点listen没写IP,或写错IP;default_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后再设置自定义响应头,测试时一眼就能区分开。这个办法在接手别人留下来的混乱配置时特别好用。