Two Manual Steps, Then an AI Runs Your Ops: DSH + RAM on Alibaba Cloud
Title: Alibaba Cloud Deployment of DSH and RAM for Ops Author: 函数小陈 Tags: Frontend, Backend
I Did Only Two Things: Deploy DSH and Open RAM. Everything Else I Left to It
This Alibaba Cloud ECS now runs two independent sites, two domains, an auto-renewing certificate, and a daily backup set.
Only two things were done by hand: installing DeepSeek Harness and giving it a RAM identity on Alibaba Cloud. Everything else — creating databases, deploying projects, opening subdomains, configuring certificates, troubleshooting failures — I did with "a single sentence".
On the left is DSH itself, on the right is the project it deployed. Same machine: 2 vCPU / 1.8 GiB / 40 GiB.
0. First, Clarify the Division of Labor
| Phase | Who Does It | Content |
|---|---|---|
| Preparation | 🧑 Human | Deploy DSH, open RAM |
| Execution | 🤖 DSH | Create databases, deploy projects, open subdomains, expand certificates, troubleshoot failures, add backups |
| Decision | 🧑 Human | Authorization scope, confirm destructive operations, rotate keys |
Why must these two things be done by humans?
- Deploying DSH: The chicken-and-egg problem. The agent isn't running yet, so a human has to install it.
- Opening RAM: Authorization cannot grant itself. This is the only place where "a higher-privilege human must give consent".
After these two, the baton is passed.
Phase 1 · Humans Only Do Two Things
1. First: Install DSH
1.1 Installation
DeepSeek Harness is a locally running AI Agent Web UI that listens only on 127.0.0.1 by default.
npx --yes @deepseek-ai/dsh web --no-open
After startup, it prints an address with a token:
dsh web: http://127.0.0.1:3080/?token=xxxxxxxx
This machine has only 1.8 GiB of memory. Add swap before installing, otherwise the frontend compilation will likely OOM:
fallocate -l 2G /swapfile && chmod 600 /swapfile
mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
1.2 Manage with systemd
Running in the foreground is not a long-term solution. Write a unit:
# /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
Two parameters cannot be omitted:
--public-url: tells it the external address, otherwise generated links use127.0.0.1--trusted-host: adds the domain to the/apitrust whitelist, otherwise browser requests are rejected
1.3 nginx Reverse Proxy + HTTPS
The service only listens on the loopback interface; all external traffic goes through 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;
}
}
Use certbot's webroot method to issue the certificate without interrupting service:
certbot certonly --webroot -w /var/www/certbot \
-d chenjiaxin.me -d www.chenjiaxin.me \
--non-interactive --agree-tos
1.4 Change the Lock: From Token to Password
DSH uses a one-time token for login by default, changing on every restart.
For a human, that's a hassle; for an Agent, it's fatal — you can't tell it "tell me the new token" and continue.
Fortunately, DSH's cookie signing key is persistent (in ~/.dsh/.credentials.yaml), so the approach is: create a password gateway, verify the password, then use that key to sign a session cookie.
The nginx side needs only two things: forward /__login to the gateway, and convert DSH's 401 responses into a redirect to the login page.
proxy_intercept_errors on; # Key: default is off; if you don't turn it on, upstream 401 won't be intercepted
error_page 401 = @dsh_login;
location @dsh_login {
if ($http_accept ~* text/html) { return 302 /__login; }
return 401;
}
The meaning of this step is not just "easier login": it turns the gate from "a human remembering a token" into a system-managed state. Later, when the Agent demonstrates a site, you just hand over the address.
2. Second: Give the Agent an Identity on Alibaba Cloud
2.1 Create a RAM User
RAM Console → Users → Create User. Write a clear purpose into the name; six months later you won't be confused.
One identity per purpose: ai-dns-manager is for the Agent, chejx-oss is for another purpose. That way, when something goes wrong, you can pinpoint who did it.
2.2 Grant Only One Domain
The key in the policy is narrowing Resource to a specific domain, not *:
{
"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 Configure the AccessKey into the CLI
The Agent doesn't directly hold the key string; it reads the configuration through the CLI:
aliyun configure
# Enter AccessKey ID / Secret / region cn-beijing / output type json
Do not write the AccessKey into prompts or code. Let the tool read the config; the model never sees the plaintext.
2.4 Verify the Boundary (Important)
After configuration, do one thing first: verify what it can do, and more importantly, verify what it cannot do.
aliyun sts GetCallerIdentity
# → Arn: acs:ram::...:user/ai-dns-manager ✅ Confirms it's a sub-account, not the root account
aliyun alidns DescribeDomainRecords --DomainName chenjiaxin.me
# → 200 ✅ Can query
aliyun alidns DescribeDomains
# → Forbidden.RAM ✅ Cannot list all domains
aliyun vpc DescribeVpcs
# → Forbidden.RAM ✅ Cannot touch network resources
This is the capability boundary table handed to the Agent:
| Capability | Status |
|---|---|
| Query resolution records for the specified domain | ✅ |
| Add/delete/modify records on that domain | ✅ |
| List all domains on the account | ❌ |
| Operate VPC / SLB / ECS | ❌ |
Phase 2 · Pass the Baton, Leave the Rest to It
At this point I stopped. Below is "what I said" and "what it did".
3. Open a Subdomain
Create a subdomain
my.chenjiaxin.mepointing to this server
The steps it executed:
# 1. Get the public IP from the metadata service, not guess
curl -s -H "Metadata-Version: 2.0" \
http://100.100.200.200/latest/meta-data/eipv4
# 2. Check for duplicates first (avoid duplicate records)
aliyun alidns DescribeDomainRecords \
--DomainName chenjiaxin.me --RRKeyWord my
# 3. Create
aliyun alidns AddDomainRecord \
--DomainName chenjiaxin.me --RR my --Type A \
--Value 8.152.214.54 --TTL 600
You can see the difference between an Agent and a script: it checks duplicates, then creates, then verifies resolution. You don't need to tell it "check duplicates first".
⚠️ A real detail: changing a domain isn't "renaming", it's delete + create — because Alibaba Cloud's RecordId doesn't follow the domain name. It proactively explained this instead of pretending it renamed the record.
4. Deploy a Whole Project
Deploy the my-agent project, then access it via
my.chenjiaxin.me
It judged and completed all of the following on its own:
1. Discovered the environment didn't match the docs.
The project came with a docker-compose.yml, but this machine has no Docker, and ports 80/443 were already occupied. It didn't force the docs; it switched to a native deployment.
2. Installed MySQL, created databases, created a dedicated account (not root)
3. Created tables — ran the project's own sync-schema:
4. Managed the backend with systemd, Restart=always
5. Built the frontend, handed the output to nginx
6. Configured nginx for the new site: SPA routing + /api reverse proxy + SSE buffering off (without that, responses would be held and flushed all at once)
7. Expanded the certificate, adding the new domain to the same cert:
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
At this point, two sites coexist on the same machine, sharing one certificate, one nginx setup, and one backup strategy.
5. A Problem It Found on Its Own
This is the part I consider most valuable.
A call to some external service kept failing, reporting Connection error, retried 5 times, all failed. But the same address, curl, worked.
The instruction was a single sentence:
Figure out why it doesn't work, then fix it
It didn't stop at a guess of "configuration is wrong"; instead it compared different calling methods:
curl access → 200
Node forced IPv4 → 200
Node default (auto-select) → ETIMEDOUT
Conclusion: this ECS has only a link-local address, no IPv6 egress, and the target domain has both A and AAAA records. Node's automatic address family selection tries IPv6 and hangs until timeout, instead of failing fast and falling back. curl has its own fallback logic, so the command line always worked — that's the entire reason "command line works, service doesn't".
One line fixed it:
Environment=NODE_OPTIONS=--no-network-family-autoselection
Then one more cleanup step, disabling IPv6 at the system level so all runtimes benefit:
# /etc/sysctl.d/99-zz-disable-ipv6.conf
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
Effect: IPv6 connection failure time dropped from "several-second timeout" to 22 milliseconds — from "stuck" to "immediately give up, fall back to IPv4".
This troubleshooting path is not in any documentation, and it wasn't on a checklist I could have predicted before deployment.
6. Insurance Added Along the Way
These are also done by it, but worth calling out separately, because letting an AI operate production requires not just boundaries, but also safety nets.
1. Daily backups, kept 7 days
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 guarantees no table locks during backup. A systemd timer runs it daily, with Persistent=true so a missed job catches up after a reboot.
2. Concurrency writes got a version number
The project's "save" endpoint did a full overwrite, with no version check. It proactively added optimistic locking:
if (currentRev && resume.rev !== currentRev) throw new ResumeConflictError()
resume.rev = randomUUID()
On conflict, it returns 409 to let the user refresh, instead of silently overwriting.
This principle is worth generalizing: any "full overwrite" write endpoint should have a version number — not to defend against malicious intent, but against concurrency and misoperation, even when the operator is an AI.
3. Narrowed the port exposure
The backend listened on 0.0.0.0:3001 by default, which bypasses the reverse proxy and exposes plaintext HTTP. Added one rule to seal it:
iptables -I INPUT 1 -p tcp --dport 3001 ! -i lo -j DROP
Phase 3 · Review
7. Real Boundaries of This Model
After using it for a few days, some honest words.
Where the Agent is Better Than a Human
| Scenario | Reason |
|---|---|
| Troubleshooting where the symptom is clear but the path isn't | Reads logs, config, process state at once, doesn't miss pieces |
| Cross-system operations | nginx + systemd + MySQL + DNS, zero switching cost |
| Repetitive configuration adjustments | Doesn't skip a reload because of fatigue |
| Writing records of the process | It logs every step anyway |
What a Human Must Still Manage
1. Authorization scope. How much the RAM policy grants, how often to rotate keys — that judgment can't be handed over. The Agent only uses the permissions you give it; it won't decide for you how much to give.
2. Confirmation of destructive operations. Dropping databases, changing security groups, touching production traffic must have human consent. The rule this time was simple: read operations are free, write operations must be explained before execution.
3. Key management. A small incident happened: during troubleshooting, I printed a config to the terminal and a key leaked into the logs. I rotated it immediately. The Agent won't proactively protect you from this; you have to watch it yourself.
4. Deciding "what should be done". It can help you do things right, but it can't decide what to do.
8. Summary
What's truly valuable this time isn't "AI can type commands"; it's the way the ops context got persisted:
- Permission boundaries are written in the RAM policy, not in someone's memory
- Deployment steps are written in the systemd and nginx configs, not in a chat log
- Troubleshooting steps are written in the commit history, available next time the same problem appears
And "deployment" itself — it's precisely the part that's easiest to replace.
What's truly scarce is "finding the problem, explaining clearly what happened, then fixing it", and that's exactly what the Agent is good at.
If you want to try it, here's a suggested starting point:
- Deploy DSH (section 1 of this article)
- Create a read-only RAM user (only
Describe*permissions) - Have it inventory your existing resources, see if its judgment is reliable
- If it looks good, gradually grant write permissions
The transition from read-only to write is far safer than going all-in at once.
Environment: Alibaba Cloud Linux 3 / 2 vCPU 1.8 GiB / nginx 1.24 / MySQL 8.0 / Node 24 / DeepSeek Harness