Sanvi

5 分钟

折腾 StudyThai 国内版:阿里云 ECS 一周踩坑笔记

两周前刚写完把 StudyThai 搬到新加坡的笔记,正打算缓两天,发现 ICP 备案的事躲不掉了。备案要求服务器在大陆境内,主域在新加坡的那套架构对监管来说不存在——要做大陆合规通路,就得另开一台 ECS,把同一份代码在阿里云上再跑一遍。

我以为这是个一晚上能搞定的活。结果一周过去,平均每天有一两个小时在跟阿里云较劲。记一下,免得三个月后再开新实例时又把同样的坑踩一遍。

为什么不能继续用新加坡那台

简单说:ICP 备案号是大陆生态的"准入证"。没有备案:

  • 上不了微信小程序(域名校验那一步直接拒)

  • 上不了国内任何应用商店的 Webview 入口

  • 不能投信息流广告(巨量引擎 / 腾讯广告 / 小红书都查备案)

  • 大陆访问会被运营商间歇性掐——不一定挂,但很慢、很抽风

所以决定主域留新加坡跑国际,单独开一个 .com.cn 域名跑大陆合规版本,部署到阿里云 ECS 上,分支叫 studythai-cn

逻辑很简单。开始干活,第一天部署就跪了。

周一晚上:Coolify 自己拉不下来 helper 镜像

实例开好、Coolify 装好、push 一下代码触发部署。Coolify Dashboard 显示进度条转了 8 分钟,然后红了:

Preparing container with helper image: ghcr.io/coollabsio/coolify-helper:1.0.14

第一反应是"我的镜像是不是名字写错了"。看了半天才反应过来——这一行报的根本不是我的镜像。这是 Coolify 自己用的"工头"镜像,每次部署都要先 docker run 一下它,让它去 clone 代码、跑 build、push 结果。helper 拉不下来 = 部署根本没开始,我的代码连"被尝试"都没尝试。

根因后来想清楚:阿里云 ECS 到 ghcr.io 走的是国际链路,晚高峰丢包严重,几百 MB 的镜像在默认 timeout 内拉不完。ghcr.io 又不像 docker.io 有阿里云官方 mirror,是个事实上的"国内运维盲区"——出了问题没人替你兜。

修法走了点弯路。先去改 /etc/docker/daemon.jsonregistry-mirrors 配了 dockerproxy,重启 docker 再试,还是不通。又翻了半小时文档才意识到:Docker 的 registry-mirrors 只对 docker.io 生效ghcr.io 是另一套域名前缀,配了等于没配。这一段纯属浪费时间。

最后用的是南京大学开放的 ghcr mirror。手动拉一次,然后给它起一个 ghcr.io/... 的别名:

docker pull ghcr.nju.edu.cn/coollabsio/coolify-helper:1.0.14

Docker 找镜像是"本地优先"——本地已经有一个叫这个名字的镜像了(虽然实际是从 nju 拉的),Coolify 再 docker run 时就直接命中缓存,根本不走网络。

5 分钟解决,但这是个长期债。Coolify 每次升级 helper 版本号会变,缓存就失效,又得跑一次。我后来加了个 cron 脚本每天预拉一次最新版兜底,但本质上还是在跟"Coolify 把 helper 硬编码"这个设计赛跑。

第一天到这里以为搞定了。

周二:业务镜像也卡,但上一招不管用

第二天再触发一次部署,新的失败行:

Pulling image: ghcr.io/sanvibyfish/studythai-cn-dev:abc1234

这次拉的是我自己的业务镜像,GitHub Actions build 完推到 ghcr.io 的那个。同样的网络问题。我想"那再 nju mirror + retag 一次就行了吧"——然后意识到这次不行。

两个原因:

一是业务镜像是 private 的。代码不想公开。第三方公益 mirror 只支持 public,private 的鉴权 token 是 GitHub 颁发的,第三方没法转发——直接配会 401 拒绝。

二是业务镜像 tag 每次部署都不一样。helper 那种"预拉一次永久用"的招在我自己的镜像上不成立——明天的 commit sha 还没出生,怎么预拉。

那一晚上我把各种方案在脑子里过了一遍:

  • 把镜像改成 public:代码里 bundle 着不少业务逻辑,不想公开,否决

  • 自建 ghcr 反代:要租一台境外 VPS 跑 nginx,配 SSL,维护麻烦

  • Docker Hub 免费 plan:只能 1 个 private repo + 200 pull/6h,频繁部署会被限流

  • 阿里云 ACR 企业版:100+ 元/月,个人项目不值

  • 本地 docker save + scp:太土了,先不想

调研到一半的时候才想起来去搜一下"ACR 个人版"——结果阿里云 ACR 个人版完全免费。3 个命名空间 + 300 repo + 单 image 5GB,对我这种个人项目纯纯够用。

那一刻有点泄气。我前面浪费了大半天调研"看起来更通用"的方案,最后才发现阿里云自己就有现成的免费档。腾讯云、华为云也都有。云厂商把免费档放在那里就是为了绑客户,正好被我用上,根本不丢人。下次我第一反应应该是"这家云厂商有没有现成的免费方案",而不是"我能不能自己搭一个"。

链路改造很轻:GitHub Actions 多 push 一份到 ACR,Coolify 改用 ACR 的 VPC 内网地址——因为我的 ECS 和 ACR 同 region(杭州),pull 走阿里云内网,几百 MB 几秒拉完,零流量费。push 走公网(本地/CI 不在 VPC 里只能走公网),pull 走 VPC(ECS 在 VPC 里享受内网),这是 ACR 设计的核心模式,搞懂之后觉得设计得挺漂亮。

