Skip to content

Lucky搭配Cloudflare穿透内网实践

约 7428 字大约 25 分钟

luckycloudflare

2025-12-14

前言

之前有写过Lucky内网穿透实践,其中介绍了利用 Lucky 做内网穿透的使用方式,今天继续介绍搭配 Cloudflare 的使用技巧。

多说一句,内网穿透的方式很多,有商业付费方案,付了钱开通账号直接用;也有个人付费方案,买个轻量云主机,搭建 frp、wireguard、tailscale derp 节点等。但那些都不在本次讨论范围内,多数使用场景下都不是必要付费的,而本文要说的是,纯免费打造媲美付费效果的内网穿透方案。

开始本文前,一个假设的前提是已经利用 Lucky 做好了 Stun 内网穿透,如果没有建议先看Lucky内网穿透实践
能够实现效果通过一个域名无端口访问到内网,且支持双栈网络访问

开始前的准备

  1. 一个 Cloudflare 账号。如果没有先去官网免费注册
  2. 至少一个域名,并指向了 Cloudflare 的 DNS 服务器。如果没有可以从DigitalPlatDNSHE免费获取,然后托管到 Cloudflare;

1. 穿透原理

我认为在进行一项新事务前,如果时间允许的话,应该先了解其工作原理。如果直接上手,会导致做的过程中云里雾里,很容易在无意中出错,然后花费大量时间排查,效率极其低下。
而实现了解原理也有助于自身评估,是否需要花费精力操作,因为每个人有不同的使用场景,没有永远的银弹,只有适合自己的才是最好的方案。

首先,在 FullCone NAT 的网络模式下,Lucky 会利用 STUN 协议打洞,在运营商的路由器上打通一个 NAT 映射,并维持住这个映射,之后任意客户端都可以通过这个映射到内网服务。

这就是 STUN 打洞的过程:内网服务依次通过家庭路由器和运营商路由器两层NAT,每一次的NAT都会在路由器上留下一个地址和端口的映射,最终以一个公网身份请求同样处于公网的STUN服务器。接着STUN服务器返回响应,回传这个公网地址,那么内网服务就知道了自己的公网身份了,只要维持住这个映射,那么任意设备就可以通过访问这个公网地址达到内网服务了。

其中100.80.48.188这个是运营商大内网中的地址,如果在同一运营商网络(移动/联通/电信)下,是可以拿这个地址访问的;而189.123.45.67:31580就是真正的公网地址,任意运营商网络下的设备都可以访问到。

从大的角度看,外部网络访问到内网的过程是这样,而内网中又如何将请求转发到实际提供服务的机器上呢?

这里要说名明的一下,我的 Lucky 是部署在主路由的 OpenWrt 上,光猫桥接用主路由拨号。如果你不能设置光猫桥接,或者 Lucky 没有部署在主路由上,那就需要用到 DMZ 或者 Upnp 把 Lucky 需要穿透的端口映射到拨号设备上。
举例:通过 Lucky 设置 STUN 穿透时,要指定一个穿透通道本地端口,假如是7788。这个意思是,由于公网的 ip 和 port 是动态的、随机的,指不定什么是否就会变的,但是本地得监听一个固定的端口,那么公网流量通过31580这个随机端口访问进来,流量到了路由器后实际是到了eth0这个网卡的7788端口。

路由器上通常都可以看到WAN口和LAN口,其中WAN口接的是光猫,连接到的是外部网络,而外部流量进来也是到达了eth0网卡,当拨号上网成功时,这个网卡就会被运营商分配一个 ip(100.80.48.188);LAN口是内网交互的网口,对应的网卡名通常是eth1,它的 ip 通常是静态的,也就是我们常常设置的 192.168.0.1。

我们内网中的设备通常都是 192.168.0.x,也就是说它和eth1网卡是通的,但刚刚我们说外部流量是打到了eth0的 7788 端口。那我们就需要把eth0的 7788 端口上到流量转发到eth1上某个 ip 的某个端口上,这个转发操作可以在路由器上的防火墙设置端口转发,也可以直接让 Lucky 内部自行转发,但推荐使用防火墙那的端口转发,因为那是内核级的转发,性能会更高一些,资源占用也会更低一些。

