跳到主内容
文章阅读

我的项目上线之旅

2026/10/012 次阅读27 分钟

从零到上线:我用 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
返回顶部