<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kenneth Phang</title>
    <description>The latest articles on DEV Community by Kenneth Phang (@kenneth_phang_fb8bb559a6f).</description>
    <link>https://dev.to/kenneth_phang_fb8bb559a6f</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1599109%2Fedf044a0-9e24-40e6-bda8-216780f6a227.jpg</url>
      <title>DEV Community: Kenneth Phang</title>
      <link>https://dev.to/kenneth_phang_fb8bb559a6f</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kenneth_phang_fb8bb559a6f"/>
    <language>en</language>
    <item>
      <title>Should You Run Your AI Assistant Inside Docker? I Researched It So You Do Not Have To</title>
      <dc:creator>Kenneth Phang</dc:creator>
      <pubDate>Thu, 05 Mar 2026 04:47:56 +0000</pubDate>
      <link>https://dev.to/kenneth_phang_fb8bb559a6f/should-you-run-your-ai-assistant-inside-docker-i-researched-it-so-you-do-not-have-to-1ep4</link>
      <guid>https://dev.to/kenneth_phang_fb8bb559a6f/should-you-run-your-ai-assistant-inside-docker-i-researched-it-so-you-do-not-have-to-1ep4</guid>
      <description>&lt;p&gt;I run &lt;a href="https://openclaw.ai" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt; — an open-source AI assistant that connects to WhatsApp, Telegram, and Discord with full system access (shell, files, browser, voice). It currently runs directly on the host with Docker sandbox containers for tool isolation.&lt;/p&gt;

&lt;p&gt;I spent a week researching whether to move the &lt;strong&gt;entire gateway&lt;/strong&gt; into Docker. Here is what I found — and why I decided not to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Options
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option A: Host-Based (Current)
&lt;/h3&gt;

&lt;p&gt;Gateway runs on the host as a systemd service. Sandbox containers handle tool execution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────┐
│              HOST                     │
│  ┌────────────────────────────────┐  │
│  │  OpenClaw Gateway (systemd)   │  │
│  │  - WhatsApp connection        │  │
│  │  - Telegram bot               │  │
│  │  - API keys in ~/.openclaw    │  │
│  └──────────┬─────────────────────┘  │
│             │ Docker socket          │
│             ▼                        │
│  ┌────────────────────────────────┐  │
│  │  Sandbox Containers           │  │
│  │  - Tool execution only        │  │
│  │  - Filesystem isolation       │  │
│  └────────────────────────────────┘  │
└──────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option B: Full Docker
&lt;/h3&gt;

&lt;p&gt;Everything in Docker — gateway, tools, sandbox.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option C: Hybrid
&lt;/h3&gt;

&lt;p&gt;Gateway in Docker, sandbox as sibling containers via Docker socket.&lt;/p&gt;

&lt;p&gt;Option C sounded like the sweet spot. &lt;strong&gt;It is not.&lt;/strong&gt; Here is why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Docker Socket Paradox
&lt;/h2&gt;

&lt;p&gt;This is the core problem that killed Option C for me.&lt;/p&gt;

&lt;p&gt;OpenClaw needs to spawn Docker containers for sandboxed tool execution. If the gateway is inside Docker, it needs access to the Docker daemon. The standard approach: mount &lt;code&gt;/var/run/docker.sock&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But mounting the Docker socket gives the container root-equivalent access to the entire host.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An attacker (or a rogue AI prompt injection) could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a privileged container mounting &lt;code&gt;/&lt;/code&gt; (host root filesystem)&lt;/li&gt;
&lt;li&gt;Read &lt;code&gt;/etc/shadow&lt;/code&gt;, SSH keys, every API key on the server&lt;/li&gt;
&lt;li&gt;Spawn cryptomining containers&lt;/li&gt;
&lt;li&gt;Delete all containers, volumes, and images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even a read-only mount does not help — the Docker API is still fully writable through the socket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Goal:     Isolate gateway from host for security
Requires: Docker socket to spawn sandbox containers
Result:   Docker socket gives root access to host
Net:      Security improvement = 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You containerize for security, then hand back all the access via the socket. It is like locking your front door but leaving the key under the mat.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mitigations Exist But Add Complexity
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Docker socket proxy&lt;/strong&gt; restricts which API endpoints are accessible. But it adds another container to manage, must be carefully configured, and still allows container creation (which is the attack vector).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sysbox runtime&lt;/strong&gt; allows nested containers without socket mount or privileged mode. But it requires host-level installation and is not widely supported.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Podman&lt;/strong&gt; is daemonless and rootless. But OpenClaw sandbox currently assumes Docker.&lt;/p&gt;

&lt;h2&gt;
  
  
  WhatsApp Pairing Breaks in Docker
&lt;/h2&gt;

&lt;p&gt;OpenClaw uses the Baileys library (WhatsApp Web protocol). Pairing requires a QR code scan from your phone. Inside Docker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Session data must be volume-mounted. If the volume mapping breaks on restart, you re-pair.&lt;/li&gt;
&lt;li&gt;Docker NAT layer adds latency to WebSocket connections.&lt;/li&gt;
&lt;li&gt;I already see frequent gateway disconnects (HTTP 499, 428, 503) on the host. Docker networking would make this &lt;strong&gt;worse&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Container restart without proper volume persistence = lost WhatsApp session = manual re-pair.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Workarounds exist (noVNC for QR display, phone number OTP pairing) but they add fragility to something that currently just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Networking Gets Complicated Fast
&lt;/h2&gt;

&lt;p&gt;The gateway needs outbound access to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WhatsApp servers (WebSocket)&lt;/li&gt;
&lt;li&gt;Telegram API&lt;/li&gt;
&lt;li&gt;Anthropic, OpenAI, Google APIs&lt;/li&gt;
&lt;li&gt;npm registry&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Health check endpoint on port 18789&lt;/li&gt;
&lt;li&gt;Inter-container communication with sandbox containers&lt;/li&gt;
&lt;li&gt;DNS resolution (flaky inside Docker)&lt;/li&gt;
&lt;li&gt;Tailscale VPN needs &lt;code&gt;--cap-add NET_ADMIN&lt;/code&gt; and &lt;code&gt;/dev/net/tun&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every layer of Docker networking adds 1-2ms latency and a potential failure point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persistent State is a Minefield
&lt;/h2&gt;