重要Lucky 服务千万不能经过任何网络代理,即使把模式设置为直连也不行,因为代理软件为了做规则分流,通常会接管所有的 DNS 请求,统一返回 fakeip,这会导致 Lucky 的 STUN 穿透无法获取到正确的公网 ip

2. DDNS分流

Lucky 的功能很强大,除了可以帮我们获取到公网地址,可以做 DDNS 将公网 ip 同步到 DNS 记录中。假设我们上面准备的域名是happy.de5.net,如果把ipv4 和 ipv6 的地址都记录到happy.de5.net*.happy.de5.net,到时候公网该如何访问?
在当前的网络环境下,我们是通过 STUN 拿到的 ipv4 公网,地址和端口都是动态的,而非原生的公网地址,但 ipv6 的公网地址是原生的。
假设内网中反代服务监听了端口 666 转发到后端的 8080 端口,如果外网设备用 ipv6 地址访问时,就应该用https://happy.de5.net:666,通过固定的端口就可以了;但是如果外网用的是 ipv4 访问时,就应该用https://happy.de5.net:31580,通过随机端口访问,并且这个随机端口还是会变的。可是外网设备一般不会主动选择用 ipv4 还是 ipv6 发起请求,这取决于设备所处的网络环境。而通常家用路由器的 ipv6 开关是关闭的,这种情况下就拿不到 ipv6 地址,别人无法通过 ipv6 访问你,而没有 ipv6 地址的设备更是无法访问你。

有人可能会说,可以设置 SRV 记录啊,支持同时记录 ip 和 端口号,确实是,Lucky 也支持 SRV 记录,但问题是:不是所有的设备都支持查询 SRV 记录,比如浏览器就不会,它只会找 A/AAAA 记录,也就是去查询你的 ipv4/ipv6 地址,如果查不到记录,那就报错。

似乎没有优解方案了?

别着急,我们换个角度想想,既然合在一起不行,那干脆就把 ipv4 和 ipv6 分开来设置,然后外部请求时统一用一个入口,根据来源 ip 类型将请求实际打到我们的 ipv4 或 ipv6 地址上去。这个意思不就和反向代理差不多了吗?Ok,既然想清楚了,那就开始干。

申请证书及 DDNS

Lucky 确实很强大,你可能需要的网络相关的东西,它都提供给你了,包括证书申请及续期。操作起来非常简单,具体看开头提到的文章,这里只说思路。

我们分别为*.v4.happy.de5.net*.v6.happy.de5.net这两个泛域名申请证书,前缀可以随意,这里用v4v6主要是为了见名知意。然后利用 Lucky 提供的 DDNS 功能,将 ipv4 地址解析到*.v4.happy.de5.net,将 ipv6 地址解析到*.v6.happy.de5.net注意:不要勾选指定代理状态。如果勾选了,在 Cloudflare 上托管域名的 DNS 记录就会被代理,显示一个黄色的云朵,也就是常说的小黄云

为什么不要开启小黄云?

一旦开启了小黄云,你的 DNS 记录就会被 Cloudflare 代理,表现为:当你 ping v4.happy.de5.netv6.happy.de5.net 的任意子域名时,得到的是 Cloudflare 的泛ip,而非你自己的真实ip。好处是可以隐藏你的真实 ip,并且享受 cloudflare 带来的安全防护,坏处是支持访问的端口有限,stun 拿到的公网端口都是随机的,很可能就是 cloudflare 不支持的,并且也不能被 workers 和 pages 访问。
当开启了小黄云时,你的服务器对于 cloudflare 来说就是源站,公网请求到 cloudflare,cloudflare 到源站的这个过程叫做“回源”。

3. 重定向规则

如果说上一个步骤是分离,这个步骤就是合并。通过访问一个域名 https://ok.happy.de5.net,根据访问者的 ip 类型,浏览器自动重定向到 https://ok.v4 .happy.de5.net:31580 或 https://ok.v5.happy.de5.net:666。要实现这个效果就得利用 Cloudflare 的重定向规则

