<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>阿里云ECS on 老朱的赛博工位</title><link>https://blog.zjkun.top/tags/%E9%98%BF%E9%87%8C%E4%BA%91ecs/</link><description>Recent content in 阿里云ECS on 老朱的赛博工位</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Wed, 21 Oct 2026 13:30:00 +0800</lastBuildDate><atom:link href="https://blog.zjkun.top/tags/%E9%98%BF%E9%87%8C%E4%BA%91ecs/index.xml" rel="self" type="application/rss+xml"/><item><title>99 元 ECS 上线踩坑单：nginx+node 双容器与 Docker/HTTPS 那些坑</title><link>https://blog.zjkun.top/posts/ecs-deploy-pitfalls/</link><pubDate>Wed, 21 Oct 2026 13:30:00 +0800</pubDate><guid>https://blog.zjkun.top/posts/ecs-deploy-pitfalls/</guid><description>99 元/年 2核2G 阿里云 ECS，从 Docker 安装到 my-nuxt-app 上线再到 certbot HTTPS，11 天踩的全部实坑：加速器白名单、win32 原生包混入、无扩展名 import ESM 炸、Nitro 不读 .env、续期锁。现象→根因→修法，照单排查。</description><content:encoded><![CDATA[<p>10 月 10 日 18:56，我的 Nuxt3 企业管理系统 my-nuxt-app 在 99 元/年的阿里云 ECS 上跑起来了。从下单到 10/20 博客挂上 HTTPS，十一天踩的坑我全记在部署档案里——这篇按「现象→根因→修法」完整放出，给同样用便宜机器的人照单排查。</p>
<h2 id="机器与前置决策">机器与前置决策</h2>
<ul>
<li><strong>机型</strong>：阿里云 e 实例 2 核 2G，99 元/年续费同价，深圳（长沙延迟约 20ms），公网 IP 47.112.2.228</li>
<li><strong>系统</strong>：Ubuntu 22.04.5，<strong>不勾任何预装应用</strong>（宝塔/WordPress/Docker/LNMP 全不要，避免占用 2G 内存）</li>
<li><strong>内存预算</strong>：nginx 64MB + node-app 384MB + mysql 512MB + ai-demos 512MB ≈ 1.2G，加系统开销很紧——<strong>开工先加 swap</strong>：<code>/swapfile</code> 2G 生效后才继续；装完 Docker 实测 used 242Mi / available 1.3Gi，健康</li>
<li><strong>向量/模型推理一律走云端 API，不在本机跑</strong>——2G 内存的机器想都别想</li>
</ul>
<h2 id="坑-1docker-官方脚本被网络重置">坑 1：Docker 官方脚本被网络重置</h2>
<p><strong>现象</strong>：<code>curl get.docker.com | sh</code> 直接失败。
<strong>根因</strong>：官方安装源网络不通。
<strong>修法</strong>：apt 装 <code>docker.io</code>（装出来 Docker 29.6.2，够用）。别跟官方脚本较劲。</p>
<h2 id="坑-2个人版镜像加速器白名单">坑 2：个人版镜像加速器白名单</h2>
<p><strong>现象</strong>：配了阿里云专属加速器（写入 <code>/etc/docker/daemon.json</code>）后，<code>nginx:alpine</code> 秒拉，<code>node:22-alpine</code>、<code>node:24-alpine</code> 全部报 <code>not found</code>。
<strong>根因</strong>：个人版加速器有白名单，node 新版镜像不在名单里——不是你的 compose 写错了。
<strong>修法</strong>：加 daocloud 源 <code>https://docker.m.daocloud.io</code> 兜底，放进 daemon.json 的 mirrors 数组<strong>首位</strong>。</p>
<h2 id="坑-3compose-旧-command-让容器永远起不来">坑 3：compose 旧 command 让容器永远起不来</h2>
<p><strong>现象</strong>：容器在，端口不通，日志里一直循环输出等待信息。
<strong>根因</strong>：compose 的 command 还是老模板里「等 server.js 出现」的死循环——而 Nitro 生产入口根本不是 server.js。
<strong>修法</strong>：command 直接换成 <code>node .output/server/index.mjs</code>，配 working_dir。上线架构：/root/app 下 compose 双容器，nginx:80 → node-app:3000（注意 <strong>service 名是 <code>node-app</code> 不是 <code>app</code></strong>，互指会报错）。</p>
<h2 id="坑-4windows-构建产物混入-win32-原生包">坑 4：Windows 构建产物混入 win32 原生包</h2>
<p><strong>现象</strong>：本地 Windows <code>npm run build</code> 产物传服务器，Linux 容器里起不来，报错指向 <code>@img/sharp-win32-x64</code>、<code>@node-rs/jieba-win32-x64-msvc</code> 这类包。
<strong>根因</strong>：跨平台构建，产物里带了 Windows 专属二进制。
<strong>修法（临时）</strong>：从 <code>.output/server/package.json</code> 删掉所有 <code>win32|msvc</code> 条目 → 容器内 <code>npm i --omit=dev</code>（npmmirror 源）换 Linux 二进制。还有个 npm optionalDependencies 的 bug：<code>@node-rs/jieba-linux-x64-gnu@2.0.2</code> 不会自动装，要<strong>手动补装</strong>。</p>
<h2 id="坑-5无扩展名-importdev-能跑生产-esm-必炸">坑 5：无扩展名 import——dev 能跑，生产 ESM 必炸</h2>
<p><strong>现象</strong>：<code>import '@node-rs/jieba/dict'</code> 本地 dev 一切正常，生产启动直接报错。
<strong>根因</strong>：无扩展名 import 在纯 ESM 生产环境解析不了。
<strong>修法（临时）</strong>：产物 sed 成 <code>dict.js</code>——注意 index.mjs 和 chunks/ <strong>都要扫</strong>。
<strong>修法（根治，10/12 做的）</strong>：分词器整体迁移 <strong>@node-rs/jieba → <a href="mailto:segmentit@2.0.3">segmentit@2.0.3</a>（纯 JS）</strong>，坑 4、5 一并消灭：</p>
<ul>
<li>package.json 卸 jieba 装 segmentit</li>
<li><code>server/utils/jieba.ts</code> 重写：<code>useDefault(new Segment()).doSegment</code> 取词，补 WORD_ONLY 正则滤标点</li>
<li><code>nuxt.config.ts</code> 加 <code>nitro.externals.inline: ['segmentit']</code>——CJS 命名导入的坑，产物实测炸过一次，内联解决</li>
<li>新增 <code>types/segmentit.d.ts</code></li>
</ul>
<p>验证：本地 <code>npm run build</code> 通过，<code>node .output/server/index.mjs</code> 启动打出 <strong><code>[bm25] 索引预热完成：172 chunk/3015 词项/2688ms</code></strong>——生产路径真跑通分词，<code>.output</code> 内无 node-rs 残留。从此「本地 build → 传产物 → 重启」就完事，不用登服务器装依赖。</p>
<h2 id="坑-6nitro-生产不读-env">坑 6：Nitro 生产不读 .env</h2>
<p><strong>现象</strong>：明明 <code>.env</code> 在，生产容器里环境变量全空。
<strong>根因</strong>：Nitro 只在 dev/build 阶段读 <code>.env</code>，生产运行时不加载——这是文档里写着的，但踩了才有记忆。
<strong>修法</strong>：compose 加 <code>env_file: .env</code> 注入。顺带确认：TiDB Cloud（东京 gateway:4000）从 ECS 直连无白名单拦截，连通✓。</p>
<h2 id="坑-7certbot-续期的两个细节">坑 7：certbot 续期的两个细节</h2>
<p>10/20 给 blog.zjkun.top 上 HTTPS（A 记录 → 47.112.2.228，Let&rsquo;s Encrypt 有效期到 2027-01-18）：</p>
<ul>
<li>证书用 docker 版 certbot <strong>webroot 方式</strong>签发；nginx 80 端口只留 <code>/.well-known/acme-challenge/</code>，其余 301→https</li>
<li>自动续期：<code>/etc/cron.d/certbot-renew</code> 每日 04:30 跑 <code>certbot renew --webroot &amp;&amp; nginx reload</code></li>
<li><strong>踩坑</strong>：首次 dry-run 被「残留实例锁」挡过——之前手工跑的 certbot 会话没退干净。kill 旧进程后正常。看到 lock 报错先查进程，别怀疑配置</li>
</ul>
<h2 id="一键部署脚本">一键部署脚本</h2>
<p>博客现在是纯静态挂载，不动 node-app 容器：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-powershell" data-lang="powershell"><span style="display:flex;"><span><span style="color:#75715e"># deploy_blog.ps1：本地 hugo 构建 → tar → scp → 服务器解压</span>
</span></span><span style="display:flex;"><span>$ErrorActionPreference = <span style="color:#e6db74">&#39;Stop&#39;</span>
</span></span><span style="display:flex;"><span>&amp; <span style="color:#e6db74">&#39;D:\tools\hugo\hugo.exe&#39;</span> -s D:\study\blog --minify -d public
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> ($LASTEXITCODE <span style="color:#f92672">-ne</span> <span style="color:#ae81ff">0</span>) { <span style="color:#66d9ef">throw</span> <span style="color:#e6db74">&#39;hugo build failed&#39;</span> }
</span></span><span style="display:flex;"><span>tar.exe -czf D:\study\blog_dist.tar.gz -C D:\study\blog\public .
</span></span><span style="display:flex;"><span>scp -o BatchMode=yes D:\study\blog_dist.tar.gz root@&lt;server&gt;<span style="color:#960050;background-color:#1e0010">:</span>/root/app/blog_dist.tar.gz
</span></span><span style="display:flex;"><span>ssh -o BatchMode=yes root@&lt;server&gt; <span style="color:#e6db74">&#34;rm -rf /root/app/blog/*; tar -xzf /root/app/blog_dist.tar.gz -C /root/app/blog; rm /root/app/blog_dist.tar.gz; echo DEPLOYED&#34;</span>
</span></span></code></pre></div><p>改 nginx/compose 前一律先备份（blog.conf.bak-20261020b、docker-compose.yml.bak2），改坏能秒回滚。</p>
<h2 id="还开着的坑如实说">还开着的坑（如实说）</h2>
<ul>
<li><code>GET /api/document</code> 仍 500：<code>Cannot find package 'canvas'</code>（nitro externalized 但 package.json 没声明），修法已写明待执行</li>
<li>产物里 sharp 仍是原生包，「零重装部署」要等它也纯 JS 化才算完全达成</li>
<li><code>CHROMA_URL</code> 还指 localhost:8000，用到向量检索时要处理；<code>restart: always</code> 待确认</li>
</ul>
<h2 id="收个尾">收个尾</h2>
<p>七个坑里六个是同一类病：<strong>「本机能跑」≠「Linux 容器里能跑」</strong>——平台二进制、模块解析、环境变量，全是跨环境的那三堵墙。2 核 2G 的机器跑 nginx+node-app 绰绰有余，重活（数据库、向量、大模型）全部外包给云 API，99 元的快乐就回来了。</p>
<blockquote>
<p>上线全记录基于 2026 年 10 月 10-20 日实操档案；服务器账号与密钥细节不在本文范围。</p>
</blockquote>
]]></content:encoded></item></channel></rss>