你是否也遇到过这种场景:家里或办公室一台机器上,用 Nginx 跑了好几个自建网站——个人博客、导航页、内部工具、照片库,内网访问一切正常,但一出差、一回家、手机切到 4G,就死活打不开了。IP 不固定、运营商不给公网 IP、路由器端口映射又不稳定,来回折腾,最后发现问题的核心不是 Nginx 配置,而是缺一个“固定的公网入口”。
这篇文章就是来解决这个问题的。我会用一台带固定公网 IP 的云服务器作为统一入口,通过内网穿透工具把本地多个 Nginx 站点安全地暴露出去,再在云端 Nginx 里按子域名做反向代理分发,最终实现用一个固定公网地址访问本地所有网站的效果。整个过程会包含架构图思路、完整配置文件、参数解释、踩坑实录,适合已经把 Nginx 跑起来、但卡在外网访问这一步的个人开发者、运维新手、以及想把手头多个自建服务统一发布到公网的折腾型玩家。
1. 方案选型:为什么是“固定IP云服务器 + 内网穿透 + Nginx反代”三件套
1.1 先看两个最容易翻车的方向
先说一个我最早踩过的坑:直接在路由器上做端口映射,把内网 Nginx 的 80 端口映射出去。这个方案成立的前提是,宽带运营商给你分配了公网 IPv4 地址。但现实里很多地区的光猫是 NAT 转发,你看到的 WAN 口 IP 是运营商私网地址,映射了也白映射,外网根本访问不到。就算你有公网 IP,很多宽带的 IP 是动态的,今天能用明天就变,域名解析跟不上,照样歇菜。
还有人会用 ngrok 这类在线穿透服务。优点是不用自己买服务器,缺点是免费版域名随机、有流量限制、速度不稳定,而且多站点场景下要开多个隧道,维护起来非常难受。偶尔演示一下可以,作为长期方案不合适。
所以,真正稳定且可控的路径就只剩下一条:准备一台有固定公网 IP 的云服务器(轻量级即可),用它作为所有流量的统一入口;本地各 Nginx 站点依然跑在内网各自的端口上;中间用 frp 这种自建内网穿透工具打通隧道;最后在云服务器的 Nginx 里按域名把请求反向代理到隧道对应的本地端口。
1.2 整体架构和数据流转
我把整个链路画成一句话:用户访问 https://blog.example.com,DNS 把这个域名解析到云服务器的固定公网 IP,云服务器上 Nginx 收到 443 端口的 HTTPS 请求后,根据 server_name 匹配到对应的 server 块,将请求反向代理到本机 frp 监听的某个回环端口,frp 客户端在本地收到数据后,转发给本地跑在 8081 端口的 Nginx 站点,最终返回页面。
用一张表格来表达这几层关系可能更直观:
| 层级 | 角色 | 位置 | 端口 | 职责 |
|---|---|---|---|---|
| 域名解析 | DNS 的 A 记录 | 域名服务商 | 80/443 指向云服务器 IP | 将用户请求导向固定入口 |
| 云端入口 | 云服务器 Nginx | 公网服务器 | 80/443 | TLS 终止、按域名分发 |
| 隧道服务端 | frps | 公网服务器 | 7000(仅限内网访问) | 接收 frpc 连接 |
| 隧道客户端 | frpc | 本地主机 | 动态 | 将本地端口映射到云端 |
| 本地源站 | 本地 Nginx 多站点 | 本地主机 | 8081/8082/8083 | 提供实际页面内容 |
这里最关键的一个设计决策是:为什么不让 frp 直接把本地不同的端口分别映射成云服务器的 80 和 81、82?因为 80 和 443 是 HTTPS 服务的标准端口,一个 IP 上同时监听多个裸端口,证书、访问控制、日志收集都会变成噩梦。你希望在浏览器里用 https://blog.example.com 和 https://cloud.example.com 来区分服务,而不是 http://IP:81 这种又丑又难记的地址。通过云上 Nginx 做一层反向代理,把域名和隧道端口解耦,后续加站点只需要加一个 server 块和一条 frp 映射,完全不需要动其他配置。
1.3 前置准备清单
- 一个域名(普通 .com 就行,国内服务商需要实名,海外注册商则无此步骤)
- 一台有固定公网 IP 的云服务器,Linux 系统,1核1G 即可,带宽建议 3Mbps 以上(便宜够用)
- 本地一台常开的设备,装好 Nginx,确定各站点已经通过不同端口或域名在内网正常访问
- frp 的安装包(GitHub releases 里按系统和架构下载,服务端是 linux_amd64,客户端根据本地系统选)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 本地 Nginx 多站点准备:先把“货架”摆好
2.1 目录规划和多站点配置
很多人本地一台 Nginx 跑多个站点,习惯把配置全部堆在一个 nginx.conf 里,短时间没问题,但一旦站点数量上来,找配置、改站点、排查错误都极其痛苦。我的习惯是采用 Debian/Ubuntu 系的 sites-available / sites-enabled 目录结构:
code复制/etc/nginx/
├── nginx.conf
├── sites-available/
│ ├── blog.conf
│ ├── cloud.conf
│ └── nav.conf
└── sites-enabled/
├── blog.conf -> ../sites-available/blog.conf
├── cloud.conf -> ../sites-available/cloud.conf
└── nav.conf -> ../sites-available/nav.conf
每个站点一个独立配置文件,启用时在 sites-enabled 里建软链接,禁用时删掉软链接即可。这样改配置不需要重启整个 Nginx,nginx -s reload 就能平滑生效。
2.2 为什么要让每个本地站点监听独立端口
在纯内网环境下,一个 Nginx 的 80 端口上就可以用不同 server_name 区分站点。但在内网穿透场景下,frp 客户端本地转发时需要通过端口来区分目标服务。虽然 frp 也支持按域名区分,但那是在 frp 内部做路由,配置复杂度和排错难度都会上升。我更建议的做法是:本地 Nginx 为每个站点绑定一个独立端口,例如:
- blog 站点监听 8081
- cloud 站点监听 8082
- nav 站点监听 8083
这样 frp 做端口映射时,规则就是“云服务器回环端口 ↔ 本地 8081/8082/8083”,一一对应,清晰明了。而且这些端口由于只绑定在 127.0.0.1 或内网网卡上,本机防火墙规则也很容易写。
2.3 一个可参考的本地站点配置
以 blog 站点为例,sites-available/blog.conf 内容如下:
nginx复制server {
listen 127.0.0.1:8081;
server_name blog.example.com;
root /var/www/blog;
index index.html;
location / {
try_files $uri $uri/ =404;
}
access_log /var/log/nginx/blog_access.log;
error_log /var/log/nginx/blog_error.log;
}
注意这里的 listen 我写的是 127.0.0.1:8081,只允许本机访问。这个端口本身不需要被局域网其他设备访问,因为它只和 frpc 通信。这样即使未来有人通过某种方式进入了内网,也没法直接访问到管理后台之类的敏感目录。
检查配置并重载:
bash复制nginx -t
systemctl reload nginx
然后本机验证:
bash复制curl -H "Host: blog.example.com" http://127.0.0.1:8081
能正常返回页面内容,说明本地源站准备完毕。其他站点照着同样模式改端口和 root 路径即可。
3. 云端 Nginx 反向代理:固定公网入口的“总闸门”
3.1 反代在架构里的作用
有了固定公网 IP 的云服务器,有两种做法:一个是把所有流量直接按端口转发到本地,另一个就是在云端 Nginx 做反向代理。我强烈推荐后者。反代层解决的不只是“端口太丑”的问题,它天然形成了一个统一接入层,SSL 证书只需要在云端配置一份,内部传输可以走普通 HTTP,安全性由隧道 token 保障;本地源站的真实 IP 和端口对外完全隐藏,攻击面显著缩小;日志也集中在云端的 Nginx access log 里,分析问题比翻每一台机器方便太多。
从原理上讲,反向代理就是让用户以为自己访问的是 blog.example.com,实际上 Nginx 收到请求后作为中转站,代替用户去请求后端的 frp 隧道端口,再把响应原样返回。和正向代理(用户主动配置代理上网)完全不同,反向代理对用户是透明的,用户根本感知不到后面的那台电脑在哪儿。
3.2 按子域名分发:server_name 匹配规则
云端 Nginx 配置的核心是多 server 块。Nginx 收到请求后,按顺序匹配 server_name,命中哪个块就把请求交给哪个块处理。在 /etc/nginx/conf.d/ 下分别建 blog.conf、cloud.conf、nav.conf 即可。
以 blog 为例:
nginx复制server {
listen 80;
server_name blog.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name blog.example.com;
ssl_certificate /etc/letsencrypt/live/blog.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:7101;
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;
}
}
这里我把 TLS 终止放在云端,然后再用 proxy_pass 转发到本机的 7101 端口——这个端口不是本地源站的直接映射,而是 frp 服务端在云服务器上监听的回环端口。后面配置 frp 时你会看到,它把云端回环端口和本地源站端口一一绑定。整条链路里,云上 Nginx 只认 127.0.0.1 的回环地址,公网网卡上除了 80/443,其他端口一律不放行。
3.3 按路径分发的坑:静态资源 404 问题
如果你不想为每个站点申请子域名,也可以考虑按路径分发,例如 https://example.com/blog 指向博客、https://example.com/cloud 指向云盘。但从实操经验看,路径分发在多站点场景里有三个绕不开的坑:
第一个是静态资源 404。如果前端代码里引用资源用的是绝对路径(比如 /static/css/main.css),那么它不会自动带上 /blog 前缀,会被云上 Nginx 拿到后去匹配根路径的站点,结果是找不到。要么逐个改前端资源路径,要么在 location 里做 rewrite,非常繁琐。第二个是 Cookie 路径问题。登录类应用的 session Cookie 默认只对当前路径生效且限定 path=/,多站点按路径分发时不同站点容易互相覆盖 Cookie。第三个是开发框架里的路由问题。很多现代前端框架是 history 模式路由,按路径分发后刷新任意二级页面都会 404。
所以我的建议非常明确:多站点一律采用子域名分发。子域名方案里,每个站点都是一个完整的根路径应用,后端代码不用做任何适配,前端资源、Cookie、路由全部正常。反正你已经有域名了,多解析几个 A 记录成本为零,何必给自己找麻烦。
3.4 HTTPS 证书申请与自动续期
有了固定入口之后,下一步是给每个子域名配上 HTTPS。原因很简单:浏览器对 HTTP 站点越来越不友好,而且如果你在本地站点里部署过 PWA、Service Worker,没有 HTTPS 这些能力根本没戏。
我用的是 Let’s Encrypt 的 certbot,一条命令即可完成证书申请和 Nginx 配置自动写入:
bash复制apt install certbot python3-certbot-nginx
certbot --nginx -d blog.example.com -d cloud.example.com -d nav.example.com
certbot 会自动修改 Nginx 配置,添加证书路径并把 80 端口重定向到 443。续期一般通过 systemd timer 或 cron 每天执行即可:
bash复制certbot renew --quiet --deploy-hook "systemctl reload nginx"
注意续期脚本里加了 --deploy-hook,它的作用是在证书实际更新之后才重载 Nginx,避免每次检查都触发 reload。
4. frp 内网穿透搭建:打通本地与云端的“隧道”
4.1 为什么选 frp 而不是其他穿透工具
内网穿透工具有不少,ngrok 用起来简单,但自托管版本功能相对基础;nps 也不错,但作者维护节奏时快时慢;tailscale / zerotier 这类异地组网工具,安全性做得很好,但它们的定位是“把你拉进一个虚拟内网”,而不是“提供一个公网可直接访问的固定入口”。如果你的目标是“用固定公网地址给不特定设备访问”,frp 是自建方案里最主流、文档最全、社区踩坑记录最多的选择,配置也足够直白。
frp 的架构分两端:frps 是服务端,跑在云服务器上;frpc 是客户端,跑在本地内网机器上。frpc 主动向 frps 发起连接,在两者之间建立一条加密隧道,同时在公网服务器上监听指定端口,流量到该端口后自动沿隧道转发到本地局域网的服务端口。
4.2 云端 frps 配置与 systemd 守护
在云服务器上下载并解压 frp,进入解压目录,编辑 frps.toml(新版 frp 配置文件格式是 toml):
toml复制bindPort = 7000
auth.token = "换成一段足够长的随机字符串"
核心配置就这两项。bindPort 是 frpc 和 frps 之间通信的控制端口,出于安全考虑,我建议云服务器防火墙和安全组不把 7000 端口暴露到公网,只允许你自己的固定出口 IP 访问它。这样一来,即使有人扫描到 7000 端口也连不上 frps,本地 frpc 却是可以主动连出去的,完全不受影响。
启动服务之前,为了稳定运行,我会把它交给 systemd 托管:
ini复制[Unit]
Description=frp server
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/frp/frps -c /usr/local/frp/frps.toml
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
保存后执行:
bash复制systemctl daemon-reload
systemctl enable --now frps
4.3 本地 frpc 配置:多站点映射明细
本地同样下载 frp 解压,编辑 frpc.toml:
toml复制serverAddr = "你的云服务器固定IP"
serverPort = 7000
auth.token = "和 frps 里设置的 token 保持一致"
[[proxies]]
name = "blog"
type = "tcp"
localIP = "127.0.0.1"
localPort = 8081
remotePort = 7101
[[proxies]]
name = "cloud"
type = "tcp"
localIP = "127.0.0.1"
localPort = 8082
remotePort = 7102
[[proxies]]
name = "nav"
type = "tcp"
localIP = "127.0.0.1"
localPort = 8083
remotePort = 7103
每个 [[proxies]] 就是一条隧道映射规则。以 blog 为例:本地 frpc 会连接云服务器 frps,然后要求 frps 在云服务器的 7101 端口上开始监听。任何发往云服务器 127.0.0.1:7101 的请求,都会经过隧道原封不动地送到本地 127.0.0.1:8081,也就是本地 blog 站点。
为什么 remotePort 不直接用 8081 或 80?因为云服务器的公网入口只应该开放 80 和 443,所有隧道回环端口绑定在 127.0.0.1 上,外部流量根本碰不到。而且如果你在云服务器上跑了多个 frp 隧道,回环端口从 7101 开始递增,排查的时候非常直观。
启动本地 frpc:
bash复制systemctl enable --now frpc
同样建议写成 systemd 服务,开机自启。这里有一个容易忽略的点:本地 frpc 所在的机器如果重启了,systemd 的 RestartSec=5 是理想的重连间隔,但在网络刚恢复的前几秒,DNS 解析可能失败。我习惯在 frpc 服务里加上 After=network-online.target 和 Wants=network-online.target,确保网络真正就绪后再启动。
4.4 关键参数背后的逻辑
type = "tcp" 意味着这是在传输层做纯端口转发,不需要 frp 解析 HTTP 内容。它可以转发任意 TCP 流量,不只是 Nginx 站点,未来你本地要暴露 SSH、数据库管理面板、自建 Git 服务,都是同样套路。
localIP 填 127.0.0.1 就可以了,因为 frpc 和本地 Nginx 在同一台机器上。如果你的 frpc 跑在 Docker 容器里,而本地 Nginx 跑在宿主机,那 localIP 要填 Docker 网关地址,通常是 172.17.0.1,这点在容器化部署时特别容易踩坑。
auth.token 的作用是防止未授权的 frpc 连接上你的 frps。token 不要用弱口令,我见过有人直接设成 123456,结果被扫描器识别,对方用 frp 建了一条到内网的隧道,然后把端口映射到公网,自己的内网机器成了别人的跳板。这种情况大概率不是你的错,但完全可以避免。
5. 域名解析、防火墙与安全加固
5.1 域名解析配置
到域名服务商的控制台,为每个子域名添加 A 记录,都指向云服务器的固定公网 IP。泛解析会是更省事的做法:添加一条 *.example.com 的 A 记录,指向同一 IP。这样未来再加 tool.example.com、git.example.com 时,不需要再改 DNS,只要在云端 Nginx 加 server 块、在 frp 加一条映射、去 certbot 签发证书即可。新站点的上线时间能从小时级压缩到十几分钟。
DNS 解析生效后,在本地先做一层验证:
bash复制dig +short blog.example.com
返回的 IP 必须和云服务器固定 IP 一致,否则后面排查全是白费功夫。
5.2 云服务器防火墙与安全组
这是很容易被忽略、也是很多问题“凭空出现”的环节。云服务商的安全组和 Linux 系统内防火墙是两层独立机制,只放行其中一层可能不够。
我的默认原则:
- 安全组只放行 80(HTTP)、443(HTTPS)、22(SSH)
- 不向公网放行 frp 的
bindPort7000 - 如果必须远程 SSH,建议修改默认端口或开启密钥登录,密码登录非常容易成为爆破目标
在 Linux 云服务器上,用 ufw 快速收紧:
bash复制ufw default deny incoming
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
注意,因为 frp 的隧道端口 7101-7103 只绑定在 127.0.0.1,ufw 默认 deny incoming 不会影响回环回环通信。真正要关注的是 frps 的 bindPort 7000:它绑定在所有网卡上,frpc 需要从公网连进来。如果不想把 7000 暴露给全世界,又希望云服务器上的 frps 能接受本地 frpc 的连接,有两个办法:一个是在云服务器安全组和 ufw 里把 7000 端口限制为只允许自己家宽出口 IP 访问,另一个是改用 frp 的 tls 加密通信并且配合强 token,两个手段同时用更稳。
5.3 本地与云端双重安全建议
公网暴露是“被迫害妄想”性质的安全设计,几条经验供参考:
一,所有本地 Nginx 站点只监听内网或回环地址,不要为了省事把 listen 写成 0.0.0.0:8081。这样即使 frp 隧道被攻破,攻击者也无法直接接触其他内网设备。
二,frp 的 auth.token 设置得足够长,建议 32 位以上随机字符串,用 openssl rand -hex 16 生成。
三,云端 Nginx 统一加一层安全响应头,借助 add_header 可以在每个 server 块里加,也可以在 http 层统一加:
nginx复制add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
四,如果本地站点有用户登录功能(比如 Nextcloud),建议在云端 Nginx 的 location 里开启基础访问认证或者用 fail2ban 扫日志封禁持续爆破的 IP。免费又简单,能过滤掉绝大多数扫描流量。
6. 常见问题与排查技巧实录
6.1 问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 外网访问超时 | 安全组或 ufw 未放行 80/443 | 按 5.2 节检查两层防火墙 |
| 域名打不开但 IP 能访问 | DNS 未生效或 A 记录错误 | dig +short 检查解析结果,等 TTL 过期 |
| 云服务器 443 跳到了 Nginx 默认站点 | server_name 匹配失败,或者证书配置错误 |
nginx -t 检查;按域名建独立 server 块 |
| 本地能 curl 通但外网不行 | frpc 未连上 frps 或 remotePort 没有监听 | 查看 frpc 日志、云服务器 `ss -tlnp |
| 页面出来但样式全乱 | 路径分发导致的静态资源绝对路径问题 | 改用子域名分发,或前端资源前缀改为相对路径 |
| HTTPS 证书报域名不匹配 | 证书申请时少了该域名或签发失败 | 重新运行 certbot,加上 -d 参数 |
| 修改 Nginx 配置后不生效 | 语法错误阻止 reload | nginx -t 找出错误,修正后再 systemctl reload nginx |
6.2 分层排查的思路
端口映射类问题,必须在链路上一层一层找,千万不要从中间开始。我的排查顺序是这样的:
先用 dig +short blog.example.com 确认 DNS 正常,接着在云服务器本机执行 curl -I https://blog.example.com,如果返回 200 说明云上 Nginx 和证书都没问题;如果本机都打不开,那就是 Nginx 配置问题。再在云端验证隧道,执行 curl -I http://127.0.0.1:7101,能通说明 frp 隧道正常,问题出在 Nginx 转发配置;不通就去看 frpc 和 frps 日志。
整个排查过程用到的命令就几条,但组合起来非常高效:
bash复制# 云服务器上检查端口监听
ss -tlnp | grep 7101
# 本机查看 frpc 日志
journalctl -u frpc -f
# 检查 Nginx 语法
nginx -t
# 带 Host 头验证本地站点
curl -H "Host: blog.example.com" http://127.0.0.1:8081
6.3 路径分发时的三个专属坑
虽然前面建议子域名方案,但确实有人因为域名证书、备案等限制只能用路径分发。如果你非要走这条路,三个坑必须提前规避:
第一个是静态资源 404。前端构建后的 index.html 里,资源引用往往以 / 开头,比如 /assets/app.js。路径分发后,浏览器请求的是 https://example.com/blog,页面里的资源链接会被解析成 https://example.com/assets/app.js,完全绕过了 /blog 前缀。解决方法是让前端以相对路径构建,或者在云端 Nginx 的 location 里加一条 rewrite 规则把 /assets/ 强制重写到对应的子目录。
第二个是 Cookie 路径冲突。站点 A 和站点 B 都跑在 example.com 下,登录 Cookie 的 path 如果同时是 /,A 站点登录态会串到 B 站点。需要在各自后端设置 Cookie 的 path 为 /blog 和 /cloud,登录态才互不干扰。
第三个是 WebSocket 和 SSE 长连接。反向代理默认会缓冲响应,路径分发时很容易把 WebSocket 的 Upgrade 头丢掉。必须显式声明:
nginx复制proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
否则实时推送功能一律失效。
写在最后的几点体会
整套链路跑通之后,最大的收益是“加站点变成了一件十分钟的小事”:本地写好站点配置,frpc 里加一条隧道映射,云端 Nginx 加一个 server 块,最后 certbot 签个证书,新站直接上线。我个人的建议是,先把本地所有站点都调试通过再动云端,这样每一层都是可控的,出问题能迅速定位。
另外强调一句:把服务暴露到公网就是把风险暴露到公网,token、防火墙、证书、安全响应头这些能开的都开上,别偷懒。尤其是 frp 的 token,别用弱口令,也别图省事把 7000 端面向全网敞开,只让可信来源访问就好。这套方案目前我已经稳定跑了两年多,中途除了云服务器到期迁移过一次,再没动过架构本身。如果你也想把本地几个 Nginx 站点统一发布到公网,按照这个思路一步步来,大概率能一次跑通。