接下来一周:Coolify 时不时变红,没规律

镜像问题彻底修完,部署是通了。我以为该歇了。

结果接下来几天,Coolify Dashboard 里这台 ECS 隔三差五变红——"Server is reachable" 验证失败。有时候几分钟自己好,有时候挂半小时。最让人崩溃的是没规律:早上好好的,中午突然挂,晚上又好了。

SSH 上去看,能连。Docker 正常。Coolify 容器在跑。但 Coolify 的 health check 就是不通。

排查走了三层,每一层都让我学到点新东西。

第一层是阿里云安全组。这是最坑的。阿里云的安全组是白名单制,22 端口默认只对"快速配置"里勾过的 IP 开放。我建实例时图省事勾了"我的当前 IP",结果家里宽带换了 IP,Coolify 服务器(在 Singapore)的出口 IP 也没在白名单里——它从一开始就只是偶尔能通。"偶尔能通"是因为 Singapore 那台 VPS 出口偶尔命中了我加过的某条历史规则,纯靠运气。

把安全组临时改成 22 端口 0.0.0.0/0,立刻稳了。我之前以为"老掉线"是阿里云不稳定,结果其实是阿里云太稳了——稳到我自己配错的规则它一条不漏地执行。后来缩回精准放行 Coolify Singapore 那台机器的公网 IP,安全和便利都有。

**第二层是阿里云的"突发性能型"实例陷阱。**我开的是 t6 入门款,看着便宜(每月几十块),后来发现一个规律:每次跑完一次 Coolify 部署,接下来 1-2 小时 SSH 响应都很慢,health check 偶尔超时

查了下才知道 t5 / t6 的 CPU 是 基线 10% + 攒积分模式:平时只能用 0.2 颗 CPU,攒够积分才能短时跑满。build 一次 Next.js 直接把积分吃光,build 完 sshd 就被限速到 10% 跑,自然慢。

阿里云这一档是表面便宜实际坑——任何要持续 build / 跑后台任务的服务都不该用 burstable 型。最后升级到 g6 普通型,贵 2-3 倍,但国内业务起来后这点钱值得。如果是真的省钱,应该用阿里云的 ECI 或抢占式实例,绕开 burstable 这个陷阱品类

**第三层是跨境 SSH 抖动。**Coolify 在 Singapore,ECS 在杭州,中间走国际出口。晚 8-12 点大陆使用高峰,国际链路丢包率经常 20%+,SSH handshake 在默认 10s timeout 内完不成就报失败。

这个没办法根治——除非把 Coolify 也搬到大陆(那就要再备案一次)。最后做了两件事降噪:SSH timeout 调到 30s,health check 间隔从 1 分钟拉到 5 分钟。少误报,但响应慢,是个权衡。

还有个隐形雷:磁盘

Singapore 那篇文章里写过 3 月份的事故——Coolify 累积旧 docker 镜像把磁盘撑到 100%,PostgreSQL 崩了,全站 15 分钟不可用。

这次在阿里云开 ECS 时我主动开了 100GB 系统盘(默认 40GB),第一周就加了 cron:

# 每周日凌晨清 7 天没用的 image + 已停的容器 + 悬空 volume

Coolify 自己没有"自动清理"开关。这是它的一个长期产品 gap,几乎所有 Coolify 用户最终都会踩——3 月那次让我学一次就够了

几条想跟一周前的自己说的话

合上电脑回想这一周,几句最想跟一周前的自己说的:

**ECS 直接选 g6 普通型,别贪 t6 那几十块。**真要省钱用 ECI 或抢占式,别碰 burstable——它不是"便宜版",是"看上去便宜的陷阱版"。

**系统盘开 100GB+。**默认 40GB 三个月必爆,到时候在凌晨抢救 PostgreSQL 你会想现在多花的那点钱。

第一天就开 ACR 个人版(免费)。配好 ECS 到 ACR 的 VPC 内网地址,省下后面所有跟 ghcr.io 较劲的时间。

安全组别勾"快速配置"那个"我的当前 IP"。写死 Coolify 服务器的固定公网 IP,外加你自己的管理员 IP,不要图省事 0.0.0.0/0,也不要让阿里云替你猜。

GitHub Actions 默认双 push——ghcr.io(开发可见性)+ ACR(生产 pull 源)。不要等"等 ghcr 真的拉不动了再加 ACR",那时候你已经在凌晨救火。

装完 Coolify 第一件事是加 docker system prune 的 cron,不是先去配域名。

Coolify 跨区管理时,SSH timeout 调 30s,health check 间隔 5 分钟。默认值是按"同机房延迟"设计的,跨国跨网都不合理。

整周的总结其实是一句话

Singapore 那次的总结是"提前预演 + 一行 DNS 回滚"——技术上的优雅。

这次的总结很不优雅:国内云有一种叫"环境成本"的隐形税——GitHub 慢、突发型 CPU 陷阱、安全组复杂、跨境抖动……单个都不大,叠起来能让我前两周一直在救火。

我之前一直以为这种"环境成本"是工程能力不够才会撞——总觉得"别人 / 资深点的人不会踩这些坑"。这一周明白过来,这些坑就是基础设施本身的一部分,不是你的水平问题。承认它、把它写进预算和流程里,比试图"用对的方式"绕开它便宜得多

合规税就是合规税。下次再开新实例,第一天就按文末那张清单跑一遍,剩下的时间留给真正的业务。