点击托管在 Cloudflare 上的域名happy.de5.net,接着在左侧菜单找到规则->概述,点击创建规则
我们可以为 ipv4 和 ipv6 分别创建规则。

3.1 创建 ipv4 重定向规则

编辑单一重定向

规则名称:home-redirect-v4

如果传入请求匹配…

选择自定义筛选表达式

当传入请求匹配时...

字段运算符条件
主机名等于*.happy.de5.netAnd
IP源地址包含以下各项0.0.0.0/0

表达式预览

(http.host eq "*.happy.de5.net" and ip.src in {0.0.0.0/0})

则...

URL 重定向

类型表达式状态码
动态wildcard_replace(http.request.full_uri, "https://*.happy.de5.net/*", "https://${1}.v4.happy.de5.net:31580/${2}")307

勾选保留查询字符串


上述说明:

  1. ip 来源值为 0.0.0.0/0,就表示匹配所有的 ipv4 地址。
  2. https://*.happy.de5.net/* 这里的 * 就是占位符,可以指代任意字符串。https://${1}.v4.happy.de5.net:31580/${2} ,这里 ${1} 就表示引用第 1 个 * 的内容, ${2} 表示引用第 2 个 * 的内容,以此类推。
  3. 307/302 表示临时重定向, 305/301 表示永久重定向。如果永久重定向的话,浏览器会记住重定向后的地址,这里我们当然要用临时重定向。而 302 是历史遗留产物,不规范,无论重定向前是哪种请求方式,重定向后都变成了 GET 请求,这就可能会丢失请求 body,而 307 就很好的保留了原始的请求方式。

重定向规则这里有个问题

举例,原始请求:https://okk.happy.de5.net/best,重定向后:https://okk.v4.happy.de5.net/best:31580。
31580 是 STUN 拿到的动态公网端口,一旦发生了变化这条重定向规则就走不通了。

好在 Cloudflare 的设置都提供了 api 方式修改,我们完全可以在 STUN 的 WebHook 处来发起一个请求来修改重定向规则。

具体步骤如下:

  1. 获取 Api 令牌

点击右上角个人简介->配置文件,再点击左侧菜单中的API令牌,在API令牌区域点击创建令牌->自定义令牌

令牌名称:随意

权限:
账户 账户规则集 编辑
区域 单一重定向 编辑

帐户资源:
包括 {你的账户}

区域资源:
包括 特定区域 happy.de5.net

接着保存并记下 Api令牌。

  1. 获取区域(Zone)ID

点击托管的域名,将页面拉到最下面,就可以看到区域ID

  1. 获取重定向规则集ID

发起如下请求:

GET https://api.cloudflare.com/client/v4/zones/{区域ID}/rulesets
Authorization: Bearer {Api令牌}

在响应结果中搜索http_request_dynamic_redirect,这一项的 id 就是重定向的规则集ID

  1. 获取重定向规则

发起如下请求:

GET https://api.cloudflare.com/client/v4/zones/{区域ID}/rulesets/{规则集ID}
Authorization: Bearer {Api令牌}

在响应结果中就可以看到刚刚设置的重定向规则home-redirect-v4,这一项的 id 就是规则ID

  1. 修改重定向规则

发起如下请求:

PATCH https://api.cloudflare.com/client/v4/zones/{区域ID}/rulesets/{规则集ID}/{规则ID}
Authorization: Bearer {Api令牌}

{
  "description": "home-redirect-v4",
  "enabled": true,
  "expression": "(http.host eq \"*.happy.de5.net\" and ip.src in {0.0.0.0/0})",
  "action": "redirect",
  "action_parameters": {
    "from_value": {
      "preserve_query_string": true,
      "status_code": 307,
      "target_url": {
        // 假设 STUN 获取到的最新公网端口是 25778
        "expression": "wildcard_replace(http.request.full_uri, \"https://*.happy.de5.net/*\", \"https://${1}.v4.happy.de5.net:25778/${2}\")"
      }
    }
  }
}

3.2 创建 ipv6 重定向规则

编辑单一重定向

规则名称:home-redirect-v6

如果传入请求匹配…

选择自定义筛选表达式

当传入请求匹配时...

