这是“AI 时代独立开发者服务器实验”的结束篇。上篇完成了 SSH 加固、服务器底座、后端骨架和 ICP 备案;这一篇把服务真正部署到服务器,并通过公网 HTTPS 验收
我的个人 ICP 备案通过了
看到“管局审核通过”时,很容易产生一种错觉:域名已经备案,服务器也买好了,接下来应该可以直接让小程序访问
但当时服务器的真实状态是:
备案解决的是合规前置条件,不会替我上传代码、启动容器、解析域名或签发证书
所以结束篇真正要完成的是这条链路:
本地后端代码 -> 安全上传 -> 服务器内部部署 -> 内部接口验收 -> 创建恢复点 -> DNS 解析 -> Caddy 自动 HTTPS -> 公网内外双重验收
目标也保持得很小:暂时不做用户系统、数据库或真实业务,只为已经上架的“惜诗工具箱”和“汉字防线”建立一个共享后端,并完成连通测试
这台服务器只有 2 核 2GB。数据库不是不能装,但在没有数据模型和真实业务需求时提前安装,只会增加内存占用、备份责任和安全面
因此第一版采用:
两个产品暂时共享一个 API 服务,但使用独立路由命名空间:
这样做不是为了把两个产品永久绑在一起,而是先复用部署、日志、HTTPS 和监控基础设施。以后业务变复杂,可以继续拆分服务,路由边界不会混乱
第一次部署没有直接开放公网 80 和 443,而是使用下面的内部结构:
服务器本机 curl | v127.0.0.1:8080 | vNginx 容器 :8080 | vFastify API 容器 :3000
API 容器不映射任何宿主机端口,Nginx 只绑定127.0.0.1:8080。即使阿里云防火墙配置失误,外网也不能直接访问 API 的 3000 端口或内部 Nginx 的 8080 端口
这种顺序的好处是把两个问题分开:
如果内部部署失败,就不需要同时排查 HTTPS
Codex 为项目生成了scripts/upload-project.sh。它不是简单地把整个目录复制到服务器,而是只上传部署需要的文件:
它会排除:
服务器上的.env不参与覆盖。重复上传前后,脚本会比较它的指纹,但不会显示内容;上传结束也不会自动启动或重启容器
在 Mac 项目目录执行的命令只有这一类:
bash scripts/upload-project.sh \ "$SERVER_TARGET" \ "$IDENTITY_FILE"
服务器地址和私钥路径只保存在当前终端变量中,不写进项目,正常结果包含:
ENV_FILE_PRESERVED=presentUPLOAD_OK remote_dir=server-labUpload completed. Containers have not been started.
这里最重要的一句话是最后一行。上传和发布是两个不同动作:上传成功不应该自动把未经确认的代码推到线上
整个过程里最容易犯的错误,不是 Linux 命令本身,而是“当前到底在哪台电脑上”
我在 Mac 终端执行:
cd "$HOME/server-lab"sudo bash scripts/deploy-first-service.sh
但服务器项目实际位于服务器的/home/admin/server-lab。Mac 上当然没有这个目录,cd失败后,下一行sudo bash仍然在原目录继续执行,于是又出现Missing .env
后来做了两层修正
第一层,用&&连接依赖前一步成功的命令:
cd "$HOME/server-lab" &&sudo bash scripts/deploy-first-service.sh
第二层,部署和验收脚本在任何 Docker 操作前先检查:
system=Linuxuser=adminproject=present
如果在 Mac、错误用户或错误目录执行,脚本会立即退出
这比提醒自己“下次看清提示符”更可靠。人会重复犯错,自动保护应该让同一种错误只造成一次教训
第一次构建失败:not found 不一定是镜像不存在
在服务器执行部署脚本后,第一次构建停在:
node:22-bookworm-slim: not found
同时 Docker Compose 提示没有安装 buildx。Codex 没有先去安装 buildx,因为这个提示只是构建器回退警告,真正失败的是基础镜像元数据解析
只读诊断得到三组结果:
结论是:Docker 本身正常,阿里云镜像加速服务也在线,但当前加速入口没有所需镜像;服务器又无法直连 Docker Hub
已经缓存的hello-world能运行,只能证明本地 Docker 运行时正常,不能证明其他镜像现在还能拉取
接着,我在 Mac 和服务器上验证了 DaoCloud 项目级代理
Node 和 Nginx 的 OCI 清单都返回 HTTP 200,内容摘要也一致。项目于是临时改为显式代理地址,并把镜像固定到摘要,而不是只写一个可能变化的标签
第二次构建已经能解析摘要,却在下载真实镜像层时失败:
TLS handshake timeout
请求从镜像代理跳转到了 R2 对象存储。清单接口可达,但保存镜像层的数据链路在成都服务器上不可用
这一步改变了后面的验证标准:
判断一个镜像源是否可用,不能只看标签或清单返回 200,还要验证目标平台清单和至少一个真实镜像层的数据链路
新的候选来源是 AWS ECR Public 提供的 Docker Official Images
这次没有立即重试构建,而是先完成三层只读验证:
- 服务器读取真实镜像层的 1 个字节,返回 HTTP 206
Node 和 Nginx 均通过后,项目把基础镜像固定为 AWS ECR Public 地址加 OCI 摘要。固定摘要可以避免同一标签未来静默指向不同内容
但 Node 镜像能下载,不代表npm ci一定能成功。
检查package-lock.json后发现,其中有 81 个依赖地址指向registry.npmjs.org;服务器访问官方元数据和实际包文件均返回 000,而 npmmirror 元数据返回 200
最终没有手工改写锁文件,而是在构建时使用:
npm ci \ --registry=https://registry.npmmirror.com \ --replace-registry-host=always \ --no-audit \ --no-fund
这里有三个细节:
- 保留
package-lock.json,确保依赖版本仍被锁定 --replace-registry-host=always 让锁文件中的官方下载主机在本次安装中替换为可达镜像- npm 继续使用锁文件中的
integrity哈希校验下载内容
依赖漏洞审计不塞进生产镜像的网络构建路径,而是在独立验证阶段执行。这样不会因为审计服务网络波动,让一个内容已经确定的生产构建无故失败
最终配置再次安全上传后,服务器返回:
PINNED_IMAGE_POLICY=verifiedNPM_SOURCE_POLICY=verifiedDEPLOY_CONTEXT_POLICY=verifiedSERVICE_CHECK_POLICY=verifiedUPLOAD_OK remote_dir=server-lab
随后在服务器执行:
cd "$HOME/server-lab" &&sudo bash scripts/deploy-first-service.sh
这一次:
- AWS ECR Public 成功提供固定摘要的 Node 和 Nginx 镜像
npm ci
独立只读验收命令是:
sudo bash scripts/check-first-service.sh
最终标志:
FIRST_SERVICE_READ_ONLY_CHECK_OK
验收不只执行一次curl,还检查:
- 容器使用只读根文件系统和
no-new-privileges - 重启策略、内存、CPU 和 PID 限制与 Compose 一致
部署后,Mac 曾经关机,SSH 会话也断开了
重新连接服务器时,API 和 Nginx 已连续运行两天,仍然都是healthy,四个接口仍返回 HTTP 200
原因很简单:SSH 只是远程管理通道,不是服务器电源,也不是容器进程的父终端。容器由 Docker daemon 管理,并设置了:
restart: unless-stopped
只要云服务器本身和 Docker 服务还在运行,本地电脑关机不会让服务停止。这个结果看似基础,但对第一次管理服务器的人很重要:
关闭终端不会关闭云服务器;停止云服务器、停止 Docker 或显式停止容器,才会影响服务
内部部署验收完成后,我准备再创建一份系统盘快照,却收到:
SnapshotLimitExceed
轻量应用服务器最多保留 3 个快照。快照不是越多越好,配额有限时应按恢复价值选择
我删除了一个已经被后续恢复点覆盖的旧底座快照,并创建:
after-first-service-20260727
最终保留三个阶段:
删除快照不可恢复,所以这一步由我在控制台确认,Codex 只负责解释取舍和记录结果
根域名未来可能用于个人主页、产品介绍或下载页,所以 API 使用:
api.xishideai.cn
我在阿里云 DNS 中新增api的 A 记录,指向服务器公网 IPv4
DNS 保存后,第一次查询返回:
DNS_A_PENDING
稍后从权威 DNS、阿里云公共 DNS 和另一公共 DNS 查询,再把结果与控制台地址在本机内存中比较,最终返回:
DNS_A_MATCH
DNS 解析成功仍不等于服务已经上线。当时 Nginx 只监听回环地址,公网防火墙也没有新增 80,所以知道域名指向哪里的人依然访问不到业务
HTTPS 可以只用 Nginx 配合 Certbot,也可以让 Caddy 同时承担证书和代理
当前项目已经有一层 Nginx,里面承载请求大小、超时、健康检查和内部代理策略。为了避免在公网发布阶段同时改动太多,我选择:
公网 TCP 80/443 | vCaddy:证书、HTTP 跳转、安全响应头 | vNginx:内部反向代理 | vFastify API
Caddy 负责:
- 添加 HSTS、
X-Content-Type-Options、X-Frame-Options等安全响应头
Nginx 和 API 的内部端口设计保持不变。Caddy 的证书和私钥保存在服务器 Docker 命名卷
公网变更前,服务器只读预检确认:
- API、Nginx、Docker 和 containerd 正常
- 可用内存约 1.1GiB、Swap 约 2GiB、磁盘约 32GB。
Caddyfile 也不是上传后直接启动。先用临时、无宿主机端口的 Caddy 容器完成:
COMPOSE_CONFIG_OKCADDYFILE_FORMAT_OKCADDYFILE_VALIDCADDY_FINAL_PRESTART_CHECK_OK
这些检查不会申请正式证书,也不会让 80/443 开始监听
直到这里全部通过,我才单独确认阿里云防火墙变更:
- 新增公网 TCP 80,用于 HTTP 跳转和 ACME 验证
- 复用控制台中已经启用的 TCP 443 规则,不重复添加
- 不开放 3000、8080、8888 或数据库端口
服务器上的公网部署命令是:
cd "$HOME/server-lab" &&sudo bash scripts/deploy-public-https.sh
脚本会先复核镜像摘要、DNS、内部端口和 Compose 策略,再启动 Caddy。成功后,三个容器均为healthy
服务器侧完整验收:
sudo bash scripts/check-public-https.sh
最终返回:
PUBLIC_HTTPS_CHECK_OK
结果如下:
第一次公网验收在“Caddy 容器策略”处失败
只读诊断发现,Compose 中写的是:
NET_BIND_SERVICE
Docker 实际返回:
CAP_NET_BIND_SERVICE
它们是同一个 Linux capability 的两种规范化表示。容器策略本身没有问题,错的是验收脚本只接受其中一种字符串
修正检查逻辑后重新上传脚本,没有重启容器,完整验收通过
随后我在 Mac 上做真正的外网验证。HTTP 已经返回 308,但循环变量写成了path,接下来的curl、grep、openssl全部显示:
command not found
这不是服务器故障。zsh 中的小写path是与大写PATH绑定的特殊数组,给它赋值会破坏命令搜索路径
最终把变量改为request_path,在脚本里设置受控的标准PATH,并明确要求通过/bin/bash执行:
/bin/bash scripts/check-external-https.sh
结果返回:
MAC_EXTERNAL_HTTPS_CHECK_OK
服务器内部验收能证明容器和证书链路正常;Mac 外部验收则证明真实公网 DNS、HTTP 跳转、HTTPS 证书和响应头都能被用户侧访问。两边都通过,才算闭环
最终验收时的资源占用大约是:
这说明当前连通测试服务对 2 核 2GB 足够轻,但不能据此推断真实业务永远只占这些资源。数据库、图片处理、大量日志、并发请求和服务器内构建都会改变资源曲线
目前没有数据库,也没有真实业务数据。这反而让第一次上线更容易恢复:镜像或配置有问题,可以回滚 Compose 或系统盘快照,不需要处理数据一致性
服务器技术上线已经完成:
- Fastify、Nginx、Caddy 三个容器健康运行
产品域名接入已经完成:根据实际配置结果,“惜诗工具箱”和“汉字防线”的微信后台均已添加https://api.xishideai.cn为合法请求域名
公安联网备案已经提交:
cd- Docker Hub
not found可能是加速与网络问题,不一定是标签不存在 - 镜像清单 HTTP 200 不代表镜像层一定可下载
- 固定镜像摘要只能防止标签漂移,不能替代首次来源验证
- 本地电脑关机或 SSH 断开不会停止 Docker 服务
- Docker capability 可能以带
CAP_前缀的形式返回 - zsh 的
path是特殊变量,普通循环变量不要使用这个名字
如果只看最后的命令,这个项目似乎只是安装 Docker、写 Compose、申请证书
真正节省时间的是 Codex 把模糊问题变成了可验证的状态:
- 构建失败后,区分 Docker 运行时、镜像清单和镜像层网络
- 对
.env、镜像摘要、终端环境和端口范围加入自动保护 - 在修改防火墙、启动公网服务和删除快照前停下来等我确认
但 Codex 没有替我保管 SSH 私钥、输入密钥口令、决定删除哪个快照、确认公网防火墙,也不能替我承担备案与服务运营责任
这套分工贯穿整个系列:
人负责边界、秘密、身份和确认;Codex 负责分析、自动化、验证和记录
这三篇文章从一台 68 元的阿里云轻量服务器开始
第一篇,我体验宝塔,最终重置为纯净 Ubuntu,完成系统初始化、Swap、SSH 密钥和 Docker
第二篇,我关闭密码与 root 登录,补全内核更新链路,修复重启后的swappiness,搭好 TypeScript/Fastify 后端骨架,并完成域名和个人 ICP 备案
结束篇,我经历三次构建、镜像和 npm 网络排查、安全上传、内部部署、快照轮换、DNS、Caddy HTTPS,以及服务器内外双重验收
最后得到的并不是一台“什么都装了”的服务器,而是一套很小但完整的个人开发者基础设施:
可恢复的 Ubuntu 底座 + 只允许密钥的 SSH + Docker Compose + 可验证的镜像和依赖来源 + Fastify API + Nginx 内部代理 + Caddy 自动 HTTPS + DNS、快照、脚本与操作日志
不会 Linux,不代表可以跳过风险和验证;更现实的方式,是让 AI 把每一步翻译成“为什么做、应该看到什么、失败后怎么办”
这台服务器现在还没有真正的业务数据,但它已经具备了承接“惜诗工具箱”和“汉字防线”后端功能的入口