&lt;p&gt;Everything in &lt;code&gt;~/.openclaw/&lt;/code&gt; needs volume mounts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config:/home/openclaw/.openclaw&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./workspace:/home/openclaw/.openclaw/workspace&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./media:/home/openclaw/.openclaw/media&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Risks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UID/GID mismatches&lt;/strong&gt; between host user and container user&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume corruption&lt;/strong&gt; if the container crashes mid-write&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backup complexity&lt;/strong&gt; increases (Docker volumes vs simple &lt;code&gt;tar&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File ownership conflicts&lt;/strong&gt; when accessing workspace from both host and container&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media files&lt;/strong&gt; (voice messages, images) need careful permission handling&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Debugging Gets Painful
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Host&lt;/th&gt;
&lt;th&gt;Docker&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;View logs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;journalctl --user -u openclaw-gateway&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker logs openclaw-gateway&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restart&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl --user restart openclaw-gateway&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker restart openclaw-gateway&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shell&lt;/td&gt;
&lt;td&gt;Direct SSH&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker exec -it openclaw-gateway bash&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edit config&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vim ~/.openclaw/openclaw.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Edit volume or exec in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Install plugin&lt;/td&gt;
&lt;td&gt;&lt;code&gt;openclaw plugins install ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exec in + restart container&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You lose systemd watchdog, journald integration, and the ability to just SSH in and fix things.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sandbox-in-Sandbox Problem
&lt;/h2&gt;

&lt;p&gt;OpenClaw spawns Docker containers for tool isolation. If the gateway is also in Docker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker-in-Docker (DinD):&lt;/strong&gt; Needs &lt;code&gt;--privileged&lt;/code&gt; flag, which defeats all security benefits. Storage driver conflicts (overlay2 inside overlay2). Known stability issues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sibling containers via socket:&lt;/strong&gt; Returns to the Docker socket paradox (root access).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sysbox:&lt;/strong&gt; True nested containers without privilege escalation. But limited platform support and not tested with OpenClaw.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance Overhead (Minor But Real)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Container startup: ~1-3s overhead per sandbox spawn&lt;/li&gt;
&lt;li&gt;Network NAT: ~1-2ms per request&lt;/li&gt;
&lt;li&gt;Memory: ~50-100MB for container runtime&lt;/li&gt;
&lt;li&gt;Disk I/O through overlay2: 5-15% slower for writes&lt;/li&gt;
&lt;li&gt;TTS audio processing and browser screenshots slightly slower&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  It Would Require a Major Rewrite
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://github.com/kenken64/clawmacdo" rel="noopener noreferrer"&gt;clawmacdo&lt;/a&gt; — a Rust CLI that deploys OpenClaw to DigitalOcean and Tencent Cloud via 16 SSH-based provisioning steps. Moving to Docker means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace Steps 9-15 with &lt;code&gt;docker compose up&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Build and publish a gateway Docker image&lt;/li&gt;
&lt;li&gt;Rewrite backup/restore for Docker volumes&lt;/li&gt;
&lt;li&gt;Change Web UI progress streaming from SSH to Docker log tailing&lt;/li&gt;
&lt;li&gt;Re-test the Tencent Cloud integration I just finished&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Estimated effort: 2-3 weeks. For questionable security improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Scorecard
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;Mitigatable?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Docker socket = root access&lt;/td&gt;
&lt;td&gt;CRITICAL&lt;/td&gt;
&lt;td&gt;Partial (proxy)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WhatsApp pairing breaks&lt;/td&gt;
&lt;td&gt;HIGH&lt;/td&gt;
&lt;td&gt;Yes (fragile)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sandbox-in-sandbox&lt;/td&gt;
&lt;td&gt;HIGH&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;clawmacdo rewrite&lt;/td&gt;
&lt;td&gt;HIGH (effort)&lt;/td&gt;
&lt;td&gt;Yes but expensive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Networking complexity&lt;/td&gt;
&lt;td&gt;MEDIUM-HIGH&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State management&lt;/td&gt;
&lt;td&gt;MEDIUM&lt;/td&gt;
&lt;td&gt;Yes (volumes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debugging harder&lt;/td&gt;
&lt;td&gt;MEDIUM&lt;/td&gt;
&lt;td&gt;Yes (tooling)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance overhead&lt;/td&gt;
&lt;td&gt;LOW&lt;/td&gt;
&lt;td&gt;Negligible&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  My Decision: Stay on Host
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short-term:&lt;/strong&gt; Keep the host-based architecture. Harden with allowlists, workspace-only file access, and group policies. This provides better security ROI than containerization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Medium-term:&lt;/strong&gt; Watch for OpenClaw Podman support (rootless sandboxing) and Sysbox maturity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long-term:&lt;/strong&gt; Revisit when rootless container runtimes are natively supported and WhatsApp Business API replaces QR-based pairing.&lt;/p&gt;

&lt;p&gt;The right answer is not always the most technically sophisticated one. Sometimes keeping it simple is the engineering decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Research Doc
&lt;/h2&gt;

&lt;p&gt;The complete analysis with architecture diagrams is on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/kenken64/clawmacdo/blob/tencent/docs/DEPLOYMENT_ARCHITECTURE_RESEARCH.md" rel="noopener noreferrer"&gt;DEPLOYMENT_ARCHITECTURE_RESEARCH.md&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And the CLI itself:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/kenken64/clawmacdo" rel="noopener noreferrer"&gt;github.com/kenken64/clawmacdo&lt;/a&gt;&lt;/strong&gt; — star it if you found this useful!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The best deployment architecture is the one where adding Docker does not accidentally give root access to the thing you are trying to isolate.&lt;/em&gt; 🦞🔒&lt;/p&gt;

</description>
      <category>docker</category>
      <category>security</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>How I Added Multi-Cloud Support to My Rust CLI — DigitalOcean + Tencent Cloud in One Tool</title>
      <dc:creator>Kenneth Phang</dc:creator>
      <pubDate>Thu, 05 Mar 2026 04:37:06 +0000</pubDate>
      <link>https://dev.to/kenneth_phang_fb8bb559a6f/how-i-added-multi-cloud-support-to-my-rust-cli-digitalocean-tencent-cloud-in-one-tool-1c9k</link>
      <guid>https://dev.to/kenneth_phang_fb8bb559a6f/how-i-added-multi-cloud-support-to-my-rust-cli-digitalocean-tencent-cloud-in-one-tool-1c9k</guid>
      <description>&lt;p&gt;My open-source Rust CLI, &lt;strong&gt;&lt;a href="https://github.com/kenken64/clawmacdo" rel="noopener noreferrer"&gt;clawmacdo&lt;/a&gt;&lt;/strong&gt;, originally only supported DigitalOcean. Last week, I added full &lt;strong&gt;Tencent Cloud&lt;/strong&gt; support — and I want to share how I designed the multi-cloud architecture, implemented TC3-HMAC-SHA256 signing in Rust, and kept the provisioning pipeline provider-agnostic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;

&lt;p&gt;One CLI that deploys an AI assistant stack to &lt;strong&gt;any&lt;/strong&gt; cloud provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# DigitalOcean (existing)&lt;/span&gt;
clawmacdo deploy &lt;span class="nt"&gt;--provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;digitalocean &lt;span class="nt"&gt;--do-token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xxx

&lt;span class="c"&gt;# Tencent Cloud (new!)&lt;/span&gt;
clawmacdo deploy &lt;span class="nt"&gt;--provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tencent &lt;span class="nt"&gt;--tencent-secret-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xxx &lt;span class="nt"&gt;--tencent-secret-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same 16-step automated deployment. Same result. Different cloud.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture: CloudProvider Trait
&lt;/h2&gt;

&lt;p&gt;The key insight was that &lt;strong&gt;only the infrastructure layer changes per provider&lt;/strong&gt;. The SSH provisioning pipeline (install Node.js, configure OpenClaw, start the gateway) is identical — it just needs an IP address to SSH into.&lt;/p&gt;

&lt;p&gt;So I created a &lt;code&gt;CloudProvider&lt;/code&gt; trait:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[async_trait]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;trait&lt;/span&gt; &lt;span class="n"&gt;CloudProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Send&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Sync&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;upload_ssh_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;KeyInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;delete_ssh_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;create_instance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CreateInstanceParams&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InstanceInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;wait_for_active&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InstanceInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;delete_instance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;list_instances&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InstanceInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;DoClient&lt;/code&gt; and &lt;code&gt;TencentClient&lt;/code&gt; implement this trait. The deploy command just dispatches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DeployParams&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DeployRecord&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nf"&gt;resolve_provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="py"&gt;.provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;CloudProviderType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;DigitalOcean&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;run_do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nn"&gt;CloudProviderType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Tencent&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;run_tencent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Steps 1-4 are provider-specific (create keys, create instance). Steps 5-16 are shared (SSH in, provision, start gateway). Clean separation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing TC3-HMAC-SHA256 in Rust
&lt;/h2&gt;

&lt;p&gt;Tencent Cloud uses a custom request signing scheme called &lt;strong&gt;TC3-HMAC-SHA256&lt;/strong&gt;. Every API request must be signed with a 4-layer HMAC chain. Here is how it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Date Key:    HMAC-SHA256("TC3" + SecretKey, date)
Service Key: HMAC-SHA256(dateKey, service)
Signing Key: HMAC-SHA256(serviceKey, "tc3_request")
Signature:   HMAC-SHA256(signingKey, stringToSign)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Rust, using the &lt;code&gt;hmac&lt;/code&gt; and &lt;code&gt;sha2&lt;/code&gt; crates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Hmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Mac&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;sha2&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Sha256&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;HmacSha256&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Hmac&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Sha256&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;hmac_sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;mac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;HmacSha256&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_from_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;mac&lt;/span&gt;&lt;span class="nf"&gt;.update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;mac&lt;/span&gt;&lt;span class="nf"&gt;.finalize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.into_bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.to_vec&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Build the 4-layer signing key&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;secret_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmac_sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TC3{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.as_bytes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="nf"&gt;.as_bytes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;secret_service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmac_sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;secret_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="nf"&gt;.as_bytes&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;secret_signing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmac_sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;secret_service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;b"tc3_request"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;hmac_sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;secret_signing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string_to_sign&lt;/span&gt;&lt;span class="nf"&gt;.as_bytes&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Authorization&lt;/code&gt; header then looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TC3-HMAC-SHA256 Credential=AKIDxxx/2026-03-05/cvm/tc3_request,
SignedHeaders=content-type;host, Signature=abc123...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was the trickiest part — getting the canonical request format exactly right. One extra newline or wrong header order and the signature fails silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tencent Cloud API Surface
&lt;/h2&gt;

&lt;p&gt;I implemented these Tencent Cloud APIs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CVM (Cloud Virtual Machine):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;RunInstances&lt;/code&gt; — Create instances with cloud-init, SSH keys, tags&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DescribeInstances&lt;/code&gt; — Poll status, get public IP, list by tag&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TerminateInstances&lt;/code&gt; — Destroy instances&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;KeyPair:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ImportKeyPair&lt;/code&gt; — Upload SSH public key&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DeleteKeyPairs&lt;/code&gt; — Cleanup&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DescribeKeyPairs&lt;/code&gt; — List for destroy cleanup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;VPC (Security Groups):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CreateSecurityGroup&lt;/code&gt; — Firewall rules&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CreateSecurityGroupPolicies&lt;/code&gt; — SSH (22) + HTTP (80) + HTTPS (443) ingress&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DeleteSecurityGroup&lt;/code&gt; — Cleanup on destroy&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Resource Mapping
&lt;/h2&gt;

&lt;p&gt;Here is how DigitalOcean and Tencent Cloud concepts map:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;DigitalOcean&lt;/th&gt;
&lt;th&gt;Tencent Cloud&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compute&lt;/td&gt;
&lt;td&gt;Droplet&lt;/td&gt;
&lt;td&gt;CVM Instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Bearer token&lt;/td&gt;
&lt;td&gt;TC3-HMAC-SHA256&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region&lt;/td&gt;
&lt;td&gt;sgp1&lt;/td&gt;
&lt;td&gt;ap-singapore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Size&lt;/td&gt;
&lt;td&gt;s-2vcpu-4gb&lt;/td&gt;
&lt;td&gt;S5.MEDIUM4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSH Keys&lt;/td&gt;
&lt;td&gt;Account SSH Keys&lt;/td&gt;
&lt;td&gt;KeyPair API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firewall&lt;/td&gt;
&lt;td&gt;Cloud Firewall&lt;/td&gt;
&lt;td&gt;Security Groups (VPC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tags&lt;/td&gt;
&lt;td&gt;Droplet tags&lt;/td&gt;
&lt;td&gt;Instance tags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User data&lt;/td&gt;
&lt;td&gt;cloud-init&lt;/td&gt;
&lt;td&gt;cloud-init (base64)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image&lt;/td&gt;
&lt;td&gt;ubuntu-24-04-x64&lt;/td&gt;
&lt;td&gt;img-487zeit5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One gotcha: Tencent Cloud requires user data to be &lt;strong&gt;base64-encoded&lt;/strong&gt;, while DigitalOcean accepts it as plain text. Small difference, easy to miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Web UI: Dynamic Provider Switching
&lt;/h2&gt;

&lt;p&gt;The web UI (built with Axum + SSE) now has a provider dropdown that dynamically swaps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Credential fields&lt;/strong&gt; — DO Token vs Tencent SecretId/SecretKey&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Region options&lt;/strong&gt; — sgp1/nyc1/lon1 vs ap-singapore/ap-hongkong/ap-tokyo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instance sizes&lt;/strong&gt; — s-2vcpu-4gb vs S5.MEDIUM4&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All powered by a single &lt;code&gt;toggleProvider()&lt;/code&gt; JavaScript function that rewrites the form fields on change. The SSE progress streaming works identically for both providers.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLI Changes
&lt;/h2&gt;

&lt;p&gt;Every command now accepts &lt;code&gt;--provider&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Deploy&lt;/span&gt;
clawmacdo deploy &lt;span class="nt"&gt;--provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tencent &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--tencent-secret-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AKIDxxx &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--tencent-secret-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xxx &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--anthropic-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-ant-xxx

&lt;span class="c"&gt;# Status&lt;/span&gt;
clawmacdo status &lt;span class="nt"&gt;--provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tencent &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--tencent-secret-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AKIDxxx &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--tencent-secret-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xxx

&lt;span class="c"&gt;# Destroy&lt;/span&gt;
clawmacdo destroy &lt;span class="nt"&gt;--provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tencent &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--tencent-secret-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AKIDxxx &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--tencent-secret-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xxx &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;openclaw-abc12345

&lt;span class="c"&gt;# Migrate (cross-cloud supported!)&lt;/span&gt;
clawmacdo migrate &lt;span class="nt"&gt;--provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tencent &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--source-ip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.2.3.4 &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--source-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/.ssh/id_ed25519 &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--tencent-secret-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AKIDxxx &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--tencent-secret-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Backward compatible — &lt;code&gt;--provider&lt;/code&gt; defaults to &lt;code&gt;digitalocean&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trait-based abstraction pays off.&lt;/strong&gt; Adding a new provider was mostly about implementing the trait — the deploy pipeline barely changed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;TC3 signing is fiddly.&lt;/strong&gt; The canonical request must have headers in exact alphabetical order, with exact newlines. Debug by comparing your canonical request string byte-by-byte with Tencent is documentation examples.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Security groups are mandatory on Tencent.&lt;/strong&gt; Unlike DO where droplets are accessible by default, Tencent blocks all inbound traffic unless you create a security group. Forgetting this means SSH will time out and you will have no idea why.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Base64 user data.&lt;/strong&gt; Tencent requires base64-encoded cloud-init. DO takes raw text. Easy to miss, hard to debug (cloud-init silently fails).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The provisioning pipeline is the real value.&lt;/strong&gt; 80% of the deploy work (install Node, configure OpenClaw, set up systemd, start gateway) happens over SSH and is completely provider-agnostic. The cloud API is just the bootstrap.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What is Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS support&lt;/strong&gt; — EC2 + SigV4 signing (same trait pattern)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hetzner Cloud&lt;/strong&gt; — Popular in Europe, simple API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live testing on Tencent&lt;/strong&gt; — Waiting for account approval&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker-based deployment option&lt;/strong&gt; — &lt;a href="https://github.com/kenken64/clawmacdo/blob/tencent/docs/DEPLOYMENT_ARCHITECTURE_RESEARCH.md" rel="noopener noreferrer"&gt;Research complete&lt;/a&gt;, staying host-based for now&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;The tencent branch is live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/kenken64/clawmacdo.git
&lt;span class="nb"&gt;cd &lt;/span&gt;clawmacdo
git checkout tencent
cargo build &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or grab a binary from &lt;a href="https://github.com/kenken64/clawmacdo/releases" rel="noopener noreferrer"&gt;Releases&lt;/a&gt; (main branch, tencent coming soon).&lt;/p&gt;

&lt;p&gt;Star the repo if this is useful: &lt;strong&gt;&lt;a href="https://github.com/kenken64/clawmacdo" rel="noopener noreferrer"&gt;github.com/kenken64/clawmacdo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The best multi-cloud tool is the one where adding a new provider is just implementing a trait.&lt;/em&gt; 🦞🦀&lt;/p&gt;

</description>
      <category>rust</category>
      <category>cloud</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Reverse-Engineered My Own Rust CLI — Here is the Full Architecture of a 16-Step Cloud Deployer</title>
      <dc:creator>Kenneth Phang</dc:creator>
      <pubDate>Wed, 04 Mar 2026 07:35:09 +0000</pubDate>
      <link>https://dev.to/kenneth_phang_fb8bb559a6f/i-reverse-engineered-my-own-rust-cli-here-is-the-full-architecture-of-a-16-step-cloud-deployer-ji3</link>
      <guid>https://dev.to/kenneth_phang_fb8bb559a6f/i-reverse-engineered-my-own-rust-cli-here-is-the-full-architecture-of-a-16-step-cloud-deployer-ji3</guid>
      <description>&lt;p&gt;Most deployment tools are black boxes. You run a command, cross your fingers, and hope it works.&lt;/p&gt;

&lt;p&gt;I wanted something different. I built &lt;strong&gt;&lt;a href="https://github.com/kenken64/clawmacdo" rel="noopener noreferrer"&gt;clawmacdo&lt;/a&gt;&lt;/strong&gt; — a Rust CLI that deploys a full AI assistant stack to DigitalOcean in one command. And today, I am going to crack it open and show you exactly how it works inside.&lt;/p&gt;

&lt;p&gt;This is the full architecture, data flow, and design decisions behind a real-world Rust CLI that provisions cloud servers, SSHs into them, installs software, and streams live progress to a web UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30-Second Pitch
&lt;/h2&gt;

&lt;p&gt;One command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clawmacdo deploy &lt;span class="nt"&gt;--do-token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xxx &lt;span class="nt"&gt;--anthropic-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;16 automated steps later, you have a fully configured AI assistant running on a DigitalOcean droplet with WhatsApp, Telegram, Claude, GPT, and Gemini — all wired up and ready to go.&lt;/p&gt;

&lt;p&gt;But HOW does it actually work?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture: 5 Layers
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────┐
│  Layer 1: CLI + Web UI Orchestration    │
│  (Clap CLI + Axum web server + SSE)     │
├─────────────────────────────────────────┤
│  Layer 2: Infrastructure APIs           │
│  (DigitalOcean REST + SSH/SCP)          │
├─────────────────────────────────────────┤
│  Layer 3: Provisioning Pipeline         │
│  (6 ordered modules, each idempotent)   │
├─────────────────────────────────────────┤
│  Layer 4: Config + State Persistence    │
│  (DeployRecord JSON + .env + systemd)   │
├─────────────────────────────────────────┤
│  Layer 5: Progress + Error Surfaces     │
│  (Terminal UI + SSE streaming + typed    │
│   errors with actionable diagnostics)   │
└─────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let me walk through each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: Dual Interface — CLI and Web UI
&lt;/h2&gt;

&lt;p&gt;clawmacdo works two ways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI mode&lt;/strong&gt; — for terminal lovers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clawmacdo deploy &lt;span class="nt"&gt;--do-token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xxx
clawmacdo backup
clawmacdo status
clawmacdo destroy &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-droplet
clawmacdo migrate &lt;span class="nt"&gt;--source-ip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.2.3.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Web UI mode&lt;/strong&gt; — for everyone else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clawmacdo serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This spins up an &lt;strong&gt;Axum&lt;/strong&gt; web server with a full UI. You fill in a form, hit deploy, and watch real-time progress via &lt;strong&gt;Server-Sent Events (SSE)&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser ──POST /api/deploy──▶ Axum
Browser ──GET /api/deploy/{id}/events──▶ SSE stream
                                         │
                              tokio::spawn(deploy task)
                                         │
                              SSH into droplet ──▶ provision
                                         │
                              emit progress events ──▶ Browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The web deploy runs in a &lt;strong&gt;tokio background task&lt;/strong&gt; with a progress channel. The SSE endpoint reads from that channel and streams events to the browser in real-time. Terminal markers (&lt;code&gt;DEPLOY_COMPLETE&lt;/code&gt; / &lt;code&gt;DEPLOY_ERROR&lt;/code&gt;) signal the end state.&lt;/p&gt;

&lt;p&gt;This pattern — async task + channel + SSE — is surprisingly clean in Rust with tokio + Axum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: Infrastructure as Code (Without the YAML)
&lt;/h2&gt;

&lt;p&gt;No Terraform. No Pulumi. No CloudFormation. Just direct API calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;DoClient&lt;/code&gt;&lt;/strong&gt; wraps the DigitalOcean REST API via &lt;code&gt;reqwest&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create/delete droplets&lt;/li&gt;
&lt;li&gt;Manage SSH keys&lt;/li&gt;
&lt;li&gt;Poll for droplet status&lt;/li&gt;
&lt;li&gt;Tag-based resource tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SSH utilities&lt;/strong&gt; use &lt;code&gt;ssh2&lt;/code&gt; (libssh2 bindings):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Key generation (Ed25519)&lt;/li&gt;
&lt;li&gt;Remote command execution&lt;/li&gt;
&lt;li&gt;SCP file transfers&lt;/li&gt;
&lt;li&gt;Readiness polling (wait for cloud-init + SSH)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why no Terraform? Because clawmacdo needs to &lt;strong&gt;react&lt;/strong&gt; to each step. It is not declaring desired state — it is orchestrating a sequence where each step depends on the previous one. SSH readiness polling, cloud-init sentinel files, health checks — these are imperative, not declarative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: The Provisioning Pipeline (The Heart)
&lt;/h2&gt;

&lt;p&gt;This is where the magic happens. Six ordered modules, each handling one concern:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. User Provisioning
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Creates a dedicated &lt;code&gt;openclaw&lt;/code&gt; system user&lt;/li&gt;
&lt;li&gt;Sets up shell environment and sudo scope&lt;/li&gt;
&lt;li&gt;Enables systemd lingering (so user services survive logout)&lt;/li&gt;
&lt;li&gt;Migrates restored backup from root to the openclaw user&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Firewall Hardening
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Configures fail2ban for brute-force protection&lt;/li&gt;
&lt;li&gt;Sets up unattended-upgrades for automatic security patches&lt;/li&gt;
&lt;li&gt;Hardens UFW rules&lt;/li&gt;
&lt;li&gt;Adds DOCKER-USER iptables isolation rules&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Docker Setup
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Writes optimized &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Adds openclaw user to docker group&lt;/li&gt;
&lt;li&gt;Restarts Docker daemon&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Node.js + AI CLIs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Configures pnpm global directories&lt;/li&gt;
&lt;li&gt;Installs &lt;strong&gt;Claude Code&lt;/strong&gt; (Anthropic), &lt;strong&gt;Codex&lt;/strong&gt; (OpenAI), &lt;strong&gt;Gemini CLI&lt;/strong&gt; (Google)&lt;/li&gt;
&lt;li&gt;Verifies and symlinks all binaries&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. OpenClaw Installation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Creates &lt;code&gt;.openclaw&lt;/code&gt; directory structure&lt;/li&gt;
&lt;li&gt;Writes &lt;code&gt;.env&lt;/code&gt; with all API keys and messaging config&lt;/li&gt;
&lt;li&gt;Installs OpenClaw globally via npm&lt;/li&gt;
&lt;li&gt;Normalizes extension hardlinks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. Tailscale (Optional)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Installs Tailscale for private networking&lt;/li&gt;
&lt;li&gt;Runs &lt;code&gt;tailscale up&lt;/code&gt; with auth key if provided&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each module is &lt;strong&gt;idempotent&lt;/strong&gt; — you can re-run provisioning without breaking things. This matters when debugging partial failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4: State That Actually Makes Sense
&lt;/h2&gt;

&lt;p&gt;Three locations, three purposes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local machine (&lt;code&gt;~/.clawmacdo/&lt;/code&gt;)&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.clawmacdo/
├── backups/     # Timestamped .tar.gz archives
├── keys/        # Generated SSH private keys
└── deploys/     # DeployRecord JSON files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;DeployRecord&lt;/strong&gt; captures everything about a deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"droplet_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;555085163&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hostname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openclaw-b0c825bf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ip_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"178.128.222.239"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sgp1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s-2vcpu-4gb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ssh_key_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.clawmacdo/keys/abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"backup_restored"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-01T10:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Remote droplet&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.env&lt;/code&gt; — runtime secrets&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;openclaw.json&lt;/code&gt; — channel/model/plugin config&lt;/li&gt;
&lt;li&gt;systemd user service — gateway lifecycle&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Layer 5: Errors That Help You Fix Things
&lt;/h2&gt;

&lt;p&gt;This is where most deployment tools fail. Something breaks and you get &lt;code&gt;Error: deployment failed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;clawmacdo takes a different approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Typed errors&lt;/strong&gt; via a custom error enum — every failure mode has a specific type&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No auto-destroy on failure&lt;/strong&gt; — if step 12 fails, your droplet survives. You get SSH debug info so you can fix it manually&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH/cloud-init timeouts&lt;/strong&gt; include diagnostic output — not just "timed out" but actual logs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gateway validation&lt;/strong&gt; checks both systemd state AND the &lt;code&gt;/health&lt;/code&gt; endpoint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web UI streams progress incrementally&lt;/strong&gt; — you see exactly which step failed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a deliberate design choice: &lt;strong&gt;a half-deployed server you can debug is better than a destroyed server you cannot.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Full 16-Step Deploy Flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.  Resolve params + defaults
2.  Generate SSH keypair locally
3.  Upload public key to DigitalOcean
4.  Create droplet with cloud-init
5.  Poll until droplet is active
6.  Wait for SSH + cloud-init sentinel
7.  SCP backup to droplet (if selected)
8.  Restore backup on droplet
9.  Create openclaw user + permissions
10. Harden firewall (UFW + fail2ban)
11. Configure Docker
12. Install Node.js + AI CLIs
13. Install + configure OpenClaw
14. Optional: Tailscale setup
15. Start gateway + health check
16. Configure model failover + save DeployRecord
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step reports progress. Each step can fail gracefully. Each step builds on the last.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: Post-Deploy Lifecycle
&lt;/h2&gt;

&lt;p&gt;Deployment is not the end. clawmacdo also handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;whatsapp-repair&lt;/code&gt;&lt;/strong&gt; — fixes WhatsApp channel issues post-deploy (updates OpenClaw, normalizes config, restarts gateway, probes login capability)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;migrate&lt;/code&gt;&lt;/strong&gt; — SSHs into an existing droplet, backs up remotely, deploys a new one, restores&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;destroy&lt;/code&gt;&lt;/strong&gt; — cleans up everything (droplet + SSH keys on DO + local keys)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;status&lt;/code&gt;&lt;/strong&gt; — lists all your openclaw-tagged droplets&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I Learned Building This
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rust + tokio + Axum is a killer stack for CLI-with-web-UI tools.&lt;/strong&gt; The type system catches so many deployment bugs at compile time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SSH operations are messy.&lt;/strong&gt; Cloud-init takes variable time, SSH daemons need polling, and &lt;code&gt;libssh2&lt;/code&gt; has quirks. Build robust retry/wait logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Never auto-destroy on failure.&lt;/strong&gt; Leave the server running so people can debug. Print SSH commands they can copy-paste.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stream progress, do not batch it.&lt;/strong&gt; Whether terminal or web, people want to see what is happening NOW.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Idempotent provisioning modules save your sanity.&lt;/strong&gt; When step 11 fails, you want to re-run from step 11, not step 1.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The entire codebase is open source:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/kenken64/clawmacdo" rel="noopener noreferrer"&gt;github.com/kenken64/clawmacdo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Download a pre-built binary from &lt;a href="https://github.com/kenken64/clawmacdo/releases" rel="noopener noreferrer"&gt;Releases&lt;/a&gt; — available for Windows, Linux, and macOS (Apple Silicon).&lt;/p&gt;

&lt;p&gt;If this kind of systems architecture content interests you, drop a star on the repo and follow for more deep dives.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The best deployment tool is not the one with the most features — it is the one that tells you exactly what went wrong when things break.&lt;/em&gt; 🦞🦀&lt;/p&gt;

</description>
      <category>rust</category>
      <category>architecture</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Watch: Deploying an AI Assistant to DigitalOcean in 16 Steps with clawmacdo</title>
      <dc:creator>Kenneth Phang</dc:creator>
      <pubDate>Wed, 04 Mar 2026 05:08:46 +0000</pubDate>
      <link>https://dev.to/kenneth_phang_fb8bb559a6f/watch-deploying-an-ai-assistant-to-digitalocean-in-16-steps-with-clawmacdo-2a0m</link>
      <guid>https://dev.to/kenneth_phang_fb8bb559a6f/watch-deploying-an-ai-assistant-to-digitalocean-in-16-steps-with-clawmacdo-2a0m</guid>
      <description>&lt;p&gt;I recently recorded a quick demo showing how &lt;strong&gt;clawmacdo&lt;/strong&gt; deploys a full AI assistant stack to DigitalOcean — from zero to a running WhatsApp/Telegram bot in minutes.&lt;/p&gt;

&lt;p&gt;🎬 &lt;strong&gt;&lt;a href="https://youtube.com/shorts/IQm5_Ojrc-w" rel="noopener noreferrer"&gt;Watch the demo on YouTube&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Just Saw
&lt;/h2&gt;

&lt;p&gt;The entire deployment flow, start to finish:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Enter Your Credentials
&lt;/h3&gt;

&lt;p&gt;You provide your API keys once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DigitalOcean&lt;/strong&gt; personal access token (to provision the droplet)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic&lt;/strong&gt; API key (Claude as your primary AI model)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI&lt;/strong&gt; API key (GPT as failover)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini&lt;/strong&gt; API key (Google as second failover)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clawmacdo deploy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--do-token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dop_v1_xxx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--anthropic-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-ant-xxx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--openai-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-xxx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--gemini-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AIzaSy...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Configure Server Settings
&lt;/h3&gt;

&lt;p&gt;Pick your region, droplet size, and messaging channels. clawmacdo handles the rest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Automated 16-Step Deployment
&lt;/h3&gt;

&lt;p&gt;Once you hit deploy, clawmacdo runs through &lt;strong&gt;16 automated steps&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate SSH keypair&lt;/li&gt;
&lt;li&gt;Upload public key to DigitalOcean&lt;/li&gt;
&lt;li&gt;Create Ubuntu 24.04 droplet&lt;/li&gt;
&lt;li&gt;Wait for droplet to boot&lt;/li&gt;
&lt;li&gt;SSH into the droplet&lt;/li&gt;
&lt;li&gt;Install Node.js 24&lt;/li&gt;
&lt;li&gt;Install OpenClaw globally&lt;/li&gt;
&lt;li&gt;Install Claude Code (Anthropic coding agent)&lt;/li&gt;
&lt;li&gt;Install Codex (OpenAI coding agent)&lt;/li&gt;
&lt;li&gt;Install Gemini CLI (Google coding agent)&lt;/li&gt;
&lt;li&gt;Configure &lt;code&gt;.env&lt;/code&gt; with all API keys&lt;/li&gt;
&lt;li&gt;Set up OpenClaw config (&lt;code&gt;openclaw.json&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Configure model failover chain (Anthropic to OpenAI to Google)&lt;/li&gt;
&lt;li&gt;Set up systemd service&lt;/li&gt;
&lt;li&gt;Start the OpenClaw gateway&lt;/li&gt;
&lt;li&gt;Verify everything is running&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of this happens &lt;strong&gt;automatically&lt;/strong&gt;. No SSH-ing in manually. No copy-pasting configs. No debugging cloud-init scripts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Post-Deploy Setup
&lt;/h3&gt;

&lt;p&gt;Once the deployment finishes, you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connection details&lt;/strong&gt; — IP address, SSH command&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telegram pairing&lt;/strong&gt; — Connect your Telegram bot instantly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker fixes&lt;/strong&gt; — Automatic container optimization if needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WhatsApp QR code&lt;/strong&gt; — Scan with your phone to link WhatsApp&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The demo ends with a QR code appearing — scan it, and your AI assistant is live on WhatsApp. That fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Setting up an AI assistant manually takes &lt;strong&gt;30+ minutes&lt;/strong&gt; of tedious work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provisioning servers&lt;/li&gt;
&lt;li&gt;Installing dependencies&lt;/li&gt;
&lt;li&gt;Configuring API keys&lt;/li&gt;
&lt;li&gt;Setting up messaging channels&lt;/li&gt;
&lt;li&gt;Debugging what went wrong&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With clawmacdo, it takes &lt;strong&gt;one command&lt;/strong&gt;. And because it is written in Rust, it is a single binary with zero runtime dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is OpenClaw?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://openclaw.ai" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt; is an open-source AI assistant platform. Think of it as your personal Jarvis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-channel&lt;/strong&gt; — WhatsApp, Telegram, Discord, Slack, Signal, iMessage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-model&lt;/strong&gt; — Claude, GPT, Gemini with automatic failover&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full access&lt;/strong&gt; — Browser control, voice calls, file management, shell access, smart home&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted&lt;/strong&gt; — Your data stays on your server&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Download clawmacdo from the &lt;a href="https://github.com/kenken64/clawmacdo/releases" rel="noopener noreferrer"&gt;Releases page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Get a &lt;a href="https://cloud.digitalocean.com/account/api/tokens" rel="noopener noreferrer"&gt;DigitalOcean API token&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Get API keys from &lt;a href="https://console.anthropic.com/" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt;, &lt;a href="https://platform.openai.com/" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt;, and/or &lt;a href="https://aistudio.google.com/" rel="noopener noreferrer"&gt;Google&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;clawmacdo deploy&lt;/code&gt; and watch the magic happen&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/kenken64/clawmacdo" rel="noopener noreferrer"&gt;github.com/kenken64/clawmacdo&lt;/a&gt; Star us!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenClaw&lt;/strong&gt;: &lt;a href="https://openclaw.ai" rel="noopener noreferrer"&gt;openclaw.ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community&lt;/strong&gt;: &lt;a href="https://discord.com/invite/clawd" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you found this useful, drop a star on the repo — it helps others discover the project!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;From credentials to a live AI assistant on WhatsApp — all in one command. That is the power of automation.&lt;/em&gt; 🦞&lt;/p&gt;

</description>
      <category>rust</category>
      <category>opensource</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Built a Rust CLI to Deploy AI Assistants to DigitalOcean in One Command</title>
      <dc:creator>Kenneth Phang</dc:creator>
      <pubDate>Tue, 03 Mar 2026 11:50:50 +0000</pubDate>
      <link>https://dev.to/kenneth_phang_fb8bb559a6f/i-built-a-rust-cli-to-deploy-ai-assistants-to-digitalocean-in-one-command-2f5d</link>
      <guid>https://dev.to/kenneth_phang_fb8bb559a6f/i-built-a-rust-cli-to-deploy-ai-assistants-to-digitalocean-in-one-command-2f5d</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;If you use &lt;a href="https://openclaw.ai" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt; — the open-source AI assistant framework that connects Claude, GPT, and Gemini to WhatsApp, Telegram, Discord, and more — you know the pain of deploying it to a cloud server.&lt;/p&gt;

&lt;p&gt;You need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provision a VPS&lt;/li&gt;
&lt;li&gt;Install Node.js, OpenClaw, coding agents (Claude Code, Codex, Gemini CLI)&lt;/li&gt;
&lt;li&gt;Copy over your config, API keys, and messaging credentials&lt;/li&gt;
&lt;li&gt;Configure model failover&lt;/li&gt;
&lt;li&gt;Start the gateway&lt;/li&gt;
&lt;li&gt;And if you want to migrate between servers? Do it all again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;I got tired of doing this manually, so I built &lt;code&gt;clawmacdo&lt;/code&gt;&lt;/strong&gt; — a Rust CLI that handles the entire lifecycle in one command.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is clawmacdo?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/kenken64/clawmacdo" rel="noopener noreferrer"&gt;&lt;strong&gt;clawmacdo&lt;/strong&gt;&lt;/a&gt; is a Rust CLI tool for deploying and migrating OpenClaw instances on DigitalOcean.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/kenken64/clawmacdo/actions/workflows/release.yml/badge.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://github.com/kenken64/clawmacdo/actions/workflows/release.yml/badge.svg" alt="Release" width="120" height="20"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Features at a Glance
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;backup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Archives your &lt;code&gt;~/.openclaw/&lt;/code&gt; config into a timestamped &lt;code&gt;.tar.gz&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deploy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1-click deploy: provisions DO droplet, installs everything, restores config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;migrate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DO-to-DO migration: backup source → deploy new → restore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;destroy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tears down a droplet + cleans up SSH keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lists all your OpenClaw-tagged droplets with IPs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list-backups&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shows local backup archives&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  One-Click Deploy
&lt;/h2&gt;

&lt;p&gt;Here is the magic. One command to go from zero to a fully running AI assistant in the cloud:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clawmacdo deploy &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--do-token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dop_v1_xxx &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--anthropic-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-ant-xxx &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--openai-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-xxx &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--gemini-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AIzaSy... &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--whatsapp-phone-number&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;15551234567 &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--telegram-bot-token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;123456789:AA...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single command:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Generates SSH keys&lt;/strong&gt; for secure access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provisions a DigitalOcean droplet&lt;/strong&gt; (Ubuntu 24.04, Singapore region)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Installs Node 24 + OpenClaw&lt;/strong&gt; via cloud-init&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Installs Claude Code, Codex, and Gemini CLI&lt;/strong&gt; as coding agents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restores your config&lt;/strong&gt; from a local backup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configures &lt;code&gt;.env&lt;/code&gt;&lt;/strong&gt; with all your API keys and messaging tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Starts the gateway&lt;/strong&gt; and sets up model failover (Anthropic → OpenAI → Google)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verifies everything is running&lt;/strong&gt; and gives you the IP&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole process takes about 3-5 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server-to-Server Migration
&lt;/h2&gt;

&lt;p&gt;Need to move your assistant to a bigger droplet? Or just want a fresh start?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clawmacdo migrate &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--source-ip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;164.90.xxx.xxx &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--do-token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dop_v1_xxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This SSHs into your source droplet, backs up the config remotely, deploys a fresh droplet, and restores everything. Zero downtime migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Rust?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single binary&lt;/strong&gt; — no runtime dependencies, no &lt;code&gt;node_modules&lt;/code&gt;, no Python virtualenvs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-platform&lt;/strong&gt; — pre-built binaries for Windows, Linux, and macOS (Apple Silicon)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast&lt;/strong&gt; — startup is instant, SSH operations use native &lt;code&gt;libssh2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliable&lt;/strong&gt; — strong typing catches config errors at compile time&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install from release binary
&lt;/h3&gt;

&lt;p&gt;Download the latest binary for your platform from &lt;a href="https://github.com/kenken64/clawmacdo/releases" rel="noopener noreferrer"&gt;Releases&lt;/a&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Windows x86_64&lt;/td&gt;
&lt;td&gt;&lt;code&gt;clawmacdo-windows-amd64.zip&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux x86_64&lt;/td&gt;
&lt;td&gt;&lt;code&gt;clawmacdo-linux-amd64.tar.gz&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS Apple Silicon&lt;/td&gt;
&lt;td&gt;&lt;code&gt;clawmacdo-darwin-arm64.tar.gz&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Build from source
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/kenken64/clawmacdo.git
&lt;span class="nb"&gt;cd &lt;/span&gt;clawmacdo
cargo build &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check your deployed instances
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clawmacdo status &lt;span class="nt"&gt;--do-token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dop_v1_xxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Bigger Picture: OpenClaw
&lt;/h2&gt;

&lt;p&gt;If you have not heard of &lt;a href="https://openclaw.ai" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt;, it is an open-source framework that turns LLMs into personal AI assistants connected to your real communication channels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WhatsApp, Telegram, Discord, Slack, iMessage&lt;/strong&gt; — your AI responds in your actual chats&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-model&lt;/strong&gt; — Claude, GPT, Gemini with automatic failover&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool use&lt;/strong&gt; — file access, web browsing, code execution, voice calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills system&lt;/strong&gt; — extensible plugins for chess, weather, coding, and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted&lt;/strong&gt; — your data stays on your server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;clawmacdo&lt;/code&gt; makes deploying OpenClaw to the cloud as easy as deploying a static site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contributing
&lt;/h2&gt;

&lt;p&gt;The project is open source and contributions are welcome! Whether it is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supporting other cloud providers (AWS, GCP, Hetzner)&lt;/li&gt;
&lt;li&gt;Adding new deployment targets (Docker, Kubernetes)&lt;/li&gt;
&lt;li&gt;Improving the migration workflow&lt;/li&gt;
&lt;li&gt;Bug fixes and documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check out the repo: &lt;strong&gt;&lt;a href="https://github.com/kenken64/clawmacdo" rel="noopener noreferrer"&gt;github.com/kenken64/clawmacdo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;⭐ If this is useful to you, a star on GitHub helps others discover it!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with 🦀 Rust and ❤️ for the OpenClaw community.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>opensource</category>
      <category>ai</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