字段运算符条件
主机名等于*.happy.de5.netAnd
IP源地址包含以下各项2000::/3

表达式预览

(http.host eq "*.happy.de5.net" and ip.src in {2000::/3})

则...

URL 重定向

类型表达式状态码
动态wildcard_replace(http.request.full_uri, "https://*.happy.de5.net/*", "https://${1}.v6.happy.de5.net:666/${2}")307

勾选保留查询字符串


说明:和 ipv4 的重定向规则差不多,2000::/3包含了所有的 ipv6,表示匹配所有的 ipv6 地址。但重定向的端口可以是固定的 666,因为 ipv6 是原生的公网 ip,所以就不需要像 ipv4 那样通过 api 来修改规则了。当然,如果想修改也行,跟修改 ipv4 重定向规则一样的方式。

发起如下请求:

PATCH https://api.cloudflare.com/client/v4/zones/{区域ID}/rulesets/{规则集ID}/{规则ID}
Authorization: Bearer {Api令牌}

{
  "description": "home-redirect-v6",
  "enabled": true,
  "expression": "(http.host eq \"*.happy.de5.net\" and ip.src in {2000::/3})",
  "action": "redirect",
  "action_parameters": {
    "from_value": {
      "preserve_query_string": true,
      "status_code": 307,
      "target_url": {
        "expression": "wildcard_replace(http.request.full_uri, \"https://*.happy.de5.net/*\", \"https://${1}.v6.happy.de5.net:666/${2}\")"
      }
    }
  }
}

3.3 设置 DNS 记录

最后还有一个重要的步骤,手动为happy.de5.net设置两个 DNS 记录。

类型名称内容代理状态TTL
A@192.0.2.1启用自动
A*192.0.2.1启用自动

这两个记录一定要打开小黄云,才能触发重定向规则。其中192.0.2.1是一个非常特殊的 IP 地址,在 RFC 5737 规范中,它是仅用于演示、举例、写书的一个地址,不是一个有效的主机地址。你也可以设置为 0.0.0.0,随便都可以。这也就是为什么证书只申请*.v4.happy.de5.net*.v5.happy.de5.net,而happy.de5.net*.happy.de5.net不需要证书,因为它们指向的不是有效的主机,TCP 三次握手都完成不了,更别提后面的 Client Hello 拿证书了。根域名的作用仅仅是用来做重定向。


经过上面的操作,我们就可以做到通过浏览器访问一个地址,自动重定向到不同的子域,支持双栈网络访问。

但是有但是,这种方式只适合用于浏览器环境,对于一些 api 服务来说,你返回给它 302/307 根本不行,它只认 200,不是就报错。并且这还可能有资源跨域问题。浏览器地址栏有端口号丑不说,还会暴露真实的 ip 地址。

那我们就换一种方式,结合 worker 来做。

4. Worker 反代

4.1 Workers 说明

Cloudflare 这个赛博善人提供了 Workers&Pages。Workers 可以在边缘节点上执行 Serverless 函数,Pages 则可以托管静态网站,同时也能执行 Serverless 函数。

Pages 的功能很好理解,它就类似于 GitHub Pages、Vercel、Netlify 可以托管静态网站,不过 GitHub Pages 不能执行函数,Vercel 和 Netlify 可以。当前这个博客就是托管在 Vercel 和 Netlify 上。

Workers 的功能似乎有点看不懂,什么是边缘节点?什么是 Serverless?能执行什么函数?

什么是边缘节点?

首先“节点”指的就是 Cloudflare 的服务实例。Cloudflare 在全球各地都有机房,因此节点也就遍布全球。

“边缘”指的就是网络位置距离你最近的地方,因此“边缘节点”指的就是在网络位置上离你最近的那个 Cloudflare 服务实例。

因为你在 Workers 上部署的函数,Cloudflare 会自动同步到全球各个节点上去,如果你没做“Worker路由规则”或其他防护,任何人都可以访问到你的函数。
假设 Cloudflare 在香港、东京、洛杉矶、首尔有节点,这些节点上都会存在你的函数,香港朋友如果要访问你的函数,就近原则就会请求到香港节点;美国朋友如果要访问你的函数,就近原则就会请求到洛杉矶节点。这么做的好处很明显,全球任何位置的人都就近访问边缘节点,能有效降低延迟。

