跪拜 Guibai
← Back to the summary

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?

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:

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.me pointing 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:

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:

  1. Deploy DSH (section 1 of this article)
  2. Create a read-only RAM user (only Describe* permissions)
  3. Have it inventory your existing resources, see if its judgment is reliable
  4. 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