我只做了两件事:部署 DSH、开通 RAM,剩下的全交给它
这台阿里云 ECS 上现在跑着两个独立站点、两个域名、一张自动续期的证书、一套每日备份。
其中只有两件事是我亲手做的:把 DeepSeek Harness 装起来,以及在阿里云上给它开一个 RAM 身份。剩下的------建库、部署项目、开二级域名、配证书、排查故障------我都是"说了一句话"。
左边是 DSH 本体,右边是它部署出来的项目。同一台机器:2 vCPU / 1.8 GiB / 40 GiB。
零、先把分工说清楚
| 阶段 | 谁做 | 内容 |
|---|---|---|
| 准备 | 🧑 人 | 部署 DSH、开通 RAM |
| 执行 | 🤖 DSH | 建库、部署项目、开二级域名、扩证书、排查故障、加备份 |
| 决策 | 🧑 人 | 授权范围、破坏性操作确认、密钥轮换 |
为什么是这两件事必须人做?
- 部署 DSH:鸡生蛋问题。Agent 还没跑起来,只能人装。
- 开通 RAM:授权不能自己给自己。这是唯一一处"必须由更高权限的人点头"的地方。
这两件事做完,交棒。
阶段一 · 人只做两件事
一、第一件:把 DSH 装起来
1.1 装
DeepSeek Harness 是一个本地运行的 AI Agent Web UI,默认只监听 127.0.0.1。
bash
npx --yes @deepseek-ai/dsh web --no-open
启动后会打印一个带 token 的地址:
ini
dsh web: http://127.0.0.1:3080/?token=xxxxxxxx
这台机器只有 1.8 GiB 内存,装之前先加 swap,否则后面编译前端大概率 OOM:
bash
fallocate -l 2G /swapfile && chmod 600 /swapfile
mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
1.2 用 systemd 托管
前台跑不是长久之计。写一个 unit:
ini
# /etc/systemd/system/dsh-web.service
[Service]
Type=simple
WorkingDirectory=/root/dsh-workspace
ExecStart=/usr/bin/node .../.bin/dsh web \
--no-open \
--public-url https://chenjiaxin.me \
--trusted-host chenjiaxin.me
Restart=always
RestartSec=5
Environment=NODE_ENV=production
两个参数不能省:
--public-url:告诉它对外地址,否则生成的链接是127.0.0.1--trusted-host:把域名加入/api的信任白名单,否则浏览器请求会被拒
1.3 nginx 反代 + HTTPS
服务只监听本机回环,对外统一走 nginx:
nginx
server {
listen 443 ssl;
server_name chenjiaxin.me;
ssl_certificate /etc/letsencrypt/live/chenjiaxin.me/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/chenjiaxin.me/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3080;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
证书用 certbot 的 webroot 方式签发,不中断服务:
bash
certbot certonly --webroot -w /var/www/certbot \
-d chenjiaxin.me -d www.chenjiaxin.me \
--non-interactive --agree-tos
1.4 换一把锁:从 token 到密码
DSH 默认用一次性 token 登录,每次重启都变。
对人来说这是麻烦;对 Agent 来说这是致命------你没法让它"告诉我新 token 是多少"再继续。
好在 DSH 的 cookie 签名密钥是持久的(在 ~/.dsh/.credentials.yaml),所以做法是:做一个密码网关,验签通过后用这个密钥签一个会话 cookie。
nginx 侧只需要两件事:/__login 转发给网关,把 DSH 返回的 401 转成跳登录页。
nginx
proxy_intercept_errors on; # 关键:默认是 off,不打开拦不到上游 401
error_page 401 = @dsh_login;
location @dsh_login {
if ($http_accept ~* text/html) { return 302 /__login; }
return 401;
}
这一步的意义不只是"好登录":它把门禁从"人记 token"变成了系统可管理的状态。后面让 Agent 演示站点,直接给地址就行。
二、第二件:在阿里云给 Agent 开身份
2.1 建 RAM 用户
RAM 控制台 → 用户 → 创建用户。名字里写清用途,半年后回来看不会懵。
一个用途一个身份:ai-dns-manager 是给 Agent 用的,chejx-oss 是另一个用途。这样出了问题能精确定位是谁干的。
2.2 只授一个域名
策略的关键是 Resource 收窄到一个具体域名,而不是 *:
json
{
"Version": "1",
"Statement": [{
"Effect": "Allow",
"Action": [
"alidns:DescribeDomainRecords",
"alidns:AddDomainRecord",
"alidns:UpdateDomainRecord",
"alidns:DeleteDomainRecord",
"alidns:SetDomainRecordStatus"
],
"Resource": "acs:alidns:*:<账号ID>:domain/chenjiaxin.me"
}]
}
2.3 把 AccessKey 配进 CLI
Agent 不直接持有密钥字符串,而是通过 CLI 读配置:
bash
aliyun configure
# 依次输入 AccessKey ID / Secret / 区域 cn-beijing / 输出 json
不要把 AccessKey 写进 prompt 或者代码里。 让工具去读配置,模型永远看不到明文。
2.4 验证边界(重要)
配好之后先做一件事:验证它能做什么,更要验证它不能做什么。
bash
aliyun sts GetCallerIdentity
# → Arn: acs:ram::...:user/ai-dns-manager ✅ 确认是子账号,不是主账号
aliyun alidns DescribeDomainRecords --DomainName chenjiaxin.me
# → 200 ✅ 能查
aliyun alidns DescribeDomains
# → Forbidden.RAM ✅ 不能列全部域名
aliyun vpc DescribeVpcs
# → Forbidden.RAM ✅ 碰不到网络资源
这就是交给 Agent 时的能力边界表:
| 能力 | 状态 |
|---|---|
| 查询指定域名解析 | ✅ |
| 增删改该域名记录 | ✅ |
| 列举账号全部域名 | ❌ |
| 操作 VPC / SLB / ECS | ❌ |
阶段二 · 交棒,剩下的全给它
到这里我停手了。下面是"我说了什么"和"它做了什么"。
三、开二级域名
创建一个二级域名
my.chenjiaxin.me,指向这台服务器
它执行的步骤:
bash
# 1. 从元数据服务确认公网 IP,不靠猜
curl -s -H "Metadata-Version: 2.0" \
http://100.100.200.200/latest/meta-data/eipv4
# 2. 先查重(避免重复记录)
aliyun alidns DescribeDomainRecords \
--DomainName chenjiaxin.me --RRKeyWord my
# 3. 创建
aliyun alidns AddDomainRecord \
--DomainName chenjiaxin.me --RR my --Type A \
--Value 8.152.214.54 --TTL 600
能看出 Agent 和脚本的差别:它会先查重、再创建、最后验证解析。不需要你告诉它"要先查重"。
⚠️ 一个真实细节:改域名不是"重命名",是删除 + 新建 ------因为阿里云的 RecordId 不跟着域名走。它主动说明了这点,而不是假装改了个名字。
四、部署整个项目
把 my-agent 项目部署起来,部署后用
my.chenjiaxin.me访问
它自己判断并完成了这些:
1. 发现环境不符合文档 项目自带 docker-compose.yml,但这台机器没装 Docker,80/443 也已被占用。它没有硬套文档,改成原生部署。
2. 装 MySQL、建库、创建专用账号(不用 root)
3. 建表 ------跑项目自带的 sync-schema:
4. systemd 托管后端 ,Restart=always
5. 构建前端,产物交给 nginx 托管
6. 配 nginx 新站点 :SPA 路由 + /api 反代 + SSE 关缓冲(不关的话回答会攒着一起吐)
7. 扩容证书,把新域名加进同一张:
bash
certbot certonly --webroot -w /var/www/certbot \
--cert-name chenjiaxin.me \
-d chenjiaxin.me -d www.chenjiaxin.me -d my.chenjiaxin.me \
--expand --non-interactive --agree-tos
到这一步,两个站点同机共存,共用一张证书、一套 nginx、一份备份策略。
五、它自己发现的问题
这是我认为最有价值的一段。
某个外部服务调用一直失败,报 Connection error,重试 5 次全挂。但同样的地址,curl 是通的。
指令只有一句:
查一下为什么不通,修好
它没有停在"配置写错了"的猜测上,而是对比了不同调用方式:
curl 访问 → 200
Node 强制 IPv4 → 200
Node 默认(自动选择) → ETIMEDOUT
结论:这台 ECS 只有 link-local 地址,没有 IPv6 出口 ,而目标域名同时有 A 和 AAAA 记录。Node 的地址族自动选择会去试 IPv6 然后卡到超时,而不是快速失败回落。curl 有自己的回落逻辑,所以命令行一直正常------这就是"命令行能通、服务不行"的全部原因。
修复一行:
ini
Environment=NODE_OPTIONS=--no-network-family-autoselection
再做一步收尾,从系统层关掉 IPv6,让所有运行时都受益:
bash
# /etc/sysctl.d/99-zz-disable-ipv6.conf
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
效果是:IPv6 连接的失败时间从"数秒超时"降到 22 毫秒------从"卡住"变成"立刻放弃、回落 IPv4"。
这个排查路径不在任何文档里,也不在我部署前能预想到的清单上。
六、顺手加的保险
这些也是它做的,但值得单独说,因为让 AI 操作生产环境,光有边界不够,还要有兜底。
1. 每日备份,保留 7 天
bash
mysqldump --single-transaction --quick --routines --triggers \
--default-character-set=utf8mb4 my_agent | gzip -9 > "$FILE"
find "$BACKUP_DIR" -name 'my_agent-*.sql.gz' -mtime +6 -delete
--single-transaction 保证备份期间不锁表 。systemd timer 每天跑,Persistent=true 让关机错过的任务开机补跑。
2. 并发写加版本号
项目里"保存"接口是整份覆盖,没有版本校验。它主动加了乐观锁:
ts
if (currentRev && resume.rev !== currentRev) throw new ResumeConflictError()
resume.rev = randomUUID()
冲突时返回 409 让用户刷新,而不是静默覆盖。
这条原则值得推广 :任何"整份覆盖"的写接口都该有版本号------不是为了防恶意,是防并发和误操作,哪怕操作者是 AI。
3. 端口收口
后端默认监听 0.0.0.0:3001,等于绕过反代暴露明文 HTTP。加一条规则收死:
bash
iptables -I INPUT 1 -p tcp --dport 3001 ! -i lo -j DROP
阶段三 · 复盘
七、这套模式真实的边界
用下来几天,说几句实话。
Agent 做得比人好的
| 场景 | 原因 |
|---|---|
| "现象清楚、路径不明"的排查 | 一次读完日志、配置、进程状态,不遗漏 |
| 跨系统的操作 | nginx + systemd + MySQL + DNS,切换成本为零 |
| 重复性配置调整 | 不会因为疲劳漏掉某个 reload |
| 把过程写成记录 | 它本来就在记录每一步 |
人仍然必须管的
1. 授权范围。 RAM 策略给多大、密钥多久轮换------这个判断不能交出去。Agent 只会用你给的权限,不会替你决定该给多少。
2. 破坏性操作的确认。 删库、改安全组、动生产流量必须有人点头。这次的规则很简单:读操作随便做,写操作先说明再执行。
3. 密钥管理。 这次出过一个小事故------排查时我把一段配置打印到了终端,密钥落进了日志。后来立刻轮换了。Agent 不会主动帮你防范这种事,你得自己盯着。
4. 判断"什么该做"。 它能帮你把事做对,不能帮你决定做什么。
八、小结
这次真正有价值的不是"AI 会敲命令",而是把运维的上下文沉淀下来了:
- 权限边界写在 RAM 策略里,不写在谁的记忆里
- 部署步骤写在 systemd 和 nginx 配置里,不写在聊天记录里
- 排查过程写在提交记录里,下次遇到同类问题能捞出来
而"部署"这件事------它恰恰是最容易被替代的部分。
真正稀缺的是"发现问题、解释清楚发生了什么、然后修好",这正好是 Agent 擅长的。
想试的话,建议这样起步:
- 部署 DSH(本文第一节)
- 开一个只读 的 RAM 用户(只给
Describe*) - 让它帮你盘一遍现有资源,看它的判断靠不靠谱
- 觉得行,再逐步放开写权限
从只读到能写,这个过渡比一步到位安全得多。
环境:Alibaba Cloud Linux 3 / 2 vCPU 1.8 GiB / nginx 1.24 / MySQL 8.0 / Node 24 / DeepSeek Harness