其实类似的我们还听过“边缘网关”,尤其是智能家居物联网场景下,各种设备通过WIFI、蓝牙或者Zigbee互联,总得需要一个中枢(相当于指挥中心),这个中枢就是网关,因为网关处于内网中,在网络上离我们最近,因此也叫做“边缘网关”。 我们还听说过“边缘计算”呢,比如你的小爱音箱或者天猫精灵,它就是一个“边缘网关”,当我们给它发送指令,有些是简单指令,比如“定个闹钟”、“xx分钟后提醒我”、“打开主灯”,这些指令都是小爱音箱自身就能计算并完成的,就是“边缘计算”;而对于一些复杂指令,比如“最近的国际局势”、“分析过去两周的黄金走势”,这些就会发送到小米服务器(云端)计算,也就是“云计算”。
“边缘计算”的优势在于快,免去网络延迟开销就能完成任务;“云计算”的优势在于强大,能完成更复杂且多样性的任务。当然在 AI 发展的今天,边缘网关的硬件也可以内置一些小模型,支持在本地做一些复杂计算和分析任务了。

什么是 Serverless?

传统意义上,我们编码写了几个函数,无论是什么语言,如果要提供访问,就得启动一个服务器,监听 TCP/UDP,一个或多个端口。也就是说,要想提供服务就得有 Server,我们还得维持这个 Server 运行,一旦 Server 停止,函数也就无法执行了。
而 Serverless 就是让我们不用创建 Server,更不用关心 Server 的存在,函数部署到 Worker 上就能提供访问。既然连 Server 的不用创建,也就无需维持 Server 了。

传统服务器计费按照 CPU(规格、频率、核数)、内存、硬盘、带宽、流量等多种维度计费。你一旦买了服务器,这些费用就产生了,即使你没用放在那吃灰,那也是要给钱的,因此成本就相对较高。
而 Serverless 的 CPU(规格、频率、核数)、内存、硬盘、带宽、流量等都是共享的,当你的函数需要运行,Cloudflare 会调度 CPU 给你执行,按照执行时间收费,不执行就不收费,其他资源也是同样的,用多少花多少。要是不用放那吃灰,那就不收你钱,可以说优势很明显。

能执行什么函数?

可以用 Node.js 写函数,比如:正向代理、反向代理、管理系统等等,结合Cloudflare 提供的免费 KV、D1、R2 等多种存储方式,可以实现更多功能。

4.2 资源限制

Cloudflare 真的是相当慷慨,即使是免费账户,提供的资源也是非常的充裕,个人使用很难触及上限。

比如,Workers&Pages 每日请求最大限额 10W 次。这里指的是单个账户下所有 Workers&Pages 的请求次数总和。

每个 Worker 中函数执行的最大 CPU 时间是 10ms,这意味着只要不是做复杂的加/解密,死循环,批量读写大文件,几乎是达不到这个限制的。而 KV 存储更是每日 1000次写操作,10W 次读操作。

4.3 部署函数

为了规避前面重定向的一些问题,我们可以借助 Worker 来做一个反向代理服务,代码如下:

_worker.js

打开 Cloudflare 主页下左侧菜单计算和AI->Workers 和 Pages,右上角创建应用程序,名称随意。
部署完后,在Worker的设置->变量和机密添加一个TOKEN。然后在左侧存储和数据库->Workers KV创建一个实例,名称随意。然后在创建的 Worker 中绑定选择刚刚创建的 KV 实例,绑定名称为KV_STORE

建议可以放弃 url 重定向那套玩法,除非特别需要。这样我们就可以移除掉happy.de5.net*.happy.de5.net这两个 DNS 记录,然后可以给 Worker 绑定一个域名,如happy.de5.net,可以把okk.happy.de5.net绑定给 Tunnel。前端只需要访问happy.de5.net,Worker 会自动帮我们代理进行后端访问,同样支持双栈网络。

当然,完全可以注册多个域名,因为DigitalPlat免费就可以注册两个域名,DNSHE最多可以免费注册5个域名。

