从零到上线:我用 AI Agent 完成了两个全栈项目的云端部署
从购买域名,到 Cloudflare Tunnel 内网穿透,再到部署 Go-Zero 短链系统和 Rust + Next.js 博客,这篇文章记录了我一次完整的云上部署实践。
更特别的是,这次整个部署过程并不是我一个人完成的——我让运行在服务器上的 AI Agent 参与了环境检查、服务部署、故障定位和配置修改。很多原本需要不停搜索文档、翻日志、试命令的工作,都变成了人与 Agent 之间的一次次对话。
前言
一直以来,我都想拥有一个真正属于自己的域名,以及可以长期运行在公网环境中的服务。
不是简单地在本地执行:
npm run dev
然后看着:
localhost:3000
而是真正完成一次从服务器、域名、DNS、HTTPS、数据库、后端、前端,到最终公网访问的完整部署。
这次正好有一台腾讯云 Ubuntu 24.04 服务器,同时服务器上运行着一个可以直接操作 Shell、读取文件和执行命令的 AI Agent,于是我决定干脆把整个流程完整走一遍。
最终我成功部署了两个项目:
- 短链服务:https://short.zhangzhaoxue.asia
- 个人博客:https://zhangzhaoxue.asia
- 博客 API:https://api.zhangzhaoxue.asia
- 博客后台:
/admin - 博客技术栈:Rust + Next.js + PostgreSQL + Redis + RustFS
从最开始服务器连固定公网 IP 都没有,到最后两个项目全部通过 HTTPS 对外提供服务,中间踩了不少坑。
而很多真正有价值的经验,恰恰就藏在这些坑里。
第一步:部署之前,先搞清楚服务器到底是什么环境
很多人拿到服务器之后的第一反应是:
安装 Docker
拉代码
启动项目
但我现在越来越觉得,这其实不是最正确的顺序。
真正应该做的第一件事是:
搞清楚服务器的运行环境和网络模型。
于是我先让 AI Agent 对机器进行了一轮检查。
最终得到的信息大致是:
系统:Ubuntu 24.04 LTS
内存:约 7.4 GB
磁盘:约 50 GB
磁盘占用:约 38%
云厂商:腾讯云
区域:上海
内网 IP:10.4.**.**
权限:sudo 可用
服务管理:systemd 可用
真正关键的其实不是 CPU、内存或者硬盘。
而是网络。
这台服务器虽然可以正常访问互联网,也就是具备公网出口能力,但是并没有绑定一个稳定的公网入站 IP。
这意味着:
用户
↓
域名
↓
A 记录
↓
公网 IP
↓
服务器
这种最传统的部署方式根本行不通。
服务器可以主动访问外界,但外界无法直接通过一个固定公网 IP 找到它。
这一下基本决定了后面整个网络架构:
必须使用内网穿透或者反向隧道。
经验一:部署前一定先搞清楚网络模型
有没有固定公网 IP,看起来只是一个网络配置问题,但实际上会直接影响:
- DNS 如何配置
- HTTPS 如何实现
- 是否需要 Nginx
- 是否需要 FRP
- 是否需要 Tunnel
- 是否开放安全组端口
- 服务到底如何暴露到公网
所以部署的第一步不是写配置文件,而是:
先搞清楚自己的服务器到底拥有什么。
第二步:域名刚买下来,就遇到了 server hold
服务器的问题确认之后,我去 DNSPod 注册了自己的域名:
zhangzhaoxue.asia
本以为买完域名就可以开始解析了。
结果配置之后发现:
域名根本解析不了。
一开始我还怀疑:
DNS 没刷新?
NS 配置错了?
本地 DNS 缓存?
后来 AI Agent 通过 RDAP 查询域名状态,发现了两个状态:
server hold
add period
真正的问题就在:
server hold
什么是 server hold?
简单理解就是:
域名虽然注册成功了,但注册局当前禁止它参与正常 DNS 解析。
这时候不管你怎么配置:
A
AAAA
CNAME
TXT
全球 DNS 都不会正常解析。
而在国内域名注册场景中,一个非常常见的原因就是:
实名认证还没有完成。
于是我重新登录腾讯云控制台提交实名认证。
认证完成以后,域名状态恢复正常,DNS 才终于开始工作。
经验二:域名注册成功,不代表马上可以使用
尤其是在国内注册商购买域名之后,第一件事情应该检查:
域名实名认证
域名状态
NS 状态
如果域名处于:
server hold
那么后面配置多少 DNS 记录都没有意义。
这也是这次部署给我的第一个提醒:
遇到问题时,不要只盯着自己刚刚修改的配置。有时候问题根本不在这一层。
第三步:Cloudflare Tunnel,让没有公网 IP 的服务器也能提供公网服务
服务器没有固定公网 IP,于是下一个问题变成:
用户到底怎么访问我的服务器?
传统方案一般会想到:
FRP
ngrok
反向代理服务器
但这些方案很多都需要另外准备一台拥有公网 IP 的服务器作为中转节点。
后来我最终选择了:
Cloudflare Tunnel。
Cloudflare Tunnel 的核心思路
Cloudflare Tunnel 和传统端口映射最大的区别,是连接方向反过来了。
不是:
公网主动连接服务器
而是:
服务器主动连接 Cloudflare
流程大概是:
服务器
↓ 主动建立连接
Cloudflare Edge
↑
用户访问域名
服务器上的:
cloudflared
会主动向 Cloudflare 边缘节点建立一条长期加密连接。
于是用户访问:
https://zhangzhaoxue.asia
时,请求实际上先到:
Cloudflare Edge
然后 Cloudflare 再沿着已经建立好的 Tunnel,把流量送回服务器。
整个链路就变成:
用户浏览器
↓ HTTPS
Cloudflare
↓ Tunnel
cloudflared
↓
localhost
这带来了几个非常明显的好处。
第一:
服务器不需要固定公网 IP。
第二:
服务器甚至不需要直接开放公网入站端口。
第三:
HTTPS 证书由 Cloudflare 帮忙处理。
第四:
后面再增加新的项目,只需要继续按照 hostname 分流即可。
第四步:把 DNS 托管切换到 Cloudflare
首先在 Cloudflare 中添加:
zhangzhaoxue.asia
然后 Cloudflare 会生成两个 Nameserver,例如:
hunts.ns.cloudflare.com
jen.ns.cloudflare.com
随后回到 DNSPod,把域名原本的:
*.dnspod.net
Nameserver 替换成 Cloudflare 提供的 NS。
接下来 DNS 管理就正式转移到了 Cloudflare。
通过:
dig NS zhangzhaoxue.asia
可以检查全球 DNS 是否已经更新。
这个过程有时候几分钟完成,有时候则需要等待更长时间。
第五步:Cloudflare Tunnel 授权时也踩了一个小坑
服务器安装:
cloudflared
之后执行:
cloudflared tunnel login
命令会生成一个 Cloudflare 授权地址。
在浏览器打开之后,需要选择:
zhangzhaoxue.asia
进行授权。
授权完成后,Cloudflare 会把认证文件写到类似:
/home/******/.cloudflared/
的位置。
理论上整个过程非常简单。
但我第一次执行的时候,刚好遇到服务器访问 Cloudflare 回调接口超时。
浏览器最后变成了下载:
cert.pem
但是下载结束之后,我又没有立即定位到文件位置。
于是索性重新执行了一次:
cloudflared tunnel login
第二次网络正常,证书直接自动回传到了服务器。
问题就这样解决了。
经验三:一次性交互失败时,先考虑重试
很多部署问题其实不需要马上展开复杂排查。
尤其是:
OAuth 授权
网络请求
证书下发
镜像下载
临时 DNS 请求
这种一次性的网络交互。
如果操作本身:
没有副作用
可以安全重复
那么重新执行一次,往往是成本最低的解决方法。
第六步:创建 Tunnel,并用一个 Tunnel 承载多个服务
接下来创建 Tunnel:
cloudflared tunnel create shorturl
实际 Tunnel ID 已经脱敏,例如:
ab6a****-****-****-****-************
然后把不同域名全部绑定到同一个 Tunnel:
cloudflared tunnel route dns shorturl zhangzhaoxue.asia
cloudflared tunnel route dns shorturl short.zhangzhaoxue.asia
cloudflared tunnel route dns shorturl api.zhangzhaoxue.asia
Cloudflare 会自动创建对应的 CNAME。
然后真正决定:
哪个域名 → 哪个本地服务
的是:
config.yml
配置大概是:
tunnel: ab6a****-****-****-****-************
credentials-file: /home/******/.cloudflared/ab6a****-****-****-****-************.json
ingress:
# 博客前端
- hostname: zhangzhaoxue.asia
service: http://localhost:3000
- hostname: www.zhangzhaoxue.asia
service: http://localhost:3000
# 博客后端 API
- hostname: api.zhangzhaoxue.asia
service: http://localhost:8088
# 短链接服务
- hostname: short.zhangzhaoxue.asia
service: http://localhost:8090
- service: http_status:404
这时候一个很有意思的架构就形成了。
虽然服务器里面运行了很多服务:
Next.js
Rust API
Go API
gRPC
PostgreSQL
Redis
MySQL
etcd
RustFS
但是公网真正看到的只有:
zhangzhaoxue.asia
api.zhangzhaoxue.asia
short.zhangzhaoxue.asia
Cloudflare Tunnel 根据 hostname,把请求发送到不同的本地端口。
第七步:让 Tunnel 自己活着——systemd 服务化
Tunnel 如果只是在 Terminal 中运行:
cloudflared tunnel run
那么 Shell 一关,它也没了。
所以必须把它交给 systemd。
大致配置如下:
[Service]
User=******
ExecStart=/usr/local/bin/cloudflared tunnel \
--config /home/******/.cloudflared/config.yml \
run shorturl
Restart=always
RestartSec=5
这样:
服务器启动
↓
systemd
↓
自动启动 cloudflared
如果 cloudflared 意外崩溃:
systemd
↓
5 秒后自动重新拉起
至此,整个公网链路终于完整打通:
公网 HTTPS
↓
Cloudflare Edge
↓
Cloudflare Tunnel
↓
服务器 localhost
经验四:一个 Tunnel 完全可以承载多个服务
这也是 Cloudflare Tunnel 我比较喜欢的一点。
没有必要:
一个项目建一个 Tunnel
完全可以:
一个 Tunnel
+
多个 hostname
+
不同 localhost 端口
统一完成流量入口管理。
第八步:第一个真正上线的项目——Go-Zero 短链接系统
基础网络搭好以后,终于可以开始部署真正的项目了。
第一个上线的是:
open-portfolios/short-url
这是一个基于:
Go-Zero
实现的短链接服务。
它并不是一个简单 Demo,而是包含了一些比较完整的工程化设计,例如:
Bloom Filter
Singleflight
Redis 分布式锁
MySQL
Redis Cache
Prometheus
etcd 服务发现
部署这个项目的意义,也不仅仅是为了得到一个短链网站。
更重要的是,我想真正跑通一次:
Go 微服务
+
数据库
+
缓存
+
服务发现
+
公网 HTTPS
这样的完整链路。
第九步:短链系统的基础设施
整个项目需要:
Go 1.22
MySQL 8.0
Redis 7
etcd
其中:
MySQL 用于持久化:
sequence
short_url_map
Redis 用于:
序列号
缓存
分布式锁
etcd 则负责 Go-Zero 的:
服务注册与发现
整个架构可以简单理解为:
用户
↓
shorturl-api
↓
shorturl-rpc
↓
MySQL / Redis
↑
etcd 服务发现
第十步:etcd 端口问题,比想象中麻烦
项目要求 etcd 工作在:
127.0.0.1:12379
而不是默认:
2379
本以为改个端口就结束了。
结果 Ubuntu 包管理安装的 etcd 是由:
systemd
管理的。
而:
/etc/default/etcd
里面已经定义了一部分环境变量。
如果同时通过:
命令行参数
再次指定,就会出现参数冲突。
最后通过:
systemd override
解决。
例如:
[Service]
Environment=ETCD_NAME=
ExecStart=
ExecStart=/usr/bin/etcd \
--listen-client-urls=http://127.0.0.1:12379 \
--advertise-client-urls=http://127.0.0.1:12379 \
--listen-peer-urls=http://127.0.0.1:12380 \
--initial-advertise-peer-urls=http://127.0.0.1:12380 \
--initial-cluster=default=http://127.0.0.1:12380
这里还踩到了另外一个细节:
如果显式设置:
--listen-client-urls
那么:
--advertise-client-urls
也最好同步显式配置。
否则 etcd 本身的配置校验可能直接失败。
第十一步:Go 服务正式 systemd 化
代码编译:
go build -o bin/shorturl-rpc ./rpc/shorturl.go
go build -o bin/shorturl-api ./api/shorturl.go
最后生成两个真正运行的服务:
shorturl-rpc
shorturl-api
然后分别编写 systemd unit。
启动顺序:
shorturl-rpc
↓
shorturl-api
通过:
systemctl enable --now
让服务:
开机启动
+
自动恢复
上线之后进行了完整测试。
创建短链接:
POST /convert
可以正常生成:
62 进制短码
访问:
GET /b
可以返回:
HTTP 302
并成功跳转。
同时:
MySQL
Redis
etcd
Prometheus
全部正常。
到这里,第一个真正的公网项目正式上线。
第十二步:第二个项目——我的 Rust + Next.js 博客
短链部署成功以后,我开始部署第二个项目。
这个项目的复杂度明显高了一个等级。
它是我自己的博客项目:
zzx666-code/blog
技术栈:
Frontend
Next.js 16
Backend
Rust + Axum
Database
PostgreSQL
Cache
Redis
Object Storage
RustFS
Deployment
Docker Compose
这一次我没有继续全部使用 systemd。
而是按照项目原本的设计:
使用 Docker Compose 部署。
于是服务器上最终形成了一种混合架构:
Go 短链
→ systemd
Rust + Next.js 博客
→ Docker Compose
其实这也是我现在比较喜欢的一种方式。
部署方式没有必要统一。
真正应该统一的是:
可维护性
自动启动
故障恢复
第十三步:第一道墙——Docker Hub 拉不下来
服务器本身没有安装 Docker。
先安装:
docker.io
docker-compose-v2
结果刚开始:
docker pull
就超时。
原因其实也很直接:
Docker Hub 网络访问不稳定。
于是配置:
/etc/docker/daemon.json
加入镜像源:
{
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker.xuanyuan.me",
"https://hub.rat.dev"
]
}
然后:
systemctl restart docker
重新拉取镜像。
问题解决。
这个地方也让我意识到:
在国内服务器上部署项目,网络源配置并不是部署完成之后的优化,而应该是部署开始之前的基础设施。
第十四步:先把密码和环境变量处理好
随后从:
.env.production
复制:
.env
敏感字段不应该手写弱密码。
所以直接:
POSTGRES_PASSWORD=$(openssl rand -hex 16)
JWT_SECRET=$(openssl rand -hex 32)
RUSTFS_SECRET_KEY=$(openssl rand -hex 16)
生成随机值。
真实内容当然不应该写进博客。
可以统一理解为:
POSTGRES_PASSWORD=************************
JWT_SECRET=****************************************************************
RUSTFS_SECRET_KEY=************************
公网地址设置为:
NEXT_PUBLIC_SITE_URL=https://zhangzhaoxue.asia
NEXT_PUBLIC_API_URL=https://api.zhangzhaoxue.asia/api/v1
与此同时又发现一个端口冲突。
RustFS 默认端口和之前短链项目的 Prometheus:
9101
发生了冲突。
于是重新进行了端口规划,把 RustFS 调整到另外的本地端口。
这个问题不复杂,但很典型。
当服务器开始运行越来越多服务的时候:
端口规划会从“无所谓”逐渐变成必须管理的资源。
第十五步:第二道墙——Cargo 卡在 crates.io
Docker 开始构建 Rust 后端。
然后出现:
Updating crates.io index
接着:
十几分钟几乎没有变化。
又是网络问题。
Rust 项目依赖很多 crate,如果 crates.io 访问不稳定,Docker Build 就会长时间卡住。
于是给 Cargo 配置国内镜像:
[source.crates-io]
replace-with = 'rsproxy-sparse'
[source.rsproxy-sparse]
registry = "sparse+https://rsproxy.cn/index/"
[net]
git-fetch-with-cli = true
Dockerfile 中显式使用配置:
COPY Cargo.toml Cargo.lock .cargo-config.toml ./
RUN cargo build \
--config /app/.cargo-config.toml \
--release
配置完成之后,依赖终于开始正常下载。
Rust 后端最终成功编译。
第十六步:Next.js 一上来就给了我 50 个 TypeScript 错误
后端刚解决,前端又出问题了。
Next.js 构建直接出现几十个:
TS2322
错误。
问题集中在图标类型。
项目代码大量使用:
icon-critterpedia
icon-miles
这样的:
kebab-case
命名。
而当前发布版本的:
animal-island-ui
中:
IconName
只允许:
PascalCase
风格。
我甚至检查了多个版本,最终判断:
项目原作者很可能使用的是自己本地还没有正式发布的组件版本。
这类问题很有意思。
因为它属于:
TypeScript 编译期错误
但实际组件运行时对于未知 icon 又可以:
安全降级
并不会造成核心业务无法运行。
在明确了解风险之后,最终采取了一个比较务实的方式:
typescript: {
ignoreBuildErrors: true
}
让生产构建继续进行。
这里其实有一个很重要的原则:
技术上最“漂亮”的方案,并不一定是当前部署阶段最合适的方案。
前提是你必须知道:
自己跳过了什么
风险是什么
以后是否需要回来修
第十七步:真正最深的坑——服务全正常,但后台就是登录不了
所有容器终于启动。
博客首页:
正常访问
API:
正常运行
数据库:
正常
后台:
可以打开
看起来已经大功告成。
结果:
后台登录永远失败。
这个问题最后成为整次部署最值得记录的一次排查。
第十八步:先从服务端验证开始
第一步,不猜。
直接:
curl
调用后端登录 API。
结果:
HTTP 200
登录成功。
于是首先排除:
账号错误
密码错误
数据库错误
后端登录逻辑错误
第二步:
真实浏览器点击登录按钮。
结果发现:
请求压根没有真正成功到达 API。
第三步:
浏览器控制台手动:
fetch(...)
同样失败。
现在问题范围已经非常明确:
服务器端正常
浏览器端失败
也就是说:
问题很可能发生在浏览器执行环境,而不是后端。
第十九步:第一个根因——NEXT_PUBLIC_* 是构建时变量
最后翻:
next.config.ts
和前端构建文件时,终于发现第一个真正的问题。
Next.js 中:
NEXT_PUBLIC_*
变量有一个非常容易忽略的特点:
它们会在 build 阶段直接写入浏览器端 JavaScript。
也就是说:
npm run build
发生的时候,值是什么,最终浏览器拿到的就是什么。
而项目 Dockerfile 原本没有:
ARG
接收:
NEXT_PUBLIC_API_URL
结果构建出来的前端代码里,API 地址还是仓库作者原来的:
api.itbug.shop
即使运行容器时:
environment:
里面设置了正确的:
https://api.zhangzhaoxue.asia
也已经晚了。
因为浏览器 JavaScript 在:
Docker Build
阶段就已经生成了。
最终修改:
build:
context: ./frontend
args:
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL}
并在 Dockerfile 中增加:
ARG
+
ENV
让变量真正进入:
npm run build
阶段。
第二十步:第二个根因——CSP 才是真正的隐形杀手
以为 API 地址改完就结束了吗?
并没有。
项目:
Content-Security-Policy
里面还有:
connect-src
限制。
原项目只允许浏览器连接:
https://api.itbug.shop
而我的:
https://api.zhangzhaoxue.asia
根本不在允许列表里。
于是浏览器直接把请求拦截了。
也就是说:
JavaScript
↓
fetch()
↓
CSP 检查
↓
BLOCK
请求甚至没有真正离开浏览器。
而前端恰好又把这种错误统一展示成:
用户名或密码错误
这也是为什么问题一开始看起来特别具有迷惑性。
修改:
connect-src
允许:
https://api.zhangzhaoxue.asia
重新构建后:
登录
↓
成功
↓
跳转 Dashboard
↓
Token 正常存储
后台终于完整可用。
这次部署最值钱的一条经验
如果使用:
Next.js
+
Docker
一定要记住:
NEXT_PUBLIC_ 是构建期变量。*
对于浏览器端 JavaScript:
docker run
时候再传 ENV,很可能已经没有意义。
正确思路是:
docker-compose
↓
build args
↓
Dockerfile ARG
↓
ENV
↓
npm run build
另外,如果出现这种情况:
curl 正常
浏览器失败
优先考虑:
CSP
CORS
HTTPS
证书
Mixed Content
浏览器安全策略
而不是第一时间怀疑:
数据库
用户名
密码
这次如果没有按照:
后端
↓
浏览器
↓
网络请求
↓
前端构建配置
一层层排查,很容易在错误方向浪费大量时间。
第二十一步:项目更新后,怎么做到数据不丢
博客上线之后还需要面对另外一个很现实的问题:
以后 GitHub 更新代码怎么办?
不能每次升级都重新部署所有数据。
最后形成了一套比较稳定的流程:
cd blog
git stash push -m "deploy-fixes" <本地部署修改文件>
git pull origin main
git stash pop
docker compose \
-f docker-compose.prod.yml \
up -d --build
这里最关键的是:
数据库数据
不应该存在容器本身。
实际使用:
postgres_data
redis_data
rustfs_data
Docker Volume 保存数据。
于是:
删除容器
重新构建镜像
重新创建容器
都不会影响真正的数据。
在正式更新之前,我还额外做了一次:
pg_dump
作为完整数据库备份。
于是升级策略变成:
Volume
+
Backup
+
Rebuild
升级完成后再次验证:
管理员登录
首页状态
API 状态
数据库数据
页面版本
最终成功完成更新。
最终架构
现在整台服务器大致是下面这个结构:
用户浏览器
│
HTTPS
│
▼
┌───────────────────┐
│ Cloudflare Edge │
│ HTTPS / CDN / DNS │
└─────────┬─────────┘
│
Cloudflare Tunnel
│
▼
┌────────────────────────────────────────┐
│ 腾讯云 Ubuntu 24.04 │
│ 无固定公网入站 IP │
│ │
│ cloudflared │
│ │
│ zhangzhaoxue.asia ───────→ :3000 │
│ api.zhangzhaoxue.asia ───→ :8088 │
│ short.zhangzhaoxue.asia ─→ :8090 │
│ │
│ ┌────────────────────────────────────┐ │
│ │ 博客:Docker Compose │ │
│ │ │ │
│ │ Next.js 16 :3000 │ │
│ │ Rust / Axum :8088 │ │
│ │ PostgreSQL │ │
│ │ Redis │ │
│ │ RustFS │ │
│ └────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────┐ │
│ │ 短链:systemd │ │
│ │ │ │
│ │ shorturl-api :8090 │ │
│ │ shorturl-rpc │ │
│ │ MySQL │ │
│ │ Redis │ │
│ │ etcd │ │
│ └────────────────────────────────────┘ │
│ │
└────────────────────────────────────────┘
整体可以概括为:
Cloudflare
负责公网入口
systemd
负责原生服务
Docker Compose
负责多组件应用
Volume
负责持久化数据
这套架构不算复杂,但已经具备了一个个人服务器应该有的大部分基础能力:
HTTPS
多域名
服务隔离
自动启动
崩溃恢复
数据库持久化
项目升级
备份
公网访问
AI Agent 在这次部署中到底做了什么
这次部署还有一个和以前非常不一样的地方:
很多排障过程都是我和 AI Agent 一起完成的。
过去遇到:
server hold
我可能需要:
Google
↓
看 Stack Overflow
↓
看官方文档
↓
试命令
现在则变成:
我描述问题
↓
Agent 检查环境
↓
执行命令
↓
分析输出
↓
提出下一步方案
↓
继续验证
比如后台登录失败的时候,实际排查链路是:
验证 API
↓
检查浏览器
↓
检查请求
↓
检查前端配置
↓
定位 NEXT_PUBLIC
↓
继续发现 CSP
↓
重新构建
↓
验证成功
这其实就是一个非常典型的:
Agent Loop
过程。
如果联系我之前写的 Agent 系列文章来看,这次部署实际上就是一次真实案例。
Agent 不是什么神秘东西。
它做的事情就是不断:
Observe
↓
Think
↓
Act
↓
Observe Again
服务器日志、命令输出、配置文件就是它的 Context。
Shell、文件操作就是它的 Tools。
而持续排查问题的过程,就是 Agent Loop。
当这些东西结合起来之后,AI 就不再只是:
告诉我应该输入什么命令
而开始变成:
直接帮我检查
直接帮我执行
直接帮我定位问题
这也是我这次部署感受最明显的一点。
最后的复盘
完成这两个项目之后,我总结了几个真正值得记住的经验。
1. 环境决定架构
不要一上来就:
装 Docker
先检查:
公网 IP
防火墙
系统
权限
磁盘
内存
端口
因为:
没有公网 IP
和:
有公网 IP
实际上是两套完全不同的部署路线。
2. 国内服务器上,镜像和依赖源不是小问题
这次遇到了:
Docker Hub
crates.io
后续实际上:
npm
GitHub
同样都可能遇到类似问题。
所以在国内云服务器上:
Mirror
应该是基础设施,而不是出了问题以后才想起来配置。
3. 一定要理解“构建时”和“运行时”
这是我这次最大的技术收获之一。
尤其:
Next.js
+
Docker
场景。
必须清楚:
Build Time
和:
Runtime
到底分别发生了什么。
否则就会出现:
ENV 明明是对的
浏览器为什么还是访问旧 API?
这种特别难排查的问题。
4. curl 正常,浏览器不正常,就优先怀疑浏览器
包括:
CSP
CORS
Cookie
SameSite
HTTPS
证书
Mixed Content
浏览器本身就是一个非常严格的安全执行环境。
服务器能请求成功,并不能证明浏览器也允许这么做。
5. systemd 和 Docker 没有必要二选一
我最后的结构就是:
Go
→ systemd
Rust + Next.js
→ Docker Compose
完全没有问题。
适合原生部署的:
systemd
适合多组件管理的:
Docker Compose
没有必要为了所谓:
技术栈统一
强行选择同一种方案。
6. 数据和计算一定要分离
容器可以删除。
程序可以重新编译。
镜像可以重新构建。
但:
数据库
用户上传文件
对象存储
不能跟着一起消失。
所以:
Docker Volume
+
数据库备份
一定要有。
7. 自动恢复比“成功启动一次”更重要
一个真正上线的服务不能依赖:
我登录服务器
手动启动
至少应该做到:
机器重启
→ 服务自动恢复
程序崩溃
→ 服务自动拉起
所以:
systemd Restart
Docker restart policy
这些看起来不起眼的东西,其实才决定服务能不能真正长期运行。
8. AI Agent 正在改变我解决问题的方式
这是这次部署最特别的体验。
以前遇到问题时:
问题
↓
搜索
↓
阅读
↓
尝试
↓
失败
↓
继续搜索
而现在逐渐变成:
问题
↓
Agent 获取环境信息
↓
Agent 执行工具
↓
获得反馈
↓
继续判断
↓
直到定位问题
我依然需要:
理解问题
判断方案
确认风险
做最终决定
但大量:
搜索命令
检查日志
修改配置
重复验证
开始可以交给 Agent。
这让我越来越觉得:
AI Agent 真正有价值的地方,不只是“它会回答问题”,而是它可以进入真实的工作环境,通过工具不断获取反馈,并和人一起把事情真正做完。
写在最后
从一个:
没有固定公网 IP
的 Ubuntu 服务器开始,到最后真正跑起来:
Go-Zero
Rust
Next.js
MySQL
PostgreSQL
Redis