访问策略如下:

  1. 如果是 ipv4 地址连接就转发到*.v4.happy.de5.net,否则转发到*.v6.happy.de5.net
  2. 如果上述请求不成功,则转发到 Tunnel 进行兜底。

因此,实际上我们是结合使用了内网穿透的 3 种方式:STUN、V6、Tunnel。

5. Tunnel

前面提到了 Cloudflare Tunnel,它也是一种内网穿透方式,通过内网运行 Tunnel 实例,一个实例下有 4 个连接器,分别会连接到不同的 Cloudflare 节点,做服务端的高可用方案。Tunnel 自身还提供了客户端高可用的方案,只要用相同的配置起多个 Tunnel 实例,它们会自动管理连接,单节点故障转移。

下载Tunnel并在内网中任意一台设备上运行,或者直接下载Docker镜像用容器跑也行。当然,也有 OpenWrt 的版本。

Cloudflare 上要在主页左侧菜单点击Zero Trust->网络->连接器,创建隧道。
然后拿着隧道ID令牌,填写配置文件:

config.yaml
# 配置文件参数:
# - https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/configure-tunnels/cloudflared-parameters/run-parameters/
# - https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/configure-tunnels/cloudflared-parameters/origin-parameters/
tunnel: Sub-Store
credentials-file: /root/.cloudflared/{隧道ID}.json
# edge-ip-version: "4"
# protocol: http2 # quic
metrics: 0.0.0.0:20241
loglevel: info
# 暴露私网到 CF 的网络中(和 tailscale 的 subnet 一个意思,客户端需要使用 WARP 并启用 zero trust,同样要开 VPN)
warp-routing:
  enabled: false
ingress:
  - hostname: okk.happy.de5.net
    service: http://192.168.0.8:3001
  - service: http_status:404

另外还需要一个证书,访问,选择happy.de5.net这个域名,点击下载证书cert.pem

compose.yaml
services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    environment:
      - TUNNEL_TOKEN={令牌}
      - TUNNEL_ORIGIN_CERT=/etc/cloudflared/cert.pem
      - NO_AUTOUPDATE=true
    network_mode: host
    restart: unless-stopped
    command: tunnel --config /etc/cloudflared/config.yml run
    volumes:
      - ./cloudflared:/etc/cloudflared

然后在公网访问happy.de5.net请求就会到达内网的http://192.168.0.8:3001

Cloudflare 的 Tunnel 使用和 Tailscale 的 Funnel 类似。区别在于 Funnel 强制走 derp 中转会很慢,Tunnel 网速就很好。
Cloudflare warp 和 Tailscale 的 subnet 又很像,都是可以暴露子网,又都需要使用各自的客户端起启用 VNP 访问。区别在于 Cloudflare 使用的 WARP 在国内已经连不上了,而 Tailscale 客户端还能正常用,它的 subnet 很好用。

6. 总结

至此,不提 Tailscale 我们一共有 5 种内网穿透的使用方式:单ipv4(STUN)、单ipv6(原生)、Tunnel(兜底)、URL重定向、加密隧道(VPN),其中最后这种方式在本文的开头提到的文章中有详说,这里不再赘述。

基于使用场景划分:

  1. 网页端浏览,用重定向方案。
  2. 大流量传输,用 VPN 方案。
  3. api 调用,用 Worker 反代方案。

后两种方案结合起来使用体验最丝滑,免费方案也有不输付费的体验🎉。

补充(2025/12/16)

实际使用发现个问题,Worker 中请求sub.v4.happy.de5.netsub.v6.happy.de5.net总是不能通,最后都会降级到sub.happy.de5.net。后来发现,从海外 ip 访问国内 ip 是会被 GFW 拦截的,ping 域名的时候只有第一次回复,之后就黑洞了,不会再回复了。而 Cloudflare 的节点都是在大陆地区以外的,被拦截就是正常情况了。除非是从国内 ip 访问,但那样花钱是一回事,还要备案实名比较麻烦。
因此,只能改变策略了,把关键的服务部署在公网上,有挺多 PaaS 厂商的服务可以选择,比如:ZeaburLeaflowKoyeb等,它们都有相当多的免费额度可以使用,这里就以 Zeabur 为例演示。

对我来讲,关键的服务就是Sub-Store,因此需要在 Zeabur 上部署它,但可惜的是挺多厂商封杀了sub-store镜像。但也有应对策略,那就是自己编译镜像换个名。

不得不提一嘴,官方的文档写的比较乱,看起来有些费解。并且官方的构建文件写的也不好看。因此在阅读文档➕实践验证过后,优化出来如下版本:

Sub-Store Project

Dockerfile

build.sh

Dockerfile
FROM node:24-alpine3.21

WORKDIR /opt/app

RUN apk add --no-cache curl unzip tzdata ca-certificates && \
    cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone && \
    mkdir -p /opt/app/data

ENV SUB_STORE_FRONTEND_HOST=:: \
    SUB_STORE_FRONTEND_PORT=3000 \
    SUB_STORE_BACKEND_API_HOST=:: \
    SUB_STORE_BACKEND_API_PORT=3001 \
    SUB_STORE_BACKEND_MERGE=true \
    SUB_STORE_BODY_JSON_LIMIT=10mb \
    SUB_STORE_FRONTEND_BACKEND_PATH=/subark \
    SUB_STORE_BACKEND_PREFIX=true \
    SUB_STORE_FRONTEND_PATH=/opt/app/frontend \
    SUB_STORE_DATA_BASE_PATH=/opt/app/data \
    SUB_STORE_MMDB_COUNTRY_PATH=/opt/app/data/GeoLite2-Country.mmdb \
    SUB_STORE_MMDB_ASN_PATH=/opt/app/data/GeoLite2-ASN.mmdb

ARG CACHEBUST="1970-01-01 00:00:00"
ENV BUILD_TIME=${CACHEBUST}

RUN echo "Building at ${BUILD_TIME}..." && \
    curl -L "https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.bundle.js" -o /opt/app/sub-store.bundle.js && \
    curl -L "https://github.com/sub-store-org/Sub-Store-Front-End/releases/latest/download/dist.zip" -o dist.zip && \
    unzip dist.zip && \
    mv dist frontend && \
    rm dist.zip && \
    chmod 777 -R /opt/app

RUN echo '#!/bin/sh' > entrypoint.sh && \
    echo 'echo "Build time: $BUILD_TIME"' >> entrypoint.sh && \
    echo 'exec node /opt/app/sub-store.bundle.js' >> entrypoint.sh && \
    chmod +x entrypoint.sh

CMD ["./entrypoint.sh"]
build.sh
#!/bin/bash
docker buildx create --use # 执行会创建并启动一个容器,所以执行一次就可以了
# 指定一个时区,否则默认是 UTC 时间
docker buildx build \
  --platform linux/arm64,linux/amd64 \
  -f Dockerfile \
  --build-arg CACHEBUST="$(TZ=Asia/Shanghai date '+%Y-%m-%d %H:%M:%S')" \
  -t {你的 Docker Hub 用户名:someone}/{你的项目名称:sube}:v4 \
  --push .

只需要一键./build.sh即可构建并推送到 DockerHub。绝大多数环境变量已经预定义了,无需过多设置。
此版移除了http-meta功能,内存占用暴降,极大降低了 Zeabur 上的花费。另外推荐使用 Gists 备份,这样也不用挂载目录了。

Docker Compose 部署如下:

compose.yaml
services:
  sube:
    name: sube
    image: someone/sube:latest
    ports: [3001:3001]
    volumes:
        - ./data:/opt/app/data
    environment:
        # 重要:一定要设置复杂的路径防止被扫导致泄露资源信息,/sube 只是演示值
        - SUB_STORE_FRONTEND_BACKEND_PATH=/sube

Zeabur 上需要暴露端口3001类型为HTTP,假设给定的域名是sube.zeabur.app,则浏览器地址栏输入https://sube.zeabur.app?api=https://sube.zeabur.app/sube"可一键访问前后端。

再次强调:构建自己的镜像使用,名称越无关越好。不要图方便用别人的,否则一旦别人被封,自己也会连坐。