<?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: DeployHQ</title>
    <description>The latest articles on DEV Community by DeployHQ (@deployhq).</description>
    <link>https://dev.to/deployhq</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1687924%2F3c97db5e-145a-4aae-adbe-b57f149a6ec3.png</url>
      <title>DEV Community: DeployHQ</title>
      <link>https://dev.to/deployhq</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/deployhq"/>
    <language>en</language>
    <item>
      <title>Self-Host n8n on a VPS: Docker, HTTPS, and Git-Based Updates</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Thu, 21 May 2026 09:40:22 +0000</pubDate>
      <link>https://dev.to/deployhq/self-host-n8n-on-a-vps-docker-https-and-git-based-updates-2k9k</link>
      <guid>https://dev.to/deployhq/self-host-n8n-on-a-vps-docker-https-and-git-based-updates-2k9k</guid>
      <description>&lt;p&gt;Most n8n self-hosting tutorials stop at &lt;code&gt;docker compose up&lt;/code&gt;. That gets you a running instance, but it leaves a real gap: where do your workflows live? How do you upgrade without losing them? How do you roll back a bad change? Treat your n8n server like any other application — workflows in Git, infrastructure in Docker Compose, deploys triggered from &lt;code&gt;main&lt;/code&gt; — and self-hosting becomes a maintainable habit instead of a fragile pet project.&lt;/p&gt;

&lt;p&gt;This guide walks through standing up n8n on a single VPS with Docker, putting it behind HTTPS, and wiring it to a &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project so every change to your workflow repo updates the server. By the end you'll have a production-ready instance with backups, version control, and one-click rollback when something goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll build
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;n8n&lt;/strong&gt; running in Docker on a single Ubuntu VPS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres&lt;/strong&gt; alongside it as the data store (not SQLite — more on that below)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nginx + Let's Encrypt&lt;/strong&gt; in front for HTTPS on your own domain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Git repository&lt;/strong&gt; that holds the &lt;code&gt;docker-compose.yml&lt;/code&gt;, environment template, and an exportable JSON copy of every workflow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DeployHQ&lt;/strong&gt; wiring the repo to the server, so a &lt;code&gt;git push&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt; reconfigures the stack and re-imports workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end you'll be able to develop a workflow locally (or on a staging instance), export it, commit, push, and watch &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; deploy the change with rollback available if it misbehaves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why self-host n8n at all?
&lt;/h2&gt;

&lt;p&gt;n8n Cloud is good. So why bother with a VPS?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost predictability.&lt;/strong&gt; A $5-10/month VPS handles thousands of runs per day. Cloud pricing scales by execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data stays on your infrastructure.&lt;/strong&gt; Webhooks, credentials, and execution logs never leave the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom nodes.&lt;/strong&gt; You can &lt;code&gt;npm install&lt;/code&gt; community nodes the cloud version doesn't ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No execution limits.&lt;/strong&gt; Long-running flows that hit cloud step caps just run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off is operational ownership — you patch the OS, renew the certificate, watch the disk. Most of that is one-time setup. The rest of this guide is the one-time setup, done properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A VPS with Ubuntu 22.04 or 24.04 and SSH access — any of Hetzner, DigitalOcean, Vultr, Linode work fine&lt;/li&gt;
&lt;li&gt;A domain you control with an A record pointing at the VPS IP&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; account (free trial — sign-up link at the end)&lt;/li&gt;
&lt;li&gt;A GitHub or GitLab repository&lt;/li&gt;
&lt;li&gt;Docker installed locally for testing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Provision the VPS
&lt;/h2&gt;

&lt;p&gt;SSH in as root, install Docker, and set up a deploy user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@your-vps-ip
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.docker.com | sh

adduser deploy
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker deploy
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /home/deploy/n8n
&lt;span class="nb"&gt;chown &lt;/span&gt;deploy:deploy /home/deploy/n8n

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From your local machine, copy your SSH key to the deploy user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-copy-id deploy@your-vps-ip

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm passwordless login works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh deploy@your-vps-ip &lt;span class="s2"&gt;"docker --version"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the installed Docker version. That's the account &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; will use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: The n8n Docker Compose stack
&lt;/h2&gt;

&lt;p&gt;In your local repo, create &lt;code&gt;docker-compose.yml&lt;/code&gt;:&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&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;postgres_data:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_DB}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;n8n&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8nio/n8n:1.74.0&lt;/span&gt; &lt;span class="c1"&gt;# pin to a specific tag — don't use :latest in prod&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:5678:5678"&lt;/span&gt; &lt;span class="c1"&gt;# localhost only — Nginx will proxy&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_TYPE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresdb&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;N8N_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${N8N_HOST}&lt;/span&gt;
      &lt;span class="na"&gt;N8N_PROTOCOL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
      &lt;span class="na"&gt;N8N_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5678&lt;/span&gt;
      &lt;span class="na"&gt;WEBHOOK_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://${N8N_HOST}/&lt;/span&gt;
      &lt;span class="na"&gt;GENERIC_TIMEZONE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${TIMEZONE:-Europe/London}&lt;/span&gt;
      &lt;span class="na"&gt;N8N_ENCRYPTION_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${N8N_ENCRYPTION_KEY}&lt;/span&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;n8n_data:/home/node/.n8n&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./workflows:/workflows:ro&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;n8n_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four things matter here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Postgres, not SQLite.&lt;/strong&gt; SQLite is fine for kicking the tires. For anything you care about, Postgres handles concurrent executions, backups, and growth without complaint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt; is sacred.&lt;/strong&gt; It encrypts the credentials n8n stores in the database. Change it and every credential breaks — you'll have to re-enter every API key, OAuth token, and SSH credential. Generate it once with &lt;code&gt;openssl rand -hex 32&lt;/code&gt;, store it in &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; as a config file, and never lose it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WEBHOOK_URL&lt;/code&gt; must be your public HTTPS URL.&lt;/strong&gt; n8n bakes this into the URLs it gives webhook senders. If it's wrong, Stripe / GitHub / Slack will hit a dead address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port 5678 is bound to &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/strong&gt; Don't expose n8n directly to the internet — put it behind Nginx with TLS. Anyone hitting port 5678 from outside gets nothing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Create &lt;code&gt;.env.example&lt;/code&gt; to commit to the repo (without secrets):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;n8n&lt;/span&gt;
&lt;span class="py"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="py"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;n8n&lt;/span&gt;
&lt;span class="py"&gt;N8N_HOST&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;workflows.yourdomain.com&lt;/span&gt;
&lt;span class="py"&gt;N8N_ENCRYPTION_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="py"&gt;TIMEZONE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Europe/London&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real &lt;code&gt;.env&lt;/code&gt; lives in DeployHQ's server config — never in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Put Nginx in front with Let's Encrypt
&lt;/h2&gt;

&lt;p&gt;On the VPS, install Nginx and Certbot:&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="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nginx certbot python3-certbot-nginx

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a server block at &lt;code&gt;/etc/nginx/sites-available/n8n&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;workflows.yourdomain.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;client_max_body_size&lt;/span&gt; &lt;span class="mi"&gt;50M&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:5678&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;'upgrade'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_cache_bypass&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_read_timeout&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;# long-running executions&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_send_timeout&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&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;Enable it and run Certbot:&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="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx
&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot &lt;span class="nt"&gt;--nginx&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; workflows.yourdomain.com

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;proxy_read_timeout 3600&lt;/code&gt; is important. n8n executions can run for minutes when a workflow waits on an external API — without that, Nginx kills the connection and the run appears to fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: First run
&lt;/h2&gt;

&lt;p&gt;Back on your local machine, commit the repo and push it to GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git init &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git add &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Initial n8n stack"&lt;/span&gt;
git remote add origin git@github.com:you/n8n-stack.git
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the very first deploy, SSH into the VPS, copy the &lt;code&gt;.env&lt;/code&gt; over (DeployHQ will manage it from here on), and bring the stack up manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh deploy@your-vps-ip
&lt;span class="nb"&gt;cd&lt;/span&gt; /home/deploy/n8n
&lt;span class="c"&gt;# (DeployHQ will populate docker-compose.yml on first deploy — for now scp it manually)&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;code&gt;https://workflows.yourdomain.com&lt;/code&gt;, create the admin account, and you're live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Wire up Git-based deploys with DeployHQ
&lt;/h2&gt;

&lt;p&gt;This is the part most n8n tutorials skip. In DeployHQ:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a new project&lt;/strong&gt; and point it at your repo. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; supports &lt;a href="https://www.deployhq.com/deploy-from-github" rel="noopener noreferrer"&gt;deploying directly from a GitHub repo to your server&lt;/a&gt; without you wiring webhooks by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the VPS as a server.&lt;/strong&gt; Username &lt;code&gt;deploy&lt;/code&gt;, port 22, deployment path &lt;code&gt;/home/deploy/n8n&lt;/code&gt;. Use the SSH key you authorised in Step 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the &lt;code&gt;.env&lt;/code&gt; as a config file.&lt;/strong&gt; In the server's config-files section, create &lt;code&gt;/home/deploy/n8n/.env&lt;/code&gt; and paste the real values (Postgres password, &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt;, &lt;code&gt;N8N_HOST&lt;/code&gt;). &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; writes this file before every deploy — your secrets stay out of Git.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure the deploy SSH command:&lt;/strong&gt; &lt;code&gt;bash
cd /home/deploy/n8n &amp;amp;&amp;amp; \
docker compose pull &amp;amp;&amp;amp; \
docker compose up -d &amp;amp;&amp;amp; \
docker compose exec -T n8n n8n import:workflow --input=/workflows/ --separate
&lt;/code&gt;This pulls any new n8n image version, recreates containers with the new Compose config, and re-imports every workflow JSON from the mounted &lt;code&gt;/workflows&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable auto-deploy on push to &lt;code&gt;main&lt;/code&gt;.&lt;/strong&gt; Every merge now triggers a deploy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Push a no-op change to trigger the first run. Watch the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; log — you'll see the repo transferred, the env file written, the SSH commands executed. The site shouldn't blink: &lt;code&gt;docker compose up -d&lt;/code&gt; only recreates containers if something actually changed.&lt;/p&gt;

&lt;p&gt;If a deploy ever breaks something, &lt;a href="https://www.deployhq.com/features/one-click-rollback" rel="noopener noreferrer"&gt;DeployHQ's one-click rollback&lt;/a&gt; restores the previous repo state and re-runs the deploy command — a single button, no manual Compose juggling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Workflows as code
&lt;/h2&gt;

&lt;p&gt;This is the payoff for everything above. Instead of &lt;q&gt;the workflow is whatever I last clicked in the UI,&lt;/q&gt; your repo becomes the source of truth.&lt;/p&gt;

&lt;p&gt;The flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Develop locally or on a staging instance.&lt;/strong&gt; A spare n8n container on your laptop works fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export the workflow.&lt;/strong&gt; In the n8n UI: open the workflow → menu → &lt;q&gt;Download&lt;/q&gt; → save the JSON to &lt;code&gt;workflows/your-workflow-name.json&lt;/code&gt; in your repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit and push.&lt;/strong&gt; &lt;code&gt;bash
git add workflows/your-workflow-name.json
git commit -m "Add: Slack-to-Sheets sync workflow"
git push
&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DeployHQ deploys.&lt;/strong&gt; The deploy command runs &lt;code&gt;n8n import:workflow --input=/workflows/ --separate&lt;/code&gt;, which upserts the workflow into the database. Existing workflows with the same ID are updated; new ones are created.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A few practical notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One JSON file per workflow.&lt;/strong&gt; The &lt;code&gt;--separate&lt;/code&gt; flag tells n8n's importer to treat each file independently. Easier diffs in PRs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credentials are not in the JSON.&lt;/strong&gt; n8n stores them encrypted in the database, referenced by ID. Set up credentials once in the UI; the workflow JSON just references them. This is why &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt; matters — drop it and the references become unusable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The UI is now read-only in your head.&lt;/strong&gt; Anyone making changes in production goes back to staging, exports, commits. Drift kills you otherwise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same Compose pattern works for sidecar services — drop a Python agent or a Postgres backup container into the same &lt;code&gt;docker-compose.yml&lt;/code&gt;, and &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; deploys all of it together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Upgrade n8n safely
&lt;/h2&gt;

&lt;p&gt;n8n releases often. The temptation is to use &lt;code&gt;n8nio/n8n:latest&lt;/code&gt; and let it ride. Don't.&lt;/p&gt;

&lt;p&gt;With a pinned tag, upgrading becomes a one-line PR:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- image: n8nio/n8n:1.74.0
&lt;/span&gt;&lt;span class="gi"&gt;+ image: n8nio/n8n:1.78.0
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Push, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; deploys, the new image is pulled, the container recreated. If anything misbehaves, hit rollback and the previous tag is back in seconds. You get a git history of every n8n version that has ever run in production — handy when a workflow stops working and you need to find what changed.&lt;/p&gt;

&lt;p&gt;Two upgrade hygiene tips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read the changelog&lt;/strong&gt; before bumping a major. n8n occasionally deprecates nodes or changes execution semantics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot Postgres before a major.&lt;/strong&gt; A &lt;code&gt;docker compose exec postgres pg_dump ...&lt;/code&gt; to a file in a backup directory takes seconds and saves hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For deploys in general, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; runs &lt;a href="https://www.deployhq.com/features/zero-downtime-deployments" rel="noopener noreferrer"&gt;zero downtime deployments&lt;/a&gt; by default — when you eventually move to a multi-server setup, the rollout pattern stays the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Backups
&lt;/h2&gt;

&lt;p&gt;Two things to back up:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Postgres database.&lt;/strong&gt; Everything important — workflows, credentials, execution history — lives here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; postgres &lt;span class="se"&gt;\&lt;/span&gt;
  pg_dump &lt;span class="nt"&gt;-U&lt;/span&gt; n8n &lt;span class="nt"&gt;-d&lt;/span&gt; n8n &lt;span class="nt"&gt;--no-owner&lt;/span&gt; &lt;span class="nt"&gt;--clean&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backup-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;.sql

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop a small script in your repo at &lt;code&gt;scripts/backup.sh&lt;/code&gt; that runs this and uploads the file to S3 (or B2, or any object store). Cron it nightly on the VPS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;n8n_data&lt;/code&gt; volume.&lt;/strong&gt; Holds binary attachments and custom nodes. Tar it once a week to the same store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; n8n_n8n_data:/data &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;$PWD&lt;/span&gt;:/backup alpine &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;tar &lt;/span&gt;czf /backup/n8n-data-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /data &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflows themselves are already in Git, so you don't need to back those up — that's the whole point of the workflows-as-code pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;The pillar above gets you a single-node, production-ready n8n instance with version control and rollback. From here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Drop a custom AI service next to n8n.&lt;/strong&gt; The same &lt;code&gt;docker-compose.yml&lt;/code&gt; handles a sidecar Python or Node service — expose it at &lt;code&gt;http://agent:8000&lt;/code&gt; and have any n8n workflow hit it as a webhook. Same VPS, same deploy pipeline, no extra infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare with other self-hosted agent platforms.&lt;/strong&gt; If n8n's node-based model isn't the right fit, look at alternatives covered on the blog: &lt;a href="https://dev.to/deployhq/self-hosting-paperclip-on-a-vps-with-docker-and-continuous-deployment-4hh5-temp-slug-5506684"&gt;Paperclip as a self-hosted agent orchestrator&lt;/a&gt;, &lt;a href="https://dev.to/theqadiariesforyou/how-to-deploy-and-configure-openclaw-on-a-vps-4h8e-temp-slug-2543902"&gt;OpenClaw as a self-hosted AI assistant with a Skills plugin system&lt;/a&gt;, and &lt;a href="https://dev.to/deployhq/deploy-hermes-agent-on-a-vps-deployhq-workflow-how-it-differs-from-openclaw-34d9-temp-slug-5607414"&gt;Hermes Agent for self-improving agents on a VPS&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run agents inside CI/CD.&lt;/strong&gt; For a different shape of the same problem — agents that act on your repository — see &lt;a href="https://dev.to/deployhq/ai-agents-in-cicd-pipelines-from-github-issue-to-production-deploy-5b4b-temp-slug-7931927"&gt;how AI agents fit into CI/CD pipelines from GitHub issue to production deploy&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drive deploys from a terminal agent.&lt;/strong&gt; If you want Claude Code, Cursor, or Codex to trigger n8n redeploys for you, &lt;a href="https://dev.to/deployhq/deployhq-cli-deploy-from-your-terminal-or-let-your-ai-agent-do-it-7le-temp-slug-4729135"&gt;the&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; CLI exposes a deployment trigger your AI agent can call.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Ready to put n8n on infrastructure you control, with a Git-driven deploy pipeline behind it? &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Start a free&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; trial and wire your first push-to-deploy n8n stack in under twenty minutes.&lt;/p&gt;

&lt;p&gt;Questions? Email us at &lt;strong&gt;&lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt;&lt;/strong&gt; or find us on X at &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;@deployhq&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>docker</category>
      <category>n8n</category>
      <category>vps</category>
    </item>
    <item>
      <title>DeployHQ CLI: Deploy From Your Terminal (or Let Your AI Agent Do It)</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Mon, 11 May 2026 13:56:25 +0000</pubDate>
      <link>https://dev.to/deployhq/deployhq-cli-deploy-from-your-terminal-or-let-your-ai-agent-do-it-50o3</link>
      <guid>https://dev.to/deployhq/deployhq-cli-deploy-from-your-terminal-or-let-your-ai-agent-do-it-50o3</guid>
      <description>&lt;p&gt;If you have ever wished you could deploy without leaving your terminal — no browser tab, no dashboard click, no context switch — that is exactly what the new &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; CLI is built for. It is a single Go binary, called &lt;code&gt;dhq&lt;/code&gt;, that wraps the entire &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; API. You install it once and you can trigger deployments, watch them in real time, tail logs, and roll back from the command line. And because the output is machine-readable by default, your AI coding assistant can do the same thing on your behalf.&lt;/p&gt;

&lt;p&gt;This guide walks through installing the CLI, authenticating, running your first deployment, watching it live, tailing logs, and rolling back. Then we cover the part most teams care about in 2026: wiring &lt;code&gt;dhq&lt;/code&gt; into Claude Code, Cursor, Codex, and Windsurf so an AI coding agent can drive your deployments end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Terminal-First Deployment Tool Matters
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;Deploy&lt;/a&gt; steps are the easiest place for an engineering workflow to break flow. You finish a fix, you push, the test suite passes — and then you alt-tab to a browser to click a button. With a CLI on your &lt;code&gt;$PATH&lt;/code&gt;, that step collapses into a single command. With the same CLI exposed to your AI assistant, the deploy step disappears from your workflow entirely: the assistant proposes it, you approve, it ships.&lt;/p&gt;

&lt;p&gt;That second use case — agent-driven deployment — is why &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; rebuilt the CLI from scratch. The previous Ruby gem (&lt;code&gt;gem install deployhq&lt;/code&gt;) is now deprecated. The new tool, &lt;code&gt;dhq&lt;/code&gt;, follows a strict output contract designed for both humans and agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;stdout is always data&lt;/strong&gt; — a clean table for humans, JSON when piped or when &lt;code&gt;--json&lt;/code&gt; is set&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;stderr is always human messages&lt;/strong&gt; — progress, warnings, errors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No interactive prompts&lt;/strong&gt; when all required flags are provided&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have read our take on &lt;a href="https://dev.to/deployhq/clis-or-mcp-for-coding-agents-a-practical-comparison-g04-temp-slug-1463040"&gt;why CLIs are the cleanest integration surface for AI coding agents&lt;/a&gt;, this is what that argument looks like in practice for deployments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing the DeployHQ CLI
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dhq&lt;/code&gt; ships as a single binary for macOS, Linux, and Windows (amd64 and arm64). Pick the install method that matches your environment:&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;# macOS (Homebrew)&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;deployhq/tap/dhq

&lt;span class="c"&gt;# Linux or macOS (universal install script)&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://deployhq.com/install/cli | sh

&lt;span class="c"&gt;# Windows (Scoop)&lt;/span&gt;
scoop bucket add deployhq https://github.com/deployhq/scoop-bucket
scoop &lt;span class="nb"&gt;install &lt;/span&gt;dhq

&lt;span class="c"&gt;# From source (Go)&lt;/span&gt;
go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/deployhq/deployhq-cli/cmd/dhq@latest

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After install, confirm the binary is on your &lt;code&gt;$PATH&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;dhq &lt;span class="nt"&gt;--version&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;dhq --version&lt;/code&gt; and &lt;code&gt;dhq version&lt;/code&gt; work — use whichever fits your scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authenticating
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dhq&lt;/code&gt; supports two authentication paths: an interactive login that stores credentials in your OS keyring (for everyday human use), and environment variables (for CI runners and AI agents).&lt;/p&gt;

&lt;p&gt;For the interactive flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dhq auth login

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will be prompted for your account domain (e.g. &lt;code&gt;my-account.deployhq.com&lt;/code&gt;), your email, and an API key from &lt;strong&gt;Settings → Security&lt;/strong&gt;. You can create or revoke API keys from the &lt;a href="https://www.deployhq.com/support/api" rel="noopener noreferrer"&gt;DeployHQ API settings page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For non-interactive environments — CI runners, Docker images, AI agent sandboxes — set three environment variables instead:&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="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DEPLOYHQ_ACCOUNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"my-account"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DEPLOYHQ_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"you@example.com"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DEPLOYHQ_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same pattern we recommend when &lt;a href="https://dev.to/deployhq/ai-agents-in-cicd-pipelines-from-github-issue-to-production-deploy-5b4b-temp-slug-7931927"&gt;pointing AI agents at CI/CD pipelines&lt;/a&gt;: credentials live in the runner's secret store, never in prompts or repo files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your First Deployment
&lt;/h2&gt;

&lt;p&gt;The five commands you will reach for most often:&lt;/p&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;dhq projects list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List projects on your account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dhq servers list -p &amp;lt;project&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List servers and groups for a project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dhq deploy -p &amp;lt;project&amp;gt; -s &amp;lt;server&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Trigger a deployment with live progress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dhq deployments watch -p &amp;lt;project&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stream live progress for a deployment already running&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dhq deployments logs &amp;lt;id&amp;gt; -p &amp;lt;project&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch logs for a specific deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dhq rollback &amp;lt;id&amp;gt; -p &amp;lt;project&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Revert a previous deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A typical session looks like this. Discover what you have access to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dhq projects list
&lt;span class="go"&gt;NAME PERMALINK REPO
api-service api-service github.com/acme/api
marketing-site marketing-site github.com/acme/marketing

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dhq servers list &lt;span class="nt"&gt;-p&lt;/span&gt; api-service
&lt;span class="go"&gt;NAME IDENTIFIER TYPE BRANCH PATH
production prod-srv-01 SSH main /var/www/api
staging stage-srv-01 SSH main /var/www/api-staging

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then deploy. With &lt;code&gt;--wait&lt;/code&gt;, you get a live TUI progress bar that updates as the deployment runs through its phases (fetch repo → build → upload → run SSH commands):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dhq deploy &lt;span class="nt"&gt;-p&lt;/span&gt; api-service &lt;span class="nt"&gt;-s&lt;/span&gt; staging &lt;span class="nt"&gt;--wait&lt;/span&gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the deployment is done, the CLI exits with status 0 on success and non-zero on failure — exactly what your shell scripts and CI runners need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watching, Logs, and Rollback
&lt;/h2&gt;

&lt;p&gt;Three operations turn the CLI from a &lt;q&gt;fire and forget&lt;/q&gt; trigger into a control plane you can run from one terminal window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch a deployment live.&lt;/strong&gt; If a deploy is already in flight, attach to it from anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dhq deployments watch &lt;span class="nt"&gt;-p&lt;/span&gt; api-service

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The watch view streams the same TUI you would see from &lt;code&gt;--wait&lt;/code&gt;, but for a deployment that started elsewhere — useful when a teammate or a CI job kicked it off and you want to monitor without refreshing the dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tail logs after the fact.&lt;/strong&gt; When something fails, you want the full log without scrolling through the web UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dhq deployments logs DEP-12345 &lt;span class="nt"&gt;-p&lt;/span&gt; api-service

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For agent-friendly output, add &lt;code&gt;--json&lt;/code&gt; and pipe into &lt;code&gt;jq&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;dhq deployments logs DEP-12345 &lt;span class="nt"&gt;-p&lt;/span&gt; api-service &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="s1"&gt;'.data.entries[] | select(.level=="error")'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Roll back a bad deploy.&lt;/strong&gt; If a release goes sideways, revert to the previous successful deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dhq rollback DEP-12345 &lt;span class="nt"&gt;-p&lt;/span&gt; api-service &lt;span class="nt"&gt;--wait&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rollback uses the same diff engine as a forward deploy, so the only files that move are the ones that changed. This is the same &lt;a href="https://www.deployhq.com/features/one-click-rollback" rel="noopener noreferrer"&gt;one-click rollback&lt;/a&gt; primitive exposed in the web UI, just from your terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Output Contract: Why It Matters for AI Agents
&lt;/h2&gt;

&lt;p&gt;Most CLIs were not designed to be called by AI coding agents. They mix progress messages with data on stdout, throw interactive prompts when you forget a flag, and return unstructured text that the agent has to guess how to parse.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dhq&lt;/code&gt; was built around three rules that fix that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;stdout is data, stderr is text&lt;/strong&gt; — a piped command always returns parseable output; status messages never pollute it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON on demand, JSON when piped&lt;/strong&gt; — &lt;code&gt;dhq deploy ... --json&lt;/code&gt; returns machine-readable output, and the same auto-switches when stdout is a pipe&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breadcrumbs in every JSON response&lt;/strong&gt; — the CLI tells the agent what to do next&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A typical JSON response includes a &lt;code&gt;breadcrumbs&lt;/code&gt; array that explicitly lists the next reasonable command:&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;"ok"&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;"data"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"deployment_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;"DEP-12345"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"completed"&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;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deployment DEP-12345 to staging completed in 47s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"breadcrumbs"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"logs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dhq deployments logs DEP-12345 -p api-service"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rollback"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dhq rollback DEP-12345 -p api-service"&lt;/span&gt;&lt;span class="p"&gt;}&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;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;The agent does not have to memorise the command surface. It runs one command, reads the breadcrumbs, and knows what to chain next. On failure the envelope flips — &lt;code&gt;{ok: false, error, recovery}&lt;/code&gt; — so the agent gets the same structured handle on what went wrong and what to try next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring &lt;code&gt;dhq&lt;/code&gt; Into Your AI Coding Assistant
&lt;/h2&gt;

&lt;p&gt;This is where the real workflow change happens. With &lt;code&gt;dhq&lt;/code&gt; on &lt;code&gt;$PATH&lt;/code&gt; and the right rules file in your repo, your AI editor can deploy your code without you ever opening a browser. A full companion guide on driving &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; from Cursor, Claude Code, and Windsurf is in the works (sister post coming Week 7) — what follows is the short version.&lt;/p&gt;

&lt;p&gt;The pattern is the same across all four editors below: &lt;strong&gt;the capability lives in the editor's permissions config; the judgement lives in a behavioural rules file.&lt;/strong&gt; Read-only commands (&lt;code&gt;projects list&lt;/code&gt;, &lt;code&gt;servers list&lt;/code&gt;, &lt;code&gt;deployments logs&lt;/code&gt;, &lt;code&gt;deployments watch&lt;/code&gt;) are safe by default. Destructive ones (&lt;code&gt;deploy&lt;/code&gt;, &lt;code&gt;rollback&lt;/code&gt;) should require an explicit human approval gate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Code
&lt;/h3&gt;

&lt;p&gt;Claude Code governs shell access through &lt;code&gt;.claude/settings.json&lt;/code&gt;. Allow the read-only &lt;code&gt;dhq&lt;/code&gt; commands; require approval for the destructive ones:&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;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="s2"&gt;"Bash(dhq projects:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(dhq servers list:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(dhq deployments logs:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(dhq deployments watch:*)"&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;span class="nl"&gt;"ask"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="s2"&gt;"Bash(dhq deploy:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(dhq rollback:*)"&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;span class="p"&gt;}&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;Then drop a &lt;code&gt;CLAUDE.md&lt;/code&gt; block at the project root telling Claude &lt;em&gt;when&lt;/em&gt; to reach for the CLI. Behavioural rules belong in &lt;code&gt;CLAUDE.md&lt;/code&gt;; we cover the full pattern in our guide to &lt;a href="https://dev.to/saasblogs/claudemd-agentsmd-and-every-ai-config-file-explained-43hk-temp-slug-4532602"&gt;CLAUDE.md, AGENTS.md and Copilot Instructions&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Deployment&lt;/span&gt;

This project uses DeployHQ. Use the &lt;span class="sb"&gt;`dhq`&lt;/span&gt; CLI for all deploys.
&lt;span class="p"&gt;
-&lt;/span&gt; After the test suite passes on a branch, propose &lt;span class="sb"&gt;`dhq deploy -p &amp;lt;project&amp;gt; -s staging --wait`&lt;/span&gt; and wait for my approval.
&lt;span class="p"&gt;-&lt;/span&gt; Never run &lt;span class="sb"&gt;`dhq deploy ... -s production`&lt;/span&gt; without an explicit instruction containing the word "production".
&lt;span class="p"&gt;-&lt;/span&gt; For a failing deploy, fetch logs with &lt;span class="sb"&gt;`dhq deployments logs &amp;lt;id&amp;gt;`&lt;/span&gt; before suggesting a fix.
&lt;span class="p"&gt;-&lt;/span&gt; For a regression in production, propose &lt;span class="sb"&gt;`dhq rollback &amp;lt;id&amp;gt;`&lt;/span&gt; first, then debug.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cursor
&lt;/h3&gt;

&lt;p&gt;Cursor's Composer agent shells out the same way. Drop the rules in &lt;code&gt;.cursor/rules/deployhq.mdc&lt;/code&gt; — that is the path current Cursor versions auto-load, with &lt;code&gt;.mdc&lt;/code&gt; frontmatter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;We deploy with DeployHQ via the &lt;span class="sb"&gt;`dhq`&lt;/span&gt; CLI.

When asked to deploy:
&lt;span class="p"&gt;1.&lt;/span&gt; Confirm the target environment in your message before running the command.
&lt;span class="p"&gt;2.&lt;/span&gt; Use &lt;span class="sb"&gt;`dhq deploy -p &amp;lt;project&amp;gt; -s staging --wait`&lt;/span&gt; for staging.
&lt;span class="p"&gt;3.&lt;/span&gt; For production, ask me to confirm in the chat before issuing the command.
&lt;span class="p"&gt;4.&lt;/span&gt; After triggering, check &lt;span class="sb"&gt;`dhq deployments watch`&lt;/span&gt; until completion or failure.
&lt;span class="p"&gt;5.&lt;/span&gt; On failure, run &lt;span class="sb"&gt;`dhq deployments logs &amp;lt;id&amp;gt;`&lt;/span&gt; and summarise the error before suggesting a fix.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Codex CLI / Windsurf
&lt;/h3&gt;

&lt;p&gt;OpenAI's Codex CLI and Windsurf's Cascade agent both run shell commands natively. Once &lt;code&gt;dhq&lt;/code&gt; is on &lt;code&gt;$PATH&lt;/code&gt; they can call it without extra config — the work is in giving them the right behavioural rules so they do not deploy the moment they feel confident. We compared the three terminal agents in detail in &lt;a href="https://dev.to/deployhq/comparing-claude-code-openai-codex-and-google-gemini-cli-which-ai-coding-assistant-is-right-for-1ffd"&gt;Claude Code vs Codex CLI vs Gemini CLI&lt;/a&gt;, and the deployment pattern that emerged is the same: read commands open, write commands gated by an explicit instruction.&lt;/p&gt;

&lt;p&gt;For an even shorter setup, &lt;code&gt;dhq setup&lt;/code&gt; writes the right files for each editor in the right place — no manual config copying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dhq setup claude &lt;span class="c"&gt;# writes ~/.claude/skills/deployhq/SKILL.md&lt;/span&gt;
dhq setup cursor &lt;span class="c"&gt;# writes .cursor/rules/deployhq.mdc in the current repo&lt;/span&gt;
dhq setup codex &lt;span class="c"&gt;# writes AGENTS.md in the current repo&lt;/span&gt;
dhq setup windsurf &lt;span class="c"&gt;# writes global_rules.md&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each variant installs a behavioural rules file the editor actually auto-loads, plus a &lt;code&gt;SKILL.md&lt;/code&gt; decision tree and reference docs that teach the agent when each &lt;code&gt;dhq&lt;/code&gt; command is appropriate. More on the design choices behind that in our piece on the &lt;a href="https://dev.to/deployhq/6-developer-clis-that-ai-coding-agents-actually-use-well-5973-temp-slug-2135258"&gt;developer CLIs that AI coding agents actually use well&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using &lt;code&gt;dhq&lt;/code&gt; in CI/CD
&lt;/h2&gt;

&lt;p&gt;The same output contract that makes the CLI work for AI agents makes it trivial to drop into CI. A minimal GitHub Actions step:&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;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;DEPLOYHQ_ACCOUNT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOYHQ_ACCOUNT }}&lt;/span&gt;
  &lt;span class="na"&gt;DEPLOYHQ_EMAIL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOYHQ_EMAIL }}&lt;/span&gt;
  &lt;span class="na"&gt;DEPLOYHQ_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOYHQ_API_KEY }}&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dhq&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;curl -fsSL https://deployhq.com/install/cli | sh&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to staging&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dhq deploy -p api-service -s staging --wait --json&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auth via env vars, JSON output for machine parsing, exit code drives the build status. If the deploy fails, the CI job fails — no extra plumbing required. For more on running AI agents inside the same kind of pipeline, see our piece on &lt;a href="https://dev.to/deployhq/agentic-workflows-explained-how-ai-agents-are-changing-cicd-pipelines-nm0-temp-slug-2291085"&gt;agentic workflows in CI/CD&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating From the Old Ruby Gem
&lt;/h2&gt;

&lt;p&gt;If you have the old &lt;code&gt;gem install deployhq&lt;/code&gt; tool installed, the migration is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install &lt;code&gt;dhq&lt;/code&gt; using one of the methods above.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;dhq auth login&lt;/code&gt; (or set the &lt;code&gt;DEPLOYHQ_*&lt;/code&gt; environment variables) — your existing API keys still work.&lt;/li&gt;
&lt;li&gt;Update any scripts that called &lt;code&gt;deployhq deploy&lt;/code&gt; to &lt;code&gt;dhq deploy -p &amp;lt;project&amp;gt; -s &amp;lt;server&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Once everything is working, &lt;code&gt;gem uninstall deployhq&lt;/code&gt; to remove the old tool.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The old &lt;code&gt;Deployfile&lt;/code&gt; JSON format is no longer required — &lt;code&gt;dhq&lt;/code&gt; reads project context from flags, environment variables, or your interactive auth state. This is a breaking change, but a small one: most users had a single &lt;code&gt;Deployfile&lt;/code&gt; per repo, and the new flags are easier to drop into Makefiles, scripts, and CI configs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Go Next
&lt;/h2&gt;

&lt;p&gt;The CLI is one piece of a larger story. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; is increasingly designed for both humans and agents — the &lt;a href="https://www.deployhq.com/agents" rel="noopener noreferrer"&gt;DeployHQ agents page&lt;/a&gt; walks through how the platform thinks about tool design for AI editors, and where we are heading next.&lt;/p&gt;

&lt;p&gt;If you do not have a &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; account yet, &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;start a free trial&lt;/a&gt; — install &lt;code&gt;dhq&lt;/code&gt;, point it at your first project, and you can run an &lt;a href="https://www.deployhq.com/features/automatic-deployments" rel="noopener noreferrer"&gt;automated deployment from your terminal&lt;/a&gt; inside ten minutes.&lt;/p&gt;




&lt;p&gt;Questions, edge cases, or feedback on the CLI? We would love to hear from you — drop us a line at &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or find us on &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;@deployhq&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>dhq</category>
      <category>launches</category>
      <category>newfeatures</category>
    </item>
    <item>
      <title>New DeployHQ integrations: Linear, Google Chat, Telegram, and signed webhooks</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Fri, 08 May 2026 13:35:35 +0000</pubDate>
      <link>https://dev.to/deployhq/new-deployhq-integrations-linear-google-chat-telegram-and-signed-webhooks-kp4</link>
      <guid>https://dev.to/deployhq/new-deployhq-integrations-linear-google-chat-telegram-and-signed-webhooks-kp4</guid>
      <description>&lt;p&gt;We've spent the last few weeks closing the gap between &lt;q&gt;the deploy finished&lt;/q&gt; and &lt;q&gt;the right people know it finished.&lt;/q&gt; That gap is where production incidents hide: someone shipped a hotfix at 4pm, the chat channel was muted, the PR reviewer assumed it was still in staging, and the customer-facing status page never updated.&lt;/p&gt;

&lt;p&gt;Five new integrations are now live, each filling a different version of that gap. One pushes status back into the place where engineers already review code. Three send alerts into the chat tools your team actually watches. The last one is a signed webhook for everything else — status pages, dashboards, CRMs, audit logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy status, posted back to GitHub, GitLab, and Bitbucket
&lt;/h2&gt;

&lt;p&gt;The new &lt;a href="https://www.deployhq.com/features/integrations/deployment-notifications" rel="noopener noreferrer"&gt;deployment notifications integration&lt;/a&gt; closes the most common visibility gap of all: a pull request shows the diff, the conversation, and the test results — but says nothing about whether the merge actually shipped, where it shipped to, or when. We covered why that gap matters in a &lt;a href="https://dev.to/deployhq/pr-radar-vs-github-notifications-vs-email-how-developers-actually-track-pull-requests-4dp5"&gt;comparison of how developers track pull requests&lt;/a&gt;, and the same logic applies after merge: reviewers shouldn't have to guess what happened to their code.&lt;/p&gt;

&lt;p&gt;Once enabled, every &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; deploy reports back to the originating Git provider. On GitHub, deploys appear under the Deployments tab and as commit statuses on PRs. On GitLab, they show up in the Environments view and on the merge request widget. On Bitbucket, they populate the native Deployments panel. You can map projects to environments — production, staging, QA, preview — so reviewers can see which commit landed where without leaving the code review.&lt;/p&gt;

&lt;p&gt;There's no separate auth dance: the integration reuses the repository connection you already configured, and the audit trail of every deploy lives where the rest of your code history lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Linear Release for every deploy, automatically
&lt;/h2&gt;

&lt;p&gt;Release tracking is one of those tasks that sits between engineering and product, gets assigned to nobody, and quietly atrophies. The &lt;a href="https://www.deployhq.com/features/integrations/linear" rel="noopener noreferrer"&gt;Linear integration&lt;/a&gt; takes it off the backlog: every time &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; finishes a deploy, a Linear Release lands in your chosen pipeline — already linked to the issues that shipped, based on the issue IDs in the commit messages between this deploy and the last one.&lt;/p&gt;

&lt;p&gt;The integration takes a single Linear API key and lets you route different environments to different release pipelines. Production deploys can populate your customer-visible release pipeline; staging deploys can populate an internal one. The integration only creates Releases — it doesn't move issues between states, so it composes cleanly with whatever workflow automation you already have. If you've thought about the difference between &lt;a href="https://dev.to/alex_morgan_2e6f2f637b128/continuous-delivery-vs-continuous-deployment-whats-the-difference-c6f-temp-slug-5636075"&gt;continuous delivery and continuous deployment&lt;/a&gt;, this is the seam where the two meet — automatic Releases let you keep continuous-delivery discipline without manual bookkeeping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Card-formatted alerts in Google Chat spaces
&lt;/h2&gt;

&lt;p&gt;For teams running on Google Workspace, the &lt;a href="https://www.deployhq.com/features/integrations/google-chat" rel="noopener noreferrer"&gt;Google Chat integration&lt;/a&gt; drops deploy notifications straight into your existing spaces as native cards. Project name, branch, environment, deployer, and status are all visible at a glance — no plain-text wall.&lt;/p&gt;

&lt;p&gt;Different projects can route to different spaces, or one project can broadcast to several. Useful pattern: send infra deploys to a quiet &lt;q&gt;deploys-firehose&lt;/q&gt; space and customer-facing app deploys to your main engineering space, so the noise lives somewhere you can mute and the signal stays where people are paying attention. This is the same pattern we recommended when we &lt;a href="https://www.deployhq.com/blog/enhanced-slack-notifications-for-deployments" rel="noopener noreferrer"&gt;enhanced our Slack notifications&lt;/a&gt;, now extended to Google Workspace teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Telegram alerts for the on-call rotation
&lt;/h2&gt;

&lt;p&gt;Slack and Discord aren't the right fit for everyone. International teams, contractor pools, and on-call rotations often live in &lt;a href="https://www.deployhq.com/features/integrations/telegram" rel="noopener noreferrer"&gt;Telegram&lt;/a&gt;, where notifications hit personal devices reliably without a paid seat or a workspace invite. (For teams already on Discord or Microsoft Teams, we have &lt;a href="https://www.deployhq.com/blog/send-deployment-notifications-to-a-discord-or-microsoft-teams-channel" rel="noopener noreferrer"&gt;a separate guide for routing deployment notifications there&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;The new integration uses the Telegram Bot API to push deploy alerts to any chat, group, or channel. Setup is three steps: create a bot through &lt;a class="mentioned-user" href="https://dev.to/botfather"&gt;@botfather&lt;/a&gt;, add it to your destination, and paste the token plus chat ID into &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;. There's no Telegram-side server to host — the bot infrastructure is Telegram's. A common deployment pattern: a private channel for production alerts that on-call engineers subscribe to, plus a group chat for the broader team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signed HTTP webhooks for everything else
&lt;/h2&gt;

&lt;p&gt;Pre-built integrations cover the common destinations, but there's always a long tail: a customer-facing status page, an internal compliance log, a CRM that needs to know when a customer's tenant deployed, a custom dashboard the platform team built last quarter. The &lt;a href="https://www.deployhq.com/features/integrations/http" rel="noopener noreferrer"&gt;HTTP webhook integration&lt;/a&gt; handles those.&lt;/p&gt;

&lt;p&gt;Every deploy event — start, success, failure — fires a signed POST to whatever URL you configure. Each request includes an &lt;code&gt;X-DeployHQ-Signature&lt;/code&gt; header containing an HMAC-SHA256 of the raw body, keyed by your shared secret, so the receiving service can verify the request actually came from &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; and wasn't replayed or forged. That matters as soon as the receiving endpoint is publicly reachable, which most webhook endpoints are.&lt;/p&gt;

&lt;p&gt;If you'd rather poll than receive, the same data is available through &lt;a href="https://dev.to/deployhq/using-deployhqs-api-automating-your-deployment-workflows-with-scripts-and-webhooks-2ldo"&gt;the&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; API for scripted deployment workflows. Common destinations we've seen people wire up: Statuspage and Atlassian Status, Datadog events, internal Postgres audit tables, ServiceNow tickets, custom Slack apps with richer formatting than the stock connector, and customer-success tools that surface deploy timing during support conversations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits
&lt;/h2&gt;

&lt;p&gt;Each of these reuses your existing &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project configuration — the same &lt;a href="https://www.deployhq.com/features/automatic-deployments" rel="noopener noreferrer"&gt;Git deployment automation&lt;/a&gt; you already have running picks up the new notification target on its next run. Multiple integrations can run in parallel: a single deploy can post a commit status to GitHub, create a Linear Release, drop a card into Google Chat, ping a Telegram channel, and fire a webhook to your status page.&lt;/p&gt;

&lt;p&gt;For a wider tour of what plugs into &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; — error trackers, monitoring tools, project management — the &lt;a href="https://www.deployhq.com/blog/deployments-with-deployhq-integrations" rel="noopener noreferrer"&gt;DeployHQ integrations roundup&lt;/a&gt; covers the broader picture, and the &lt;a href="https://www.deployhq.com/features/integrations" rel="noopener noreferrer"&gt;integrations index&lt;/a&gt; lists every pre-built connector. If your destination isn't there, the signed webhook covers it.&lt;/p&gt;

&lt;p&gt;If you're not deploying with &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; yet, &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;start a free trial&lt;/a&gt; and wire up your first integration in the dashboard — most teams have deploy notifications flowing inside ten minutes.&lt;/p&gt;




&lt;p&gt;Got questions or a destination you'd like us to integrate with natively? Reach us at &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;@deployhq&lt;/a&gt; on X.&lt;/p&gt;

</description>
      <category>launches</category>
      <category>linear</category>
      <category>googlechat</category>
      <category>telegram</category>
    </item>
    <item>
      <title>Deploy Hermes Agent on a VPS: DeployHQ Workflow + How It Differs from OpenClaw</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Wed, 06 May 2026 14:27:19 +0000</pubDate>
      <link>https://dev.to/deployhq/deploy-hermes-agent-on-a-vps-deployhq-workflow-how-it-differs-from-openclaw-445</link>
      <guid>https://dev.to/deployhq/deploy-hermes-agent-on-a-vps-deployhq-workflow-how-it-differs-from-openclaw-445</guid>
      <description>&lt;p&gt;If you read our &lt;a href="https://dev.to/theqadiariesforyou/how-to-deploy-and-configure-openclaw-on-a-vps-4h8e-temp-slug-2543902"&gt;OpenClaw VPS deployment guide&lt;/a&gt; and walked away with a thin self-hosted gateway between your messaging apps and an LLM, you got exactly what OpenClaw is designed to be. Hermes Agent — Nous Research's open-source agent that hit v0.12 last week — sits in adjacent territory but answers a different question. Where OpenClaw asks &lt;em&gt;how do I route messages to a model?&lt;/em&gt;, Hermes asks &lt;em&gt;how do I run an agent that gets better at its job over time?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For agencies, that distinction matters. One Hermes instance per client, deployed to a small VPS, can accumulate skills, remember context across sessions, and handle scheduled work without you babysitting it. It fits squarely into the broader pattern we've been writing about — &lt;a href="https://dev.to/deployhq/agentic-workflows-explained-how-ai-agents-are-changing-cicd-pipelines-nm0-temp-slug-2291085"&gt;autonomous AI agents reshaping deployment workflows&lt;/a&gt; — except the agent lives on infrastructure you control rather than a third-party SaaS. The catch: you need a way to keep the configuration, persona files, and skill bundles in sync across however many client servers you run. That's where &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; comes in. This post covers what Hermes Agent actually is, how it differs from OpenClaw, and how to deploy it on a VPS with a repeatable Git-driven workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Hermes Agent does
&lt;/h2&gt;

&lt;p&gt;Hermes Agent is a Python project under MIT license that runs as a long-lived process on whatever infrastructure you give it. The architecture has three pieces worth understanding before you deploy it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A skills system that grows.&lt;/strong&gt; Skills are procedural memory the agent writes for itself. When Hermes solves a task — say, generating a weekly client report from your analytics — it can save that workflow as a skill and refine it on future runs. Skills are portable JSON-and-prompt artifacts you can share between agents via the agentskills.io hub, which means an agency running ten Hermes instances can develop a skill once and propagate it to every client. If you've already worked with &lt;a href="https://dev.to/deployhq/openclaw-skills-how-to-find-install-and-build-your-own-1f5a-temp-slug-1058983"&gt;OpenClaw's skills system&lt;/a&gt;, the conceptual model is similar — Hermes just adds the autonomous self-curation layer on top.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persistent cross-session memory.&lt;/strong&gt; Hermes uses the Honcho framework for user modeling and LLM-summarized recall. Conversations from yesterday inform how it responds today. The agent also performs periodic memory nudges — compacting and reorganizing what it knows so the context window doesn't bloat indefinitely. None of this works without a persistent volume on disk; we'll come back to that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-channel messaging from one process.&lt;/strong&gt; Out of the box: Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Mattermost, Email, and SMS, plus a TUI for direct terminal interaction. The agent runs once and exposes itself everywhere.&lt;/p&gt;

&lt;p&gt;On top of these, Hermes is model-agnostic — Nous Portal, OpenRouter (200+ models), OpenAI, NVIDIA NIM, or any custom endpoint — and supports six terminal backends including Docker, SSH, Daytona, and Modal for distributed or serverless execution. A &lt;code&gt;SOUL.md&lt;/code&gt; file in the repo defines the agent's personality and operating constraints, which is the file you'll customize per client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hermes Agent vs OpenClaw: when to pick which
&lt;/h2&gt;

&lt;p&gt;Both projects target the same broad audience (developers who want a self-hosted alternative to ChatGPT subscriptions) but the runtime profile is genuinely different.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;OpenClaw&lt;/th&gt;
&lt;th&gt;Hermes Agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Process model&lt;/td&gt;
&lt;td&gt;Single Node process&lt;/td&gt;
&lt;td&gt;Containerised app + sidecar services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State&lt;/td&gt;
&lt;td&gt;Stateless gateway&lt;/td&gt;
&lt;td&gt;Persistent skills + memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Min recommended VPS&lt;/td&gt;
&lt;td&gt;2GB / 1 vCPU&lt;/td&gt;
&lt;td&gt;8GB / 2 vCPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in tools&lt;/td&gt;
&lt;td&gt;None — bring your own&lt;/td&gt;
&lt;td&gt;40+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduled tasks&lt;/td&gt;
&lt;td&gt;External (cron)&lt;/td&gt;
&lt;td&gt;Built-in scheduler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-channel&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (15+ channels)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Thin chatbot front, BYO intelligence&lt;/td&gt;
&lt;td&gt;Autonomous agents that learn&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trade-off lines up neatly. OpenClaw is leaner — a 4GB VPS comfortably runs OpenClaw alongside other services, and you keep your model logic, memory, and tooling in your own application code. Hermes is heavier — Docker-friendly, with more state to persist and back up, but you get the learning loop, scheduler, and tool catalogue without writing them yourself.&lt;/p&gt;

&lt;p&gt;For most agency scenarios — &lt;em&gt;&lt;q&gt;I want to give each client a smart assistant that learns their business&lt;/q&gt;&lt;/em&gt; — Hermes wins. For projects where the agent is one component in a larger application you're already building, OpenClaw stays out of the way. If you've already deployed OpenClaw and want to keep it, Hermes can run alongside on the same VPS provided you have the RAM headroom. And if your real need is a chat front-end for self-hosted models rather than an autonomous agent, our &lt;a href="https://www.deployhq.com/blog/deploying-librechat-and-ollama-on-a-vps-automated-setup-and-configuration" rel="noopener noreferrer"&gt;LibreChat and Ollama VPS deploy guide&lt;/a&gt; covers a third path that doesn't pretend to be either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pre-flight checklist
&lt;/h2&gt;

&lt;p&gt;Before touching the server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A VPS with &lt;strong&gt;Ubuntu 22.04 LTS or Debian 12&lt;/strong&gt; , &lt;strong&gt;8GB RAM&lt;/strong&gt; , &lt;strong&gt;2 vCPU&lt;/strong&gt; , and at least &lt;strong&gt;20GB SSD&lt;/strong&gt;. Hetzner CPX21, DigitalOcean's $24 droplet, or equivalent.&lt;/li&gt;
&lt;li&gt;A domain name pointed at the VPS (Hermes itself doesn't need HTTPS, but messaging webhooks for Telegram and Slack do).&lt;/li&gt;
&lt;li&gt;An API key for at least one model provider. OpenRouter is the most flexible because you can swap models without redeploying.&lt;/li&gt;
&lt;li&gt;A Git repository (GitHub, GitLab, or Bitbucket) for your Hermes configuration. Don't put &lt;code&gt;.env&lt;/code&gt; files with real secrets in the repo — we'll handle those via &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; environment variables.&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; account. If you don't have one, &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;sign up&lt;/a&gt; — the free tier covers a single project and is enough to follow along.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1 — Server hardening
&lt;/h2&gt;

&lt;p&gt;SSH in as root, create a non-root user, and lock the basics down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adduser hermes
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;hermes
ufw allow 22/tcp
ufw allow 443/tcp
ufw &lt;span class="nb"&gt;enable&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disable root SSH login and password auth in &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;, then reload. None of this is Hermes-specific — it's the baseline you should run on any server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Install Docker
&lt;/h2&gt;

&lt;p&gt;Hermes ships a &lt;code&gt;docker-compose.yml&lt;/code&gt; and that's the path we recommend for production. We've covered the &lt;a href="https://www.deployhq.com/blog/docker-container-deployment-with-deployhq" rel="noopener noreferrer"&gt;Docker container deployment pattern with DeployHQ&lt;/a&gt; in more depth before — the same pipeline applies here. Install Docker Engine and the Compose plugin from the official Docker repo (don't use Ubuntu's bundled &lt;code&gt;docker.io&lt;/code&gt; package — it lags behind):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.docker.com | sh
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker hermes

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log out and back in as &lt;code&gt;hermes&lt;/code&gt; so the group change takes effect. Verify with &lt;code&gt;docker compose version&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Initial Hermes Agent install
&lt;/h2&gt;

&lt;p&gt;Clone the repo and prepare the configuration:&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="nb"&gt;cd&lt;/span&gt; /home/hermes
git clone https://github.com/NousResearch/hermes-agent.git
&lt;span class="nb"&gt;cd &lt;/span&gt;hermes-agent
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="nb"&gt;cp &lt;/span&gt;cli-config.yaml.example cli-config.yaml

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit &lt;code&gt;.env&lt;/code&gt; with your model provider key and any messaging tokens (Telegram bot token, Slack signing secret, etc.). Edit &lt;code&gt;SOUL.md&lt;/code&gt; to define the agent's persona — for an agency client this might be &lt;em&gt;&lt;q&gt;You are a senior project manager for ClientName. You answer questions about their deployments, surface blocked items from the standup channel, and never speculate when you don't have data.&lt;/q&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Bring it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch the logs until you see the agent connect to your messaging channels. From here, the agent is alive — but every config change still requires SSH, which is exactly the operational footgun &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; removes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Move configuration into Git
&lt;/h2&gt;

&lt;p&gt;Create a Git repo with this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hermes-config/
├── SOUL.md
├── cli-config.yaml
├── skills/
│ ├── weekly-report.json
│ └── deploy-status-check.json
├── docker-compose.override.yml
└── deploy.sh

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what's &lt;em&gt;not&lt;/em&gt; in there: &lt;code&gt;.env&lt;/code&gt;. Secrets stay in &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; as environment variables and get written to disk during deploy. The &lt;code&gt;docker-compose.override.yml&lt;/code&gt; lets you set production-specific values (volumes, restart policies, resource limits) without touching the upstream &lt;code&gt;docker-compose.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Push this to GitHub or GitLab. This is the repo &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; will track.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — DeployHQ project setup
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, create a new project and connect it to your config repo via &lt;a href="https://www.deployhq.com/deploy-from-github" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; or GitLab. Add your VPS as a server with the &lt;code&gt;hermes&lt;/code&gt; user and SSH key.&lt;/p&gt;

&lt;p&gt;Set the deployment path to &lt;code&gt;/home/hermes/hermes-config&lt;/code&gt;. Add your secrets as &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; environment variables — &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; will write them as a &lt;code&gt;.env&lt;/code&gt; file at deploy time:&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Build Pipeline&lt;/strong&gt; (or as a deploy command), add:&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="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
OPENROUTER_API_KEY=&lt;/span&gt;&lt;span class="nv"&gt;$OPENROUTER_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;
TELEGRAM_BOT_TOKEN=&lt;/span&gt;&lt;span class="nv"&gt;$TELEGRAM_BOT_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;
SLACK_SIGNING_SECRET=&lt;/span&gt;&lt;span class="nv"&gt;$SLACK_SIGNING_SECRET&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add a post-deployment SSH command that copies the config into place and restarts the container:&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="nb"&gt;cd&lt;/span&gt; /home/hermes/hermes-config
&lt;span class="nb"&gt;cp &lt;/span&gt;SOUL.md cli-config.yaml /home/hermes/hermes-agent/
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; skills/ /home/hermes/hermes-agent/
&lt;span class="nb"&gt;cd&lt;/span&gt; /home/hermes/hermes-agent
docker compose pull
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire up the auto-deploy webhook from your Git host. Now &lt;code&gt;git push&lt;/code&gt; triggers a deploy: &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; writes the latest config, pulls any updated container image, and restarts Hermes — no SSH session required. This is the &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;build pipeline&lt;/a&gt; pattern that makes the whole thing reproducible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 — Persistent data (this is the one most people miss)
&lt;/h2&gt;

&lt;p&gt;Hermes' learning is worthless if it doesn't survive a container restart. In your &lt;code&gt;docker-compose.override.yml&lt;/code&gt;:&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;hermes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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;./data:/app/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./memory:/app/memory&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./skills:/app/skills&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back the &lt;code&gt;data/&lt;/code&gt; and &lt;code&gt;memory/&lt;/code&gt; directories up daily. &lt;code&gt;restic&lt;/code&gt; to a Backblaze B2 bucket is the cheapest reliable option — about $0.005/GB/month. If your agent has been running for six months and you lose its memory, you've lost six months of accumulated context, which for a client-facing agent is genuinely painful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agency multi-tenant pattern
&lt;/h2&gt;

&lt;p&gt;Here's where this stops being a single-server tutorial and starts paying for itself.&lt;/p&gt;

&lt;p&gt;For an agency running Hermes for ten clients, you don't want ten copies of the same &lt;code&gt;docker-compose.yml&lt;/code&gt; drifting apart. The pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One Git repo per client&lt;/strong&gt; — different &lt;code&gt;SOUL.md&lt;/code&gt;, different model provider keys, different skills loadout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project per VPS&lt;/strong&gt; — each pointed at its client's config repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One shared &lt;code&gt;skills/&lt;/code&gt; Git submodule&lt;/strong&gt; — common skills (calendar lookup, time-tracking exports, deployment status) developed once, pulled into every client's repo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you build a new skill for one client and it generalises, push it to the shared submodule, bump the submodule pointer in the other clients' repos, and &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; rolls it out everywhere. No SSH-and-rsync rituals; no risk of one server lagging six months behind. This is the same operational pattern we documented for &lt;a href="https://www.deployhq.com/blog/self-host-vaultwarden-vps-docker-deployhq" rel="noopener noreferrer"&gt;self-hosting Vaultwarden across client VPS infrastructure&lt;/a&gt; — once you have it dialled in for one self-hosted service, every additional service plugs into the same pipeline. It's also why agencies tend to prefer &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; over hand-rolled CI scripts when they get past three or four servers — the &lt;a href="https://www.deployhq.com/for-agencies" rel="noopener noreferrer"&gt;for agencies&lt;/a&gt; plan exists for exactly this kind of fan-out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memory directory not mounted as a volume&lt;/strong&gt; — every container restart wipes the agent's memory. Test by killing the container deliberately and confirming sessions persist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting &lt;code&gt;SOUL.md&lt;/code&gt;&lt;/strong&gt; — Hermes will run with a generic persona, which is fine for testing but not what you want for a client deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free tier model rate limits&lt;/strong&gt; — burst usage hits caps fast. OpenRouter's pay-as-you-go is the safety net.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WhatsApp/Signal bridges aren't built in&lt;/strong&gt; — they require separate bridge processes. The Telegram and Discord integrations are the easiest starting points.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.env&lt;/code&gt; accidentally committed&lt;/strong&gt; — use a pre-commit hook or DeployHQ's environment variables exclusively. Rotate any key that touched a public repo.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Hermes Agent is a heavier commitment than OpenClaw, but the autonomous skill curation and persistent memory are real differentiators if you want an agent that gets more useful over time rather than starting from zero on every conversation. For agencies running multiple clients, the Git-driven config pattern through &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; is what makes ten Hermes instances tractable instead of ten independent operational headaches.&lt;/p&gt;

&lt;p&gt;Start with one client repo before fanning out, watch how the agent's skills evolve over the first month, and only then start replicating the pattern across the rest of your client base. The Git-driven config workflow scales linearly; the operational complexity doesn't.&lt;/p&gt;




&lt;p&gt;Got questions? Reach us at &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or on Twitter at &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;@deployhq&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>hermes</category>
      <category>tutorials</category>
      <category>vps</category>
    </item>
    <item>
      <title>6 free GitHub repos that cut your Claude Code token bill</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Mon, 04 May 2026 05:56:24 +0000</pubDate>
      <link>https://dev.to/deployhq/6-free-github-repos-that-cut-your-claude-code-token-bill-4emn</link>
      <guid>https://dev.to/deployhq/6-free-github-repos-that-cut-your-claude-code-token-bill-4emn</guid>
      <description>&lt;p&gt;If you use Claude Code seriously, your token budget is the new bottleneck. A long session blows past the context window, an unfiltered &lt;code&gt;npm install&lt;/code&gt; dumps 4,000 lines into Claude's view, and the Max plan suddenly feels less generous. The good news: the open-source community has been quietly shipping repos that solve exactly this — compressing terminal output before it reaches the model, building knowledge graphs so Claude reads only what matters, and giving you a real-time view of where your tokens go.&lt;/p&gt;

&lt;p&gt;Here are six free GitHub repos worth installing this week. Four of them cut your bill directly. Two help you see where the money is going so you can act on it. For a Max-plan user pushing daily, the combined effect can save &lt;strong&gt;$100+ a month&lt;/strong&gt;. For a Pro user, you mostly get longer sessions and faster context — still worth the install.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cut your token bill
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. rtk — compress terminal output before it hits Claude
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/rtk-ai/rtk" rel="noopener noreferrer"&gt;&lt;strong&gt;rtk&lt;/strong&gt;&lt;/a&gt; is a Rust CLI proxy that filters and compresses command output before it ever reaches your AI assistant's context. A &lt;code&gt;git status&lt;/code&gt; shrinks from 119 characters to 28. A &lt;code&gt;cargo test&lt;/code&gt; collapses from 155 lines to 3. An &lt;code&gt;npm install&lt;/code&gt; drops from 4,000 lines to about 15. Across common dev commands, the maintainers report 60-90% token reduction.&lt;/p&gt;

&lt;p&gt;The trick is straightforward: instead of running &lt;code&gt;git status&lt;/code&gt; directly, Claude runs &lt;code&gt;rtk git status&lt;/code&gt;, and only the meaningful diff hits the context window. A Claude Code hook can rewrite Bash commands transparently, so you don't have to retrain your muscle memory.&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;# macOS / Linux&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;rtk

&lt;span class="c"&gt;# Or with cargo&lt;/span&gt;
cargo &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--git&lt;/span&gt; https://github.com/rtk-ai/rtk

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Single Rust binary, zero dependencies, MIT licensed. If you only install one tool from this list, install this one.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. caveman — make Claude talk like a caveman
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/JuliusBrussee/caveman" rel="noopener noreferrer"&gt;&lt;strong&gt;caveman&lt;/strong&gt;&lt;/a&gt; is a Claude Code skill that rewrites the model's own output to be terse. The spec is exactly what it sounds like: respond like a smart caveman. Cut articles, pleasantries, and filler. Keep all technical substance. Code blocks remain unchanged. Error messages stay quoted exactly. Technical terms stay intact.&lt;/p&gt;

&lt;p&gt;The result reads weird at first — &lt;q&gt;fix bug, line 42, null check missing, add &lt;code&gt;if user&lt;/code&gt;&lt;/q&gt; — but it cuts roughly 75% of output tokens, and once you adjust, you stop noticing. The skill detects 30+ agents, including Claude Code, Codex, Cursor, Windsurf, Cline, and Aider.&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;# Claude Code (recommended)&lt;/span&gt;
claude plugin marketplace add JuliusBrussee/caveman &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; claude plugin &lt;span class="nb"&gt;install &lt;/span&gt;caveman@caveman

&lt;span class="c"&gt;# Universal fallback&lt;/span&gt;
npx skills add JuliusBrussee/caveman

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If verbose Claude prose is what's chewing through your daily limit, this is the fastest fix on the list.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. code-review-graph — let Claude read only what matters
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/tirth8205/code-review-graph" rel="noopener noreferrer"&gt;&lt;strong&gt;code-review-graph&lt;/strong&gt;&lt;/a&gt; tackles a different problem: when Claude has to review a PR or work in a large codebase, it ends up reading half the repo to understand the call graph. This tool parses your repository into an AST with Tree-sitter, stores it as a graph of nodes (functions, classes, imports) and edges (calls, inheritance, test coverage), and at review time computes the minimal set of files Claude actually needs to read.&lt;/p&gt;

&lt;p&gt;The published benchmarks show 6.8× fewer tokens on code reviews and up to 49× reduction on daily coding tasks in a Next.js monorepo. Average across six real open-source repos was 8.2×. The initial graph build takes around ten seconds for a 500-file project, and it auto-updates on every file edit and git commit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;code-review-graph
code-review-graph &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;install&lt;/code&gt; command auto-detects which AI tools you have and writes the right MCP configuration for each. After installing, restart your editor and ask Claude to build the graph for your project. Worth a closer look in &lt;a href="https://dev.to/emperorakashi20/how-code-review-graph-cuts-claude-code-token-usage-by-49x-and-whether-its-actually-worth-it-4kn1"&gt;this DEV post&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. agent-browser — stop re-authenticating every session
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/vercel-labs/agent-browser" rel="noopener noreferrer"&gt;&lt;strong&gt;agent-browser&lt;/strong&gt;&lt;/a&gt; is a browser automation CLI for AI agents, built by Vercel Labs. It's not a token compressor in the same league as rtk or caveman, but it solves a workflow that wastes more tokens than people realize: re-running the auth flow every time Claude needs to inspect a deployed site, a staging environment, or a third-party dashboard.&lt;/p&gt;

&lt;p&gt;Save the auth state once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agent-browser &lt;span class="nt"&gt;--auto-connect&lt;/span&gt; state save ./my-auth.json

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reuse it across every future session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agent-browser &lt;span class="nt"&gt;--state&lt;/span&gt; ./my-auth.json open https://app.example.com/dashboard

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it to a Claude Code workflow that drives a browser to verify a &lt;a href="https://dev.to/deployhq/effortless-automation-prompting-your-way-to-deployment-with-claude-code-and-deployhq-3cli"&gt;DeployHQ deploy&lt;/a&gt;, check a staging URL, or scrape a logged-in dashboard, and you stop burning tokens on the same login dance over and over.&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;# Global install&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; agent-browser
agent-browser &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Or as a Claude Code skill&lt;/span&gt;
npx skills add vercel-labs/agent-browser

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  See where your tokens go
&lt;/h2&gt;

&lt;p&gt;The next two repos don't reduce your usage — they tell you what your usage actually is. That sounds boring until you realise most Claude Code users have no idea which sessions cost them the most or how close they are to a hard limit until the warning hits mid-task.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. claude-usage — a local dashboard for tokens, costs, and history
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/phuryn/claude-usage" rel="noopener noreferrer"&gt;&lt;strong&gt;claude-usage&lt;/strong&gt;&lt;/a&gt; is a single-page dashboard that runs on &lt;code&gt;localhost:8080&lt;/code&gt;, reads your Claude Code JSONL logs locally, and renders Chart.js views of token usage, costs, and session history. It auto-refreshes every 30 seconds and supports model filtering, so you can see at a glance whether your costs are coming from Sonnet runs, Opus reasoning, or cache reads.&lt;/p&gt;

&lt;p&gt;It uses only the Python standard library — &lt;code&gt;sqlite3&lt;/code&gt;, &lt;code&gt;http.server&lt;/code&gt;, &lt;code&gt;json&lt;/code&gt;, &lt;code&gt;pathlib&lt;/code&gt; — so there's no pip install, no virtualenv to manage, no dependency updates to track. Clone, run, look.&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/phuryn/claude-usage
&lt;span class="nb"&gt;cd &lt;/span&gt;claude-usage
python3 cli.py dashboard

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're on a Pro or Max plan, the dashboard adds a progress bar showing where you are against the limit. That single visual changed how a few of us pace work — instead of &lt;q&gt;the model started warning me at 4pm&lt;/q&gt;, you see the burn rate building all morning.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. claude-usage-monitor — real-time terminal predictions
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor" rel="noopener noreferrer"&gt;&lt;strong&gt;claude-usage-monitor&lt;/strong&gt;&lt;/a&gt; is the other half of the visibility story. Where claude-usage is a browser dashboard, this is a terminal-resident monitor with live progress bars, current session data, burn-rate analysis, and — the real value — predictions about when you'll hit your session limit based on a 90th-percentile analysis of your last eight days of usage.&lt;/p&gt;

&lt;p&gt;It auto-detects your plan, switches modes when you cross thresholds, and shows you cost projections in real time. For a Max-plan user running multiple agents in parallel, it's the difference between &lt;q&gt;session ends mid-refactor with no warning&lt;/q&gt; and &lt;q&gt;you have ~14 minutes of headroom at current burn&lt;/q&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;# Recommended (uv)&lt;/span&gt;
uv tool &lt;span class="nb"&gt;install &lt;/span&gt;claude-monitor

&lt;span class="c"&gt;# Or pip&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;claude-monitor

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs in a side terminal pane and gets out of the way. After a week, you start pacing your day around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How these stack
&lt;/h2&gt;

&lt;p&gt;If you only install one, install rtk — terminal output is the cheapest and biggest source of wasted tokens. If you install two, add code-review-graph; for review-heavy workflows it's a step-change, not a marginal improvement. Caveman is the most polarising of the four cost-cutters, but devs who stick with it for a week never go back.&lt;/p&gt;

&lt;p&gt;The two visibility tools are complementary, not redundant. claude-usage is the analytical view you check at end of day to understand spend. claude-usage-monitor is the heads-up display you keep open while working. Run both for a week and you'll have a much sharper sense of which prompting habits are expensive and which aren't.&lt;/p&gt;

&lt;p&gt;A cleaner token budget also frees up runway for ambitious workflows that previously hit the wall — like driving Claude through a &lt;a href="https://dev.to/deployhq/effortless-automation-prompting-your-way-to-deployment-with-claude-code-and-deployhq-3cli"&gt;full deploy automation pipeline&lt;/a&gt;, &lt;a href="https://dev.to/deployhq/how-to-generate-sql-queries-with-ai-step-by-step-guide-using-claude-code-and-dbhub-422f"&gt;generating production SQL queries&lt;/a&gt;, or &lt;a href="https://dev.to/deployhq/comparing-claude-code-openai-codex-and-google-gemini-cli-which-ai-coding-assistant-is-right-for-1ffd"&gt;comparing AI CLIs&lt;/a&gt; on real codebase tasks. If you're new to Claude Code itself, start with our &lt;a href="https://dev.to/deployhq/getting-started-with-claude-code-the-ai-coding-assistant-for-your-terminal-4cba"&gt;terminal AI assistant primer&lt;/a&gt;. If your sessions are spawning noisy commits, our &lt;a href="https://dev.to/deployhq/how-to-use-git-with-claude-code-understanding-the-co-authored-by-attribution-3boi"&gt;Co-Authored-By guide&lt;/a&gt; shows how to keep history clean. And to keep the model grounded in real, current docs instead of hallucinated APIs, &lt;a href="https://dev.to/deployhq/context7-guide-stop-ai-hallucinations-with-live-docs-1330"&gt;layer in Context7&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A word on the &lt;q&gt;$100/mo&lt;/q&gt; claim
&lt;/h2&gt;

&lt;p&gt;Token-savings posts attract clickbait math. The honest version: rtk's 60-90% reduction on terminal commands and code-review-graph's 6.8-49× reduction on review tasks are real, but they apply to specific kinds of work. If you're a Max-plan user running multi-hour sessions on large codebases — the kind of user who hits the weekly limit before Wednesday — installing rtk and code-review-graph alone will plausibly save you a tier of overage or upgrade pressure each month. That's where the $100 number comes from.&lt;/p&gt;

&lt;p&gt;If you're a Pro user occasionally asking Claude to write a unit test, you'll see longer sessions and faster context loading, but you won't recover real cash because you weren't spending it in the first place. Either way, all six repos are free, MIT or similarly permissive, and take under five minutes each to install. The downside is small. Try the four cost-cutters for a week, then add the two monitors and let the data tell you what stuck.&lt;/p&gt;




&lt;p&gt;Questions or want to share which combo worked for you? Email &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or hit us up on &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;@deployhq&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>github</category>
      <category>free</category>
    </item>
    <item>
      <title>PR Radar vs GitHub Notifications vs Email: How Developers Actually Track Pull Requests</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Fri, 24 Apr 2026 12:32:48 +0000</pubDate>
      <link>https://dev.to/deployhq/pr-radar-vs-github-notifications-vs-email-how-developers-actually-track-pull-requests-4dp5</link>
      <guid>https://dev.to/deployhq/pr-radar-vs-github-notifications-vs-email-how-developers-actually-track-pull-requests-4dp5</guid>
      <description>&lt;p&gt;If you've managed more than a handful of pull requests across more than one Git platform, you've probably built your own little tracking system out of whatever was free and already installed. A few browser tabs. Email filters. Maybe a Slack integration that someone turned on two years ago and nobody can remember the auth for. It works, until it doesn't — until the PR that needed the &lt;q&gt;please merge today&lt;/q&gt; comment gets buried under twelve identical CI notifications, or until the GitLab side of your migration stays invisible for three hours because you only had the GitHub tab open.&lt;/p&gt;

&lt;p&gt;This is a comparison of the ways developers actually track PRs today — including &lt;a href="https://www.deployhq.com/features/pr-radar" rel="noopener noreferrer"&gt;PR Radar&lt;/a&gt;, the open-source Chrome extension we built because none of the existing options did what we wanted. We'll be honest about where each one wins and where it falls short, and we'll end with the one question that decides which tool you actually need. It's the same format we used for our &lt;a href="https://www.deployhq.com/blog/choosing-the-right-package-manager-npm-vs-yarn-vs-pnpm-vs-bun" rel="noopener noreferrer"&gt;package manager benchmark&lt;/a&gt; and our &lt;a href="https://dev.to/deployhq/mailtrap-vs-sendgrid-vs-mailgun-best-email-api-for-nodejs-in-2026-1m0-temp-slug-4999684"&gt;transactional email API comparison&lt;/a&gt; — practical, developer-to-developer, and honest about the gaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five options on the table
&lt;/h2&gt;

&lt;p&gt;Before the comparison, here's the shortlist. We've stayed focused on tools that &lt;strong&gt;monitor PR status&lt;/strong&gt; rather than tools that try to change your PR workflow (so Graphite, Aviator, and Reviewpad are out — they solve a different problem).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Email notifications&lt;/strong&gt; — the default for GitHub, GitLab, and Bitbucket&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub's built-in notification inbox&lt;/strong&gt; — the bell icon, the &lt;code&gt;/notifications&lt;/code&gt; page&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack integrations&lt;/strong&gt; — GitHub for Slack, GitLab for Slack, Bitbucket's bot&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub-specific extensions&lt;/strong&gt; — Refined GitHub, Notifier for GitHub&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR Radar&lt;/strong&gt; — multi-platform dashboard in the browser toolbar&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Option 1: Email — the default everyone ignores
&lt;/h2&gt;

&lt;p&gt;Every Git host turns on email notifications by default. Every developer turns most of them off within a month.&lt;/p&gt;

&lt;p&gt;The problem with email isn't that it's a bad channel. It's that PR events are high-frequency and low-information per message. A typical active PR generates email for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The initial open&lt;/li&gt;
&lt;li&gt;Every review comment&lt;/li&gt;
&lt;li&gt;Every reply in a thread&lt;/li&gt;
&lt;li&gt;Every push that re-triggers CI&lt;/li&gt;
&lt;li&gt;Every CI status change&lt;/li&gt;
&lt;li&gt;Merge or close&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Multiply by 8–12 active PRs in a normal week and you've built a firehose. The signal-to-noise ratio collapses because every email looks roughly the same in the preview pane: a PR title, a repo name, and one line of context. You cannot tell at a glance which of your fifteen unread messages is &lt;q&gt;CI failed on the PR that's blocking release&lt;/q&gt; and which is &lt;q&gt;approving nit on a docs typo.&lt;/q&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where email wins:&lt;/strong&gt; Audit trail. If you need a durable record of what happened and when, email is it. Good for compliance, useless for real-time awareness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it fails:&lt;/strong&gt; Real-time awareness, cross-platform visibility, CI status at a glance.&lt;/p&gt;




&lt;h2&gt;
  
  
  Option 2: GitHub's notification inbox
&lt;/h2&gt;

&lt;p&gt;GitHub's inbox (the bell icon and &lt;code&gt;/notifications&lt;/code&gt;) is better than email for one reason: it's scoped and threaded. A single PR becomes one inbox entry that updates in place instead of twenty separate emails.&lt;/p&gt;

&lt;p&gt;It has genuine improvements over the last couple of years — filters, custom views, &lt;q&gt;Participating&lt;/q&gt; vs &lt;q&gt;All,&lt;/q&gt; per-repo mute. If you live entirely inside GitHub, the inbox is solid.&lt;/p&gt;

&lt;p&gt;But it has three hard ceilings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's GitHub-only.&lt;/strong&gt; If your team uses GitLab for infra and GitHub for apps, the inbox solves half your problem. The other half you track… somehow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It doesn't show CI status inline.&lt;/strong&gt; The inbox will tell you &lt;q&gt;CI ran on this PR.&lt;/q&gt; It will not tell you whether it passed or failed without a click into the PR page. For a tool whose primary job is telling you what needs your attention, burying the pass/fail state behind a click is a miss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It needs a tab open.&lt;/strong&gt; The inbox icon lives inside &lt;code&gt;github.com&lt;/code&gt;. If you closed the tab during a focus block, you find out about a failed deploy the next time you cmd-T your way back. Browser desktop notifications exist but are &lt;a href="https://docs.github.com/en/account-and-profile/managing-subscriptions-and-notifications-on-github/setting-up-notifications/configuring-notifications" rel="noopener noreferrer"&gt;disabled by default&lt;/a&gt; and, even when on, only fire when a GitHub tab is already open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it wins:&lt;/strong&gt; Native, reliable, no extra auth, granular filters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it fails:&lt;/strong&gt; GitHub-only, no CI status in the list view, tab-bound.&lt;/p&gt;




&lt;h2&gt;
  
  
  Option 3: Slack integrations
&lt;/h2&gt;

&lt;p&gt;Slack is where most engineering teams already are, so routing PR events into Slack seems obvious. And for a few specific use cases — a dedicated &lt;code&gt;#deploys&lt;/code&gt; channel, a release-critical PR that the whole team is watching — it's the right tool.&lt;/p&gt;

&lt;p&gt;For everyday PR tracking, Slack is a noise amplifier. A rebase-heavy afternoon on a single PR can push twenty notifications into a channel. The team stops reading them. Important signals (&lt;q&gt;production deploy failed&lt;/q&gt;) end up in the same channel as unimportant ones (&lt;q&gt;renovate bumped a patch version&lt;/q&gt;), with the same visual weight, both probably muted by Thursday.&lt;/p&gt;

&lt;p&gt;There's also a coverage asymmetry. GitHub for Slack is mature. GitLab for Slack is functional. Bitbucket's Slack integration exists but feels like an afterthought. If you've got repos on all three, Slack gives you an uneven view of each one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it wins:&lt;/strong&gt; Team visibility on critical events, shared context during an incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it fails:&lt;/strong&gt; Personal PR tracking, signal-to-noise, multi-platform parity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Option 4: Refined GitHub and Notifier for GitHub
&lt;/h2&gt;

&lt;p&gt;If you've looked for browser extensions for this before, you've hit two classic options.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/refined-github/refined-github" rel="noopener noreferrer"&gt;Refined GitHub&lt;/a&gt; is excellent — dozens of polish-level improvements to &lt;code&gt;github.com&lt;/code&gt; pages. But it enhances GitHub; it doesn't pull PR status &lt;em&gt;out&lt;/em&gt; of GitHub into your toolbar. You still need to open the tab.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Notifier for GitHub&lt;/strong&gt; does put a badge in your toolbar, but the badge counts unread notifications — not CI state, not review state. A red badge tells you &lt;q&gt;there's something&lt;/q&gt; and nothing else. You click through to find out what.&lt;/p&gt;

&lt;p&gt;Both are GitHub-only, both assume GitHub is your whole world, and neither addresses the core visibility problem if your work spans platforms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where they win:&lt;/strong&gt; Free, lightweight, GitHub UX polish (Refined GitHub in particular is a staple).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where they fail:&lt;/strong&gt; Single-platform, no CI awareness in Notifier, no toolbar summary in Refined.&lt;/p&gt;




&lt;h2&gt;
  
  
  Option 5: PR Radar
&lt;/h2&gt;

&lt;p&gt;PR Radar is the extension we built after rotating through all four options above and still ending up with a browser window full of tabs. It's a Chrome extension (Firefox and Edge in progress), MIT-licensed, open source, and free — no paid tier, no accounts, no backend at all. Links to the store listing and repo are at the end of the post.&lt;/p&gt;

&lt;p&gt;The short version of what it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One toolbar popup&lt;/strong&gt; with every open PR across GitHub, GitLab, and Bitbucket&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Badge on the toolbar icon&lt;/strong&gt; with a live pass / fail / running count for CI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unresolved comment count&lt;/strong&gt; per PR, pulled via GraphQL so the number is actually correct&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment status inline&lt;/strong&gt; with clickable environment URLs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desktop notifications and sound alerts&lt;/strong&gt; when CI finishes (no tab required — it polls in a service worker)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-click merge&lt;/strong&gt; from the dashboard, across all three platforms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale PR dimming&lt;/strong&gt; with a configurable threshold so your attention goes to the active ones&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard shortcuts&lt;/strong&gt; — &lt;code&gt;j&lt;/code&gt;/&lt;code&gt;k&lt;/code&gt; to move between PRs, &lt;code&gt;o&lt;/code&gt; to open, &lt;code&gt;/&lt;/code&gt; to search, &lt;code&gt;?&lt;/code&gt; for the full list&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design goal was minimum cognitive load: glance at the toolbar, see whether anything needs you, go back to what you were doing. If the badge is green, there is nothing to do. If it's red, you know exactly where to look. The same mindset drove our list of &lt;a href="https://dev.to/deployhq/6-developer-clis-that-ai-coding-agents-actually-use-well-5973-temp-slug-2135258"&gt;developer CLIs that AI agents use well&lt;/a&gt; — a good dev tool should compress ten decisions into one glance.&lt;/p&gt;

&lt;p&gt;Privacy-wise, PR Radar doesn't send your tokens anywhere. Your personal access tokens sit in the browser's local storage, and the extension makes API calls directly from your browser to each Git platform. There is no PR Radar backend. We don't run analytics. There's no &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; account required — the extension works whether or not you're a &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; customer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Side-by-side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Email&lt;/th&gt;
&lt;th&gt;GitHub Inbox&lt;/th&gt;
&lt;th&gt;Slack&lt;/th&gt;
&lt;th&gt;Refined / Notifier&lt;/th&gt;
&lt;th&gt;PR Radar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitLab&lt;/td&gt;
&lt;td&gt;✅ (via email)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;⚠️ Uneven&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bitbucket&lt;/td&gt;
&lt;td&gt;✅ (via email)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;⚠️ Uneven&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI status at a glance&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;⚠️ In channel&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ Badge + inline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unresolved comment count&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy status in PR list&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works without a tab open&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ Service worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sound / desktop alerts&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;⚠️ If tab open&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge from the tool&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ All 3 platforms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Free tier + Slack cost&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Free + open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy&lt;/td&gt;
&lt;td&gt;Platform stores email&lt;/td&gt;
&lt;td&gt;Platform&lt;/td&gt;
&lt;td&gt;Slack + platform&lt;/td&gt;
&lt;td&gt;Platform&lt;/td&gt;
&lt;td&gt;100% local, no backend&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern is consistent: each mainstream option handles one or two of these well and treats the rest as out of scope. For developers who only use GitHub and already live in Slack, that's fine. For anyone working across platforms, the gaps add up fast.&lt;/p&gt;




&lt;h2&gt;
  
  
  The deployment connection
&lt;/h2&gt;

&lt;p&gt;If you're reading this on the &lt;a href="https://www.deployhq.com/blog" rel="noopener noreferrer"&gt;DeployHQ blog&lt;/a&gt;, there's a fair chance you care specifically about the last mile: the moment between &lt;q&gt;CI passed&lt;/q&gt; and &lt;q&gt;change is live.&lt;/q&gt; That moment is where PR tracking stops being a productivity nice-to-have and starts being a deployment-quality thing.&lt;/p&gt;

&lt;p&gt;Two concrete examples:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catching a broken deploy before the channel sees it.&lt;/strong&gt; When CI includes a deploy step (either an &lt;a href="https://www.deployhq.com/features/automatic-deployments" rel="noopener noreferrer"&gt;automatic deployment from Git&lt;/a&gt; or a &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;preview environment triggered by PR&lt;/a&gt;), the failure signal travels through the same notification pipes as every other CI event. In email or Slack, a failed deploy looks like every other failed check. In PR Radar, the toolbar flips red the moment the job changes state, with the deploy URL one click away. That's a four-second feedback loop instead of a forty-minute one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Merging without a tab full of PRs.&lt;/strong&gt; DeployHQ's &lt;a href="https://www.deployhq.com/features/one-click-rollback" rel="noopener noreferrer"&gt;one-click rollback&lt;/a&gt; and &lt;a href="https://www.deployhq.com/features/zero-downtime-deployments" rel="noopener noreferrer"&gt;zero-downtime deployments&lt;/a&gt; are designed to make shipping cheap. The bottleneck in that flow is usually human: somebody has to decide the PR is ready and hit merge. If that decision point lives in a popup rather than on a PR page buried in a tab group, the whole cycle tightens.&lt;/p&gt;

&lt;p&gt;Neither of these is the reason to install a browser extension on its own. But if you already care enough about deployment velocity to be reading this, the PR tracking side of the workflow probably deserves the same attention you've given &lt;a href="https://dev.to/deployhq/agentic-workflows-explained-how-ai-agents-are-changing-cicd-pipelines-nm0-temp-slug-2291085"&gt;your CI/CD pipeline&lt;/a&gt; and &lt;a href="https://www.deployhq.com/deploy-from-github" rel="noopener noreferrer"&gt;your deploy automation&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who should pick which
&lt;/h2&gt;

&lt;p&gt;Here's the actual decision tree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You only use GitHub and you're fine with the inbox.&lt;/strong&gt; Stay with it, maybe bolt on Refined GitHub for UI polish. No extension needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You only use GitHub but the inbox feels lossy.&lt;/strong&gt; Try &lt;a href="https://chromewebstore.google.com/detail/notifier-for-github/lmjdlojahmbbcodnpecnjnmlddbkjhnn" rel="noopener noreferrer"&gt;Notifier for GitHub&lt;/a&gt; for the toolbar badge. If you want CI status specifically and not just unread counts, PR Radar fits even in single-platform setups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You use GitHub + GitLab or GitHub + Bitbucket.&lt;/strong&gt; This is the case email, the inbox, and GitHub-specific extensions all fail at. A dedicated multi-platform tool is the only real answer. PR Radar is the one we built; there isn't much direct competition in the free / privacy-first slice of that category.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You're a team lead who wants team-wide visibility into deploys.&lt;/strong&gt; Slack is still right for that — route release-critical events into a dedicated channel. Just don't try to use the same Slack integration for personal PR tracking; it's the wrong tool for the wrong granularity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You care about privacy or you work on client repos with sensitive PR titles.&lt;/strong&gt; PR Radar keeps everything local — tokens in browser storage, API calls direct to platforms, no backend, no analytics. Hosted PR tools generally require you to authorize an OAuth app that sees your PR content. That's a real trade-off worth knowing about, in the same category as picking &lt;a href="https://dev.to/deployhq/6-must-have-mcp-servers-for-web-developers-in-2025-35no"&gt;which MCP servers you grant access to your codebase&lt;/a&gt; or &lt;a href="https://dev.to/deployhq/how-to-use-git-with-claude-code-understanding-the-co-authored-by-attribution-3boi"&gt;how you wire Claude Code into Git&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;PR Radar is free, open source (MIT), and takes about a minute to set up.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Install from the &lt;a href="https://chromewebstore.google.com/detail/pr-radar-pr-dashboard-ci/hkombgibegjffiadmekpiabdakkoidmh" rel="noopener noreferrer"&gt;Chrome Web Store&lt;/a&gt;&lt;/strong&gt; (Firefox and Edge builds are in progress on the &lt;a href="https://github.com/deployhq/pr-radar/releases" rel="noopener noreferrer"&gt;GitHub releases page&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Star the repo on &lt;a href="https://github.com/deployhq/pr-radar" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/strong&gt; if it saves you a tab — it's how we know to keep investing in it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File an issue&lt;/strong&gt; if a platform quirk breaks your setup; we read all of them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're already on &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, PR Radar closes the visibility gap between &lt;q&gt;PR looks good&lt;/q&gt; and &lt;q&gt;change is live&lt;/q&gt; without asking you to change anything about your deployment workflow. If you're not, it's still the fastest way we've found to stop tab-hopping between Git hosts.&lt;/p&gt;




&lt;p&gt;Questions, platform-specific gotchas, or feature requests? Email us at &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or ping us on &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;Twitter/X&lt;/a&gt;. If you'd rather build your own PR dashboard on top of the GraphQL APIs, the source is MIT-licensed and PRs are welcome.&lt;/p&gt;

</description>
      <category>prs</category>
      <category>github</category>
      <category>gitlab</category>
      <category>bitbutcket</category>
    </item>
    <item>
      <title>Mailtrap vs SendGrid vs Mailgun: Best Email API for Node.js in 2026</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Wed, 15 Apr 2026 05:07:04 +0000</pubDate>
      <link>https://dev.to/deployhq/mailtrap-vs-sendgrid-vs-mailgun-best-email-api-for-nodejs-in-2026-1ng7</link>
      <guid>https://dev.to/deployhq/mailtrap-vs-sendgrid-vs-mailgun-best-email-api-for-nodejs-in-2026-1ng7</guid>
      <description>&lt;p&gt;Mailtrap, SendGrid, and Mailgun are the three most common email APIs that Node.js developers use for transactional or promotional email sending. Each takes a different approach to SDK design, deliverability architecture, and pricing, so the right choice depends on what your team actually needs.&lt;/p&gt;

&lt;p&gt;This comparison covers installation, deliverability infrastructure, API workflow capabilities, MCP support, and pricing. All three were tested by integrating their SDKs into a Node.js project and sending emails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Comparison Overview
&lt;/h2&gt;

&lt;p&gt;All three tools offer straightforward npm installation, but there are differences worth noting in setup complexity, package size, and ecosystem maturity.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Email API&lt;/th&gt;
&lt;th&gt;Setup Time&lt;/th&gt;
&lt;th&gt;NPM Downloads / Week&lt;/th&gt;
&lt;th&gt;SDK Size&lt;/th&gt;
&lt;th&gt;TypeScript Support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mailtrap&lt;/td&gt;
&lt;td&gt;5 mins&lt;/td&gt;
&lt;td&gt;28K+&lt;/td&gt;
&lt;td&gt;90.4 kB&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mailgun&lt;/td&gt;
&lt;td&gt;10-15 mins&lt;/td&gt;
&lt;td&gt;360K+&lt;/td&gt;
&lt;td&gt;1.3 MB&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SendGrid&lt;/td&gt;
&lt;td&gt;10-15 mins&lt;/td&gt;
&lt;td&gt;1.65M+&lt;/td&gt;
&lt;td&gt;17.4 kB&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Installation and Setup Comparison
&lt;/h2&gt;

&lt;p&gt;When integrating an email service into a &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;deployment pipeline&lt;/a&gt;, you want an SDK that configures quickly and does not introduce brittle dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mailtrap
&lt;/h3&gt;

&lt;p&gt;Setup time: 5 minutes. Complexity: Easy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;npm&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt; &lt;span class="nx"&gt;mailtrap&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MailtrapClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mailtrap&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MailtrapClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MAILTRAP_API_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;recipient@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello from Mailtrap&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This is a test email.&lt;/span&gt;&lt;span class="dl"&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;The Mailtrap package follows a clean API design with intuitive REST principles. It does not require complex DNS setup for initial sandbox testing, so developers can configure their environment variables and fire off a test email right away.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mailgun
&lt;/h3&gt;

&lt;p&gt;Setup time: 10-15 minutes. Complexity: Medium to complex.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;npm&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt; &lt;span class="nx"&gt;mailgun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Mailgun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mailgun.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;form-data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mailgun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Mailgun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mailgun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MAILGUN_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;mg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-domain.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello@your-domain.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;recipient@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello from Mailgun&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This is a test email.&lt;/span&gt;&lt;span class="dl"&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;Mailgun requires DNS configuration for domain verification before you can send production emails. The SDK also depends on &lt;code&gt;form-data&lt;/code&gt; as a peer dependency, which adds a step. Understanding the email validation features adds initial overhead too.&lt;/p&gt;

&lt;h3&gt;
  
  
  SendGrid
&lt;/h3&gt;

&lt;p&gt;Setup time: 10-15 minutes. Complexity: Medium.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;npm&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;sendgrid&lt;/span&gt;&lt;span class="sr"&gt;/mai&lt;/span&gt;&lt;span class="err"&gt;l
&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sgMail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@sendgrid/mail&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;sgMail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SENDGRID_API_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;sgMail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;recipient@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello from SendGrid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This is a test email.&lt;/span&gt;&lt;span class="dl"&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;The &lt;code&gt;@sendgrid/mail&lt;/code&gt; package requires additional configuration for sender authentication and strict API key management. There are multiple ways to structure email payloads, which can be confusing during initial setup if you are working from the docs (they carry a lot of legacy content for different API versions).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Getting started verdict:&lt;/strong&gt; Mailtrap has the lowest friction for a first send. SendGrid and Mailgun both take 10-15 minutes depending on DNS propagation and how familiar you are with their respective dashboards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deliverability and Infrastructure
&lt;/h2&gt;

&lt;p&gt;How each service handles deliverability architecture matters more than raw setup speed once you are sending production emails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mailtrap
&lt;/h3&gt;

&lt;p&gt;Mailtrap enforces strict separation between transactional and bulk message streams. This is an architectural decision, not just a UI toggle. Transactional emails (password resets, order confirmations, 2FA codes) run on a dedicated IP pool that is never affected by the reputation of your marketing sends. If a promotional campaign triggers spam complaints, your critical application notifications keep arriving. Mailtrap also offers a 99.99% uptime guarantee with compensation, and detailed analytics that track opens, clicks, and bounces for up to 30 days.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mailgun
&lt;/h3&gt;

&lt;p&gt;Mailgun focuses heavily on pre-send list quality. It validates email addresses by checking DNS MX records, disposable address databases, and mailbox existence against a 450+ billion email dataset. If you have older contact lists with unknown deliverability, this validation layer can meaningfully reduce your bounce rates before you even send. Mailgun also maintains automated suppression lists that remove hard bounces and spam complainers from future sends, which protects your sending domain over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  SendGrid
&lt;/h3&gt;

&lt;p&gt;SendGrid operates at a massive scale. Its infrastructure handles enterprise-level volume for both marketing and transactional email, backed by Twilio's network. It also offers native Handlebars template logic stored in the UI and programmatic automation for drip campaigns. For large organizations that need a single platform to handle everything from password resets to weekly newsletters, SendGrid's breadth is hard to match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deliverability verdict:&lt;/strong&gt; Mailgun's pre-send validation against 450+ billion records and automated suppression lists give it the most complete deliverability toolkit. Mailtrap's stream separation is a solid architectural advantage for apps mixing transactional and promotional email. SendGrid's scale and Twilio-backed infrastructure make it the safest bet for high-volume senders.&lt;/p&gt;

&lt;h2&gt;
  
  
  API Workflow Capabilities
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Creating Email Templates
&lt;/h3&gt;

&lt;p&gt;SendGrid lets marketing teams store email templates directly in its UI using Handlebars syntax. Content updates don't require code deployments. Mailtrap and Mailgun offer full API control over templates, but developers need to manage the payload construction themselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Event Webhooks
&lt;/h3&gt;

&lt;p&gt;Webhooks let your Node.js server passively listen for delivery events.&lt;/p&gt;

&lt;p&gt;Mailtrap delivers full payload data for delivered, opened, and bounced events, with 40 internal retries every five minutes to make sure events are not dropped. Mailgun includes message IDs alongside click and bounce events, which is useful for debugging delivery issues. SendGrid tracks standard events but restricts free-tier users to a single endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Framework Integration
&lt;/h3&gt;

&lt;p&gt;All three providers offer SDKs that work with Express.js, Next.js, NestJS, Fastify, and Koa. They all support standard async/await patterns and compile-time error checking through TypeScript. Framework compatibility is not a differentiator here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API workflow verdict:&lt;/strong&gt; SendGrid's Handlebars-based template system lets users update email content without code deployments, and the broader ecosystem means more community examples and integrations. Mailgun's message-ID tracking is the strongest option for debugging delivery issues. Mailtrap's webhook retry mechanism is worth noting for teams that need guaranteed event delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP and Extensibility
&lt;/h2&gt;

&lt;p&gt;All three tools support the Model Context Protocol (MCP) for extending AI capabilities into IDEs and terminal workflows.&lt;/p&gt;

&lt;p&gt;Mailtrap provides an official MCP server focused on deliverability, template management, and multi-recipient sending. Mailgun offers an open-source MCP server for sending emails and retrieving analytics. SendGrid relies on community-created MCP servers for campaign management, which may require manual configuration.&lt;/p&gt;

&lt;p&gt;Common MCP use cases for email include generating and testing HTML email templates via AI, querying bounce rates and delivery logs, and automating recipient list segmentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extensibility verdict:&lt;/strong&gt; Mailtrap and Mailgun both offer maintained MCP servers. SendGrid's community approach gives you more flexibility but less out-of-the-box reliability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing Comparison
&lt;/h2&gt;

&lt;p&gt;Cost is often the deciding factor when scaling a Node.js application's email infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mailtrap
&lt;/h3&gt;

&lt;p&gt;Free tier covers up to 1,000 emails. The Basic plan starts at $15/month for 10,000+ emails. The Business plan runs $85/month for 100,000+ emails. Higher tiers include 24/7 priority support and extended log retention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mailgun
&lt;/h3&gt;

&lt;p&gt;Free tier allows 100 emails per day. The Foundation plan is $15/month for 10,000 emails. The Scale plan costs $90/month for 100,000 emails. Pricing gets less competitive at higher volumes, but the validation tools can save money by reducing bounces.&lt;/p&gt;

&lt;h3&gt;
  
  
  SendGrid
&lt;/h3&gt;

&lt;p&gt;Free tier offers 100 emails per day on a 60-day trial. Essentials starts at $19.95/month for 50,000 emails. Pro costs $89.95/month for 100,000 emails. The pricing structure is complex with many add-ons, but makes sense for high-volume senders who need marketing and transactional email in one platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing verdict:&lt;/strong&gt; At lower volumes all three are competitive. Mailtrap and Mailgun both start at $15/month. SendGrid costs slightly more but includes a larger email allowance on its Essentials plan. The real cost differences emerge at scale and depend on which features you actually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unique Strengths
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mailtrap:&lt;/strong&gt; Strict separation of transactional and promotional streams. An official TypeScript SDK with zero unnecessary dependencies. A 99.99% uptime SLA with automatic webhook failure detection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mailgun:&lt;/strong&gt; Industry-leading pre-send email validation against a 450+ billion record database. Batch sending to 1,000 recipients per API call using localized recipient variables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SendGrid:&lt;/strong&gt; The largest Node.js email ecosystem with 1.65 million weekly npm downloads, backed by Twilio's infrastructure. A unified API that handles both transactional coding and visual marketing campaigns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing Email Credentials in Your Deployment Pipeline
&lt;/h2&gt;

&lt;p&gt;Regardless of which &lt;a href="https://mailtrap.io/email-api/" rel="noopener noreferrer"&gt;email API&lt;/a&gt; you choose, never hardcode API keys in your application code. Inject them through environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EMAIL_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EMAIL_API_KEY is missing from environment.&lt;/span&gt;&lt;span class="dl"&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;This is a common pitfall, especially when switching between staging and production API keys for different email providers. A leaked SendGrid or Mailgun key in a public repo can result in your sending domain getting blacklisted within hours.&lt;/p&gt;

&lt;p&gt;If you are deploying a Node.js app that sends email, your deployment tool should handle these secrets safely. In &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, you can store environment variables as part of your project's &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;build pipeline configuration&lt;/a&gt; and inject them at deploy time. This keeps credentials out of your repository while ensuring each environment (staging, production) uses the correct API keys and SMTP settings.&lt;/p&gt;

&lt;p&gt;You can also use DeployHQ's &lt;a href="https://www.deployhq.com/support/projects/configuration-files" rel="noopener noreferrer"&gt;configuration file feature&lt;/a&gt; to template out &lt;code&gt;.env&lt;/code&gt; files during deployment, swapping placeholders for real values stored in the deployment target settings. For teams running separate transactional and marketing email streams (which Mailtrap enforces by default), this means you can store different API tokens per deployment target and avoid accidentally sending test emails through your production pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommendations by Use Case
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For developer and product teams shipping transactional email quickly:&lt;/strong&gt; Mailtrap. The 5-minute setup and strict stream separation let you get to production without surprises. The SDK is small, typed, and does not pull in unnecessary dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For teams managing older or unverified contact lists:&lt;/strong&gt; Mailgun. The validation API will reduce your bounce rates before emails ever leave your server. The batch sending feature (up to 1,000 recipients per API call with recipient variables) is also useful for teams sending personalized notifications at moderate scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For enterprise marketing teams that need campaigns and transactional email in one platform:&lt;/strong&gt; SendGrid. The unified ecosystem and Twilio backing handle both use cases, and the 1.65 million weekly npm downloads mean you will find answers to most integration questions on Stack Overflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making Your Choice
&lt;/h2&gt;

&lt;p&gt;There is no wrong choice among these three. Mailtrap, SendGrid, and Mailgun are all actively maintained, well-documented, and capable. All three support async/await, TypeScript, and the major Node.js frameworks. Start with the one that matches your timeline and email types, then evaluate further as your sending volume grows.&lt;/p&gt;

&lt;p&gt;The broader trend across all three providers is convergence around MCP support, TypeScript-first SDKs, and better webhook reliability. Where they still diverge is in deliverability architecture and pricing models. For deployment workflows specifically, the most important factor is how cleanly the SDK integrates with your existing &lt;a href="https://www.deployhq.com/support/projects/environment-variables" rel="noopener noreferrer"&gt;environment variable management&lt;/a&gt; and CI/CD pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Which email API is best for a Node.js beginner?
&lt;/h3&gt;

&lt;p&gt;Mailtrap has the simplest setup at around 5 minutes, with a single npm package and no peer dependencies. SendGrid is also straightforward but has more configuration options that can be confusing initially. If you just need to get a transactional email working quickly, Mailtrap or SendGrid are your best bets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I switch email providers later without rewriting my application?
&lt;/h3&gt;

&lt;p&gt;Yes, if you abstract your email sending behind a service layer. All three providers use similar patterns (API key authentication, JSON payloads, async sending), so swapping one for another typically means changing the SDK import and adjusting the payload format. Keeping your API keys in &lt;a href="https://www.deployhq.com/support/projects/environment-variables" rel="noopener noreferrer"&gt;environment variables&lt;/a&gt; rather than hardcoded makes the switch even simpler.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need a dedicated IP address for transactional email?
&lt;/h3&gt;

&lt;p&gt;Not necessarily. All three providers offer shared IP pools that work well for most applications. Dedicated IPs become relevant when you send high volumes (50,000+ emails per month) and want full control over your sender reputation. Mailtrap and SendGrid offer dedicated IPs on higher-tier plans; Mailgun includes them on the Scale plan and above.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I test emails without sending to real recipients?
&lt;/h3&gt;

&lt;p&gt;Mailtrap was originally built as an email testing tool — its sandbox captures emails in a virtual inbox so you can inspect HTML rendering, spam scores, and headers without sending to real addresses. SendGrid and Mailgun both support sandbox modes as well, though the setup requires additional configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if my email API goes down during a deployment?
&lt;/h3&gt;

&lt;p&gt;Your application should handle email API failures gracefully with retry logic and a message queue. None of these providers guarantee 100% uptime (Mailtrap offers 99.99%, the others are similar). Using &lt;a href="https://www.deployhq.com/features/zero-downtime-deployments" rel="noopener noreferrer"&gt;DeployHQ's zero-downtime deployments&lt;/a&gt; ensures your application stays available during deploys, but your email error handling should account for temporary API outages regardless of provider.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it safe to use free tiers in production?
&lt;/h3&gt;

&lt;p&gt;Free tiers are fine for low-volume applications, but they come with limitations. Mailtrap's free plan caps at 1,000 emails per month. SendGrid and Mailgun limit you to 100 emails per day. Beyond testing and early-stage projects, you will likely need a paid plan for reliable production sending with higher rate limits and better support.&lt;/p&gt;

&lt;p&gt;Ready to streamline your Node.js deployments? &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Get started with DeployHQ&lt;/a&gt; and automate your deployment pipeline today.&lt;/p&gt;




&lt;p&gt;Have questions? Reach out at &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or find us on &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;X (@deployhq)&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>node</category>
      <category>emailapi</category>
      <category>comparison</category>
    </item>
    <item>
      <title>CLIs or MCP for Coding Agents? A Practical Comparison</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Mon, 13 Apr 2026 08:34:15 +0000</pubDate>
      <link>https://dev.to/deployhq/clis-or-mcp-for-coding-agents-a-practical-comparison-3kim</link>
      <guid>https://dev.to/deployhq/clis-or-mcp-for-coding-agents-a-practical-comparison-3kim</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 5 of our series on AI coding assistants for developers. See also: &lt;a href="https://dev.to/deployhq/getting-started-with-claude-code-the-ai-coding-assistant-for-your-terminal-4cba"&gt;Getting Started with Claude Code&lt;/a&gt;, &lt;a href="https://dev.to/deployhq/getting-started-with-openai-codex-cli-ai-powered-code-generation-from-your-terminal-5hm8"&gt;Getting Started with OpenAI Codex CLI&lt;/a&gt;, &lt;a href="https://dev.to/deployhq/getting-started-with-google-gemini-cli-open-source-ai-agent-for-your-terminal-25e1"&gt;Getting Started with Google Gemini CLI&lt;/a&gt;, and &lt;a href="https://www.deployhq.com/blog/comparing-ai-cli-coding-assistants-claude-code-vs-codex-vs-gemini-cli" rel="noopener noreferrer"&gt;Comparing AI CLI Coding Assistants&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Your AI coding agent can read files, run commands, and edit code. But how does it actually connect to the tools and services it needs? In 2026, there are two dominant approaches: &lt;strong&gt;command-line interfaces (CLIs)&lt;/strong&gt; and the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;. Both give coding agents access to external capabilities — but they work in fundamentally different ways, with real consequences for context quality, reliability, and what your agent can actually do.&lt;/p&gt;

&lt;p&gt;This isn't an abstract protocol debate. The interface you choose directly affects how well your agent understands your codebase, how accurately it executes tasks, and how much manual oversight you need to provide. Let's break down what's similar, what's different, and when each approach makes sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Mean by CLIs and MCPs
&lt;/h2&gt;

&lt;p&gt;Before comparing them, let's be precise about what each term means in the context of coding agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  CLIs: The Agent Shells Out
&lt;/h3&gt;

&lt;p&gt;When a coding agent uses a CLI tool, it constructs a shell command, executes it in a subprocess, and parses the text output. This is the oldest and most universal integration pattern. Your agent might run &lt;code&gt;git log --oneline -10&lt;/code&gt; to see recent commits, &lt;code&gt;npm test&lt;/code&gt; to run your test suite, or &lt;code&gt;curl&lt;/code&gt; to hit an API endpoint.&lt;/p&gt;

&lt;p&gt;The agent treats the CLI as a black box: it sends a string, gets a string back, and interprets the result using its language understanding.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP: A Structured Tool Protocol
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; is an open standard that defines a JSON-RPC interface between AI models and external tools. Instead of executing arbitrary shell commands, the agent calls named tools with typed parameters and receives structured JSON responses.&lt;/p&gt;

&lt;p&gt;An MCP server might expose a &lt;code&gt;search_posts&lt;/code&gt; tool that accepts &lt;code&gt;{ "query": "deployment", "status": "published" }&lt;/code&gt; and returns a typed array of post objects — complete with IDs, titles, dates, and metadata. The agent never needs to parse text output or guess at field meanings.&lt;/p&gt;

&lt;h2&gt;
  
  
  What They Have in Common
&lt;/h2&gt;

&lt;p&gt;Despite the architectural differences, CLIs and MCP share several characteristics:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both extend agent capabilities.&lt;/strong&gt; Whether your agent runs &lt;code&gt;gh pr list&lt;/code&gt; or calls an MCP tool named &lt;code&gt;list_pull_requests&lt;/code&gt;, the end result is the same: the agent gains access to information and actions beyond its training data. Both approaches turn a conversational AI into something that can interact with real systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both require trust boundaries.&lt;/strong&gt; A CLI command can delete files; an MCP tool can publish a blog post. In both cases, you need permission models. Claude Code uses approval prompts for shell commands; MCP servers define their own tool permissions. The security concern is identical — you're giving an AI agent the ability to affect real systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both work across all major coding agents.&lt;/strong&gt; Claude Code, Codex CLI, and Gemini CLI all support both shell execution and MCP servers. (If you're still choosing between these three, our &lt;a href="https://www.deployhq.com/blog/comparing-ai-cli-coding-assistants-claude-code-vs-codex-vs-gemini-cli" rel="noopener noreferrer"&gt;comparison of AI CLI coding assistants&lt;/a&gt; covers the differences in detail.) The specific configuration differs, but the concept is universal. If you're building an integration, either approach will work with the tools your team already uses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both can be composed.&lt;/strong&gt; Agents regularly chain multiple CLI commands together (&lt;code&gt;git add . &amp;amp;&amp;amp; git commit -m "fix" &amp;amp;&amp;amp; git push&lt;/code&gt;), and they can chain multiple MCP tool calls in sequence. Complex workflows are possible with either approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where They Differ
&lt;/h2&gt;

&lt;p&gt;Here's where the comparison gets interesting — and where the choice actually matters for your development workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Output Structure: Text vs. Typed Data
&lt;/h3&gt;

&lt;p&gt;This is the single biggest difference, and it cascades into nearly every other comparison point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI output is unstructured text.&lt;/strong&gt; When your agent runs &lt;code&gt;ls -la&lt;/code&gt;, it gets a blob of text that it must parse using pattern matching and language understanding. This works remarkably well for simple commands, but it introduces ambiguity. Does that column represent file size or block count? Is that date in DD/MM or MM/DD format? The agent is constantly inferring structure from text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP responses are structured JSON.&lt;/strong&gt; When your agent calls an MCP tool, it gets back typed fields with known semantics. A post object has a &lt;code&gt;published_at&lt;/code&gt; field that's always a Unix timestamp, a &lt;code&gt;title&lt;/code&gt; that's always a string, and a &lt;code&gt;categories&lt;/code&gt; array that's always a list of IDs. There's no ambiguity to resolve.&lt;/p&gt;

&lt;p&gt;This matters most when agents need to make decisions based on the data. An agent parsing &lt;code&gt;git log&lt;/code&gt; output might misinterpret a commit message that contains a date-like string. An agent receiving structured commit objects from a Git MCP server won't have that problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Discoverability: Man Pages vs. Tool Schemas
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;CLI tools require the agent to know what exists.&lt;/strong&gt; The agent needs prior knowledge (from training data) about which commands are available, what flags they accept, and what their output looks like. If you have a custom deployment script at &lt;code&gt;./scripts/deploy.sh&lt;/code&gt;, the agent won't know about it unless you tell it — or it happens to find it while exploring your project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP servers declare their capabilities.&lt;/strong&gt; When an agent connects to an MCP server, it receives a complete list of available tools with descriptions, parameter schemas, and return types. The agent knows exactly what it can do without any prior knowledge. This is especially powerful for domain-specific tools — your custom &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; MCP server doesn't need to be in the model's training data for the agent to use it effectively.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Context Efficiency: Tokens Matter
&lt;/h3&gt;

&lt;p&gt;Every piece of information the agent processes consumes tokens from its context window. This has real cost and performance implications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI output is verbose and unstructured.&lt;/strong&gt; Running &lt;code&gt;docker ps&lt;/code&gt; might return 20 lines of formatted table output when the agent only needed the container ID and status. The agent ingests all of it. Multiply this across dozens of commands in a complex task, and you're burning significant context on formatting, headers, and irrelevant columns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP responses are precise.&lt;/strong&gt; An MCP tool can return exactly the fields the agent needs. If you only want post titles and IDs, the MCP server returns just that — no extra columns, no formatting overhead, no ASCII table borders. For agents working on long, multi-step tasks, this efficiency compounds significantly.&lt;/p&gt;

&lt;p&gt;Here's a concrete example. Fetching the last 10 blog posts via CLI might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://api.example.com/posts?limit&lt;span class="o"&gt;=&lt;/span&gt;10 | jq &lt;span class="s1"&gt;'.[] | {id, title, status}'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That raw JSON response might be 2,000 tokens. The equivalent MCP call returns the same data in a pre-structured format that the agent can consume in roughly 800 tokens — because there's no HTTP headers, no &lt;code&gt;jq&lt;/code&gt; processing, and no raw response wrapping.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Error Handling: Exit Codes vs. Typed Errors
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;CLI errors are inconsistent.&lt;/strong&gt; Some tools use exit codes, some print to stderr, some return error messages on stdout, and some do all three in different combinations. An agent parsing CLI output needs to handle all these patterns — and sometimes the &lt;q&gt;error&lt;/q&gt; is actually a warning that doesn't indicate failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP errors follow a standard format.&lt;/strong&gt; The protocol defines error responses with codes, messages, and optional details. The agent always knows whether a call succeeded or failed, and can make reliable decisions about retry logic, fallback strategies, or error reporting.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Security Model: Sandbox vs. Scoped Permissions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;CLI access is broad.&lt;/strong&gt; When you give an agent shell access, it can potentially run any command your user account can execute. Most coding agents mitigate this with approval prompts, but the underlying capability is unrestricted. A malicious or confused agent could run &lt;code&gt;rm -rf /&lt;/code&gt; if the permission check fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP access is scoped by design.&lt;/strong&gt; Each MCP server exposes only its specific tools. A blog management MCP server can't access your filesystem; a database MCP server can't run shell commands. The attack surface is naturally smaller. If an MCP server's token has read-only permissions, no amount of clever prompting can make it write data.&lt;/p&gt;

&lt;p&gt;This becomes especially important when agents operate with reduced oversight — in automated pipelines, background tasks, or &lt;q&gt;full auto&lt;/q&gt; modes.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. State and Authentication
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;CLIs inherit the user's environment.&lt;/strong&gt; Your shell's PATH, environment variables, SSH keys, and authentication tokens are all available to CLI commands. This is convenient — &lt;code&gt;git push&lt;/code&gt; just works because your SSH agent is running — but it also means the agent has access to all your credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP servers manage their own auth.&lt;/strong&gt; Each server handles its own authentication, typically through API tokens or service accounts configured when the server starts. This creates clear boundaries: your Google Search Console MCP server has its own service account credentials that are separate from your shell environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;CLI&lt;/th&gt;
&lt;th&gt;MCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Output format&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unstructured text&lt;/td&gt;
&lt;td&gt;Typed JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Discoverability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires prior knowledge&lt;/td&gt;
&lt;td&gt;Self-describing schemas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Context efficiency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Verbose (high token cost)&lt;/td&gt;
&lt;td&gt;Precise (low token cost)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Error handling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Inconsistent across tools&lt;/td&gt;
&lt;td&gt;Standardised protocol&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security scope&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Broad shell access&lt;/td&gt;
&lt;td&gt;Scoped per server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Inherits user environment&lt;/td&gt;
&lt;td&gt;Isolated per server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Zero (tools already installed)&lt;/td&gt;
&lt;td&gt;Moderate (server config needed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Universality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Any CLI tool works&lt;/td&gt;
&lt;td&gt;Only MCP-enabled services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ecosystem maturity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Decades of tools&lt;/td&gt;
&lt;td&gt;Growing rapidly (2024+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Offline capability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Depends on server&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which Provides Better Context for Coding Agents?
&lt;/h2&gt;

&lt;p&gt;If you're optimising for agent performance — fewer hallucinations, more accurate tool use, better multi-step reasoning — MCP has a clear structural advantage. Typed data, self-describing schemas, and efficient token usage all contribute to higher-quality agent behaviour.&lt;/p&gt;

&lt;p&gt;But that doesn't mean you should replace all CLIs with MCP servers. The practical answer is more nuanced:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use MCP for domain-specific integrations.&lt;/strong&gt; If you regularly interact with a specific service — your blog platform, deployment tool, monitoring system, or database — an MCP server gives the agent richer context and more reliable interactions. This is why tools like the &lt;a href="https://www.deployhq.com/blog/mcp-servers-every-web-developer-needs-in-2026" rel="noopener noreferrer"&gt;DeployHQ MCP server&lt;/a&gt; exist: they provide structured access to deployment management that no CLI could match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use CLIs for general-purpose operations.&lt;/strong&gt; File system operations, Git commands, package management, and build tools are universal. They work everywhere, require no setup, and agents handle them well. There's no benefit to wrapping &lt;code&gt;npm install&lt;/code&gt; in an MCP server — the CLI works perfectly for this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use both together.&lt;/strong&gt; The most effective setup in 2026 combines both approaches. Your agent uses CLIs for general development tasks and MCP servers for specialised integrations. Claude Code, for example, natively supports both — you can run shell commands and call MCP tools in the same conversation, letting the agent choose the best approach for each step.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means for Your Deployment Workflow
&lt;/h2&gt;

&lt;p&gt;For teams using &lt;a href="https://www.deployhq.com/features/automatic-deployments" rel="noopener noreferrer"&gt;automated Git deployments&lt;/a&gt;, the CLI-vs-MCP choice has practical implications:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Git operations stay CLI-based.&lt;/strong&gt; Your agent commits, pushes, and manages branches through standard Git commands. This is well-understood, universal, and effective.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployment management benefits from MCP.&lt;/strong&gt; Checking deployment status, triggering rollbacks, or inspecting server configurations are tasks where structured data matters. An MCP server that returns deployment status as &lt;code&gt;{ "status": "success", "deployed_at": "2026-04-12T10:30:00Z", "commit": "abc1234" }&lt;/code&gt; gives the agent far better context than parsing HTML or text output from a dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content operations are ideal for MCP.&lt;/strong&gt; If your workflow includes managing blog content alongside code — writing deployment guides, updating documentation, managing SEO — an MCP server provides the structured access that makes agents genuinely useful for content workflows.&lt;/p&gt;

&lt;p&gt;The pattern we see with teams using &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; is straightforward: Git CLI for code, MCP for everything else that has an API. This gives agents the broadest capabilities with the best context quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Future: Convergence
&lt;/h2&gt;

&lt;p&gt;The boundary between CLIs and MCP is already blurring. Some newer CLI tools output structured JSON by default. MCP servers can wrap existing CLIs to add structure. And coding agents are getting better at parsing unstructured output.&lt;/p&gt;

&lt;p&gt;But the trend is clear: as agents take on more complex, multi-step tasks with less human oversight, structured interfaces become more important — not less. The more an agent operates autonomously, the more it benefits from unambiguous, typed data.&lt;/p&gt;

&lt;p&gt;If you're building tools for AI agents today, MCP is worth the investment. If you're using agents for development work, configure both CLIs and the MCP servers that match your workflow. The agents that perform best are the ones with the richest, most structured context available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I use MCP and CLIs together in the same agent session?
&lt;/h3&gt;

&lt;p&gt;Yes. All major coding agents — Claude Code, Codex CLI, and Gemini CLI — support both shell commands and MCP tool calls in the same session. The agent picks the most appropriate interface for each task automatically. You don't need to choose one or the other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do MCP servers replace the need for CLI tools?
&lt;/h3&gt;

&lt;p&gt;No. MCP servers complement CLIs rather than replacing them. CLIs excel at general-purpose operations like file management, Git, and build tools. MCP servers are best for domain-specific services where structured data and scoped permissions matter. The most effective setups use both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is MCP harder to set up than just using CLIs?
&lt;/h3&gt;

&lt;p&gt;MCP requires initial configuration — you need to install and configure each server, provide API tokens, and register them with your coding agent. CLIs, by contrast, typically just work with your existing shell environment. However, the setup is usually a one-time task, and the improved agent performance often justifies the upfront effort.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does using MCP reduce token costs?
&lt;/h3&gt;

&lt;p&gt;Generally yes. MCP responses are structured and concise, which means less token usage per interaction compared to parsing verbose CLI output. For agents running long, multi-step tasks or operating at scale, this efficiency can meaningfully reduce costs — especially with pay-per-token pricing models.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which approach is more secure?
&lt;/h3&gt;

&lt;p&gt;MCP has a structural security advantage because each server exposes only specific capabilities with scoped permissions. CLI access grants broader system access by default. However, both approaches require proper configuration — MCP servers need secure token management, and CLI access needs appropriate permission prompts. Neither is inherently &lt;q&gt;safe&lt;/q&gt; without proper setup.&lt;/p&gt;




&lt;p&gt;AI coding agents are most effective when they have the right context, delivered through the right interface. For general development tasks, your terminal's CLI tools remain indispensable. For specialised integrations — content management, &lt;a href="https://www.deployhq.com/features/automatic-deployments" rel="noopener noreferrer"&gt;deployment automation&lt;/a&gt;, monitoring, and APIs — MCP provides the structured context that helps agents work accurately and efficiently.&lt;/p&gt;

&lt;p&gt;The best setup isn't one or the other. It's both, configured to match your workflow.&lt;/p&gt;

&lt;p&gt;Ready to add structured deployment management to your AI coding workflow? &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; integrates with all major AI coding assistants through both CLI and MCP, giving your agents the context they need to manage deployments confidently. &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Get started for free&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For questions or feedback, reach out at &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or on &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;Twitter/X&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devopsinfrastructure</category>
      <category>cli</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Deploy from Gitea to Your Server Automatically</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Sat, 11 Apr 2026 07:22:39 +0000</pubDate>
      <link>https://dev.to/deployhq/deploy-from-gitea-to-your-server-automatically-1dp0</link>
      <guid>https://dev.to/deployhq/deploy-from-gitea-to-your-server-automatically-1dp0</guid>
      <description>&lt;p&gt;Gitea is quietly becoming the go-to self-hosted Git service. It's lightweight (runs on a Raspberry Pi), easy to install, and gives you full control over your source code without depending on GitHub or GitLab's infrastructure.&lt;/p&gt;

&lt;p&gt;But Gitea has one notable gap: &lt;strong&gt;no built-in CI/CD&lt;/strong&gt;. Gitea Actions exists but is still maturing and requires a separate runner instance. For teams that just want to push code and have it deployed to their server, that's a lot of infrastructure for a simple requirement.&lt;/p&gt;

&lt;p&gt;Here's how to add fully automated deployments to your Gitea workflow in under 10 minutes, using webhooks and &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Gitea Needs an External Deployment Tool
&lt;/h2&gt;

&lt;p&gt;Gitea is intentionally minimal — it's a Git hosting service, not a DevOps platform. That's a feature, not a bug. But it means deployment is left as an exercise for the user.&lt;/p&gt;

&lt;p&gt;The common approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gitea Actions&lt;/strong&gt; — still in development, requires a dedicated runner, modelled after GitHub Actions but with fewer marketplace actions available&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drone CI / Woodpecker CI&lt;/strong&gt; — full CI/CD systems that integrate with Gitea, but they're heavy for simple &lt;q&gt;push to deploy&lt;/q&gt; workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual SSH&lt;/strong&gt; — &lt;code&gt;ssh server 'cd /var/www &amp;amp;&amp;amp; git pull'&lt;/code&gt; works but breaks the moment you need build commands or multiple servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook-based deployment&lt;/strong&gt; — a lightweight service watches for push events and deploys automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The webhook approach hits the sweet spot: no CI infrastructure to maintain, no YAML pipelines to write, and it works with any Gitea installation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[Developer] --&amp;gt;|git push| B[Gitea Instance]
    B --&amp;gt;|webhook POST| C[DeployHQ]
    C --&amp;gt;|git clone via SSH| B
    C --&amp;gt;|build commands| D[Build Server]
    D --&amp;gt;|changed files via SSH/SFTP| E[Production Server]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you push to Gitea, a webhook fires. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; receives the notification, clones the latest code from your Gitea repo, runs any build commands, and deploys the changed files to your server. The entire cycle takes seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step-by-Step Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Create a DeployHQ Project
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Sign up for DeployHQ&lt;/a&gt; (free for one project) and create a new project.&lt;/p&gt;

&lt;p&gt;When asked for your repository, choose &lt;strong&gt;&lt;q&gt;Enter repository URL manually&lt;/q&gt;&lt;/strong&gt; — Gitea isn't in the OAuth provider list since it's self-hosted.&lt;/p&gt;

&lt;p&gt;Enter your Gitea repository URL. You have two options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSH (recommended):&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;git@gitea.yourserver.com:username/repo.git

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;HTTPS with access token:&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;https://gitea.yourserver.com/username/repo.git

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For SSH, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; will generate an SSH key pair. Copy the public key.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Add the SSH Key to Gitea
&lt;/h3&gt;

&lt;p&gt;In your Gitea repository:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Deploy Keys&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;Deploy&lt;/a&gt; Key&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Paste the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; public key&lt;/li&gt;
&lt;li&gt;Title it &lt;q&gt;DeployHQ&lt;/q&gt;
&lt;/li&gt;
&lt;li&gt;Leave &lt;q&gt;Enable Write Access&lt;/q&gt; unchecked (read-only is sufficient)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Alternatively, add the key to your Gitea user account under &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;SSH/GPG Keys&lt;/strong&gt; for access to all your repositories.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Configure Your Server in DeployHQ
&lt;/h3&gt;

&lt;p&gt;Add your deployment target:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protocol:&lt;/strong&gt; SSH/SFTP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hostname:&lt;/strong&gt; your-production-server.com&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Username:&lt;/strong&gt; your deploy user&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication:&lt;/strong&gt; SSH key (add DeployHQ's key to the server's &lt;code&gt;authorized_keys&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment Path:&lt;/strong&gt; &lt;code&gt;/var/www/myapp/&lt;/code&gt; (or your web root)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Set Up Build Commands (If Needed)
&lt;/h3&gt;

&lt;p&gt;If your project needs a build step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="nv"&gt;%path%&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="kd"&gt;npm&lt;/span&gt; &lt;span class="kd"&gt;ci&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="kd"&gt;npm&lt;/span&gt; &lt;span class="nb"&gt;run&lt;/span&gt; &lt;span class="kd"&gt;build&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the &lt;strong&gt;Deployment Subdirectory&lt;/strong&gt; if you're deploying build output (e.g., &lt;code&gt;dist/&lt;/code&gt; for static sites, &lt;code&gt;build/&lt;/code&gt; for React apps).&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Configure the Webhook in Gitea
&lt;/h3&gt;

&lt;p&gt;This is where Gitea and &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; connect. In &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, go to your project and find the &lt;strong&gt;Webhook URL&lt;/strong&gt; (under project settings or on the servers page).&lt;/p&gt;

&lt;p&gt;In Gitea:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to your repository → &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Webhooks&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add Webhook&lt;/strong&gt; → choose &lt;strong&gt;Gitea&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target URL:&lt;/strong&gt; paste the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; webhook URL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP Method:&lt;/strong&gt; POST&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content Type:&lt;/strong&gt; &lt;code&gt;application/json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trigger On:&lt;/strong&gt; Push Events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active:&lt;/strong&gt; checked&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add Webhook&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Test it by clicking the &lt;strong&gt;Test Delivery&lt;/strong&gt; button. You should see a 200 response.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Push and Deploy
&lt;/h3&gt;

&lt;p&gt;Make a change, commit, and push:&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="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; README.md
git add &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Test deployment"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git push

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gitea fires the webhook → &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; clones, builds, and deploys. Check the deployment log in &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; to see exactly which files were transferred.&lt;/p&gt;

&lt;h2&gt;
  
  
  Branch-Based Deployments
&lt;/h2&gt;

&lt;p&gt;Map different branches to different servers for a staging/production workflow:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Branch&lt;/th&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;main&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;production-server.com&lt;/td&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;develop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;staging-server.com&lt;/td&gt;
&lt;td&gt;Staging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;feature/*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Not deployed (review locally)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, add multiple servers and assign each to a specific branch. Pushing to &lt;code&gt;develop&lt;/code&gt; deploys to staging. Merging to &lt;code&gt;main&lt;/code&gt; deploys to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Self-Signed Certificates
&lt;/h2&gt;

&lt;p&gt;If your Gitea instance uses a self-signed HTTPS certificate, DeployHQ's HTTPS clone will fail certificate verification. Two solutions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use SSH instead of HTTPS&lt;/strong&gt; — SSH cloning doesn't involve TLS certificates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a real certificate&lt;/strong&gt; — Let's Encrypt is free and works with any domain&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;SSH is the simpler fix and is recommended regardless of certificate situation — it's faster and avoids password/token management.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gitea Behind a Firewall
&lt;/h2&gt;

&lt;p&gt;If your Gitea instance isn't publicly accessible, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; can't reach it for webhook delivery or Git cloning. Options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open port 22 (SSH) to DeployHQ's IPs&lt;/strong&gt; — check DeployHQ's documentation for the IP list&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a reverse SSH tunnel&lt;/strong&gt; — more complex but avoids opening firewall ports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mirror to a public Git host&lt;/strong&gt; — Gitea can mirror to GitHub/GitLab, and &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; clones from there&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most setups, opening SSH access from DeployHQ's IPs is the simplest approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison: Gitea Deployment Options
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Setup time&lt;/th&gt;
&lt;th&gt;Maintenance&lt;/th&gt;
&lt;th&gt;Build support&lt;/th&gt;
&lt;th&gt;Multi-server&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DeployHQ&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~10 min&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Yes (npm, Composer, etc.)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Free for 1 project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gitea Actions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30-60 min&lt;/td&gt;
&lt;td&gt;Runner maintenance&lt;/td&gt;
&lt;td&gt;Yes (via Actions)&lt;/td&gt;
&lt;td&gt;Custom scripting&lt;/td&gt;
&lt;td&gt;Free (self-hosted)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Drone CI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30-60 min&lt;/td&gt;
&lt;td&gt;Server + runners&lt;/td&gt;
&lt;td&gt;Yes (Docker-based)&lt;/td&gt;
&lt;td&gt;Custom config&lt;/td&gt;
&lt;td&gt;Free (OSS edition)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Woodpecker CI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30-60 min&lt;/td&gt;
&lt;td&gt;Server + agents&lt;/td&gt;
&lt;td&gt;Yes (Docker-based)&lt;/td&gt;
&lt;td&gt;Custom config&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Manual &lt;code&gt;git pull&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;td&gt;Error-prone&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom webhook script&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1-2 hours&lt;/td&gt;
&lt;td&gt;Script maintenance&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; wins on setup time and maintenance. Drone/Woodpecker win if you need full CI (testing, multi-stage pipelines, matrix builds). Gitea Actions will likely become the default choice once it matures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-Repository Deployments
&lt;/h2&gt;

&lt;p&gt;If your project spans multiple Gitea repositories (frontend + backend, or a main app + shared library), create separate &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; projects for each. Each project has its own webhook, build configuration, and server targets.&lt;/p&gt;

&lt;p&gt;For monorepos on Gitea, use DeployHQ's deployment subdirectory feature to deploy specific directories from the repository.&lt;/p&gt;




&lt;p&gt;Gitea gives you independence from hosted Git providers. Adding &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; gives you automated deployments without the complexity of running a full CI/CD system. Push to Gitea, and your code is live — simple as that.&lt;/p&gt;

&lt;p&gt;If you're using &lt;a href="https://www.deployhq.com/deploy-from-github" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; or &lt;a href="https://www.deployhq.com/deploy-from-gitlab" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt; alongside Gitea, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; works with all of them from a single dashboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Start deploying from Gitea&lt;/a&gt; — set up takes about 10 minutes.&lt;/p&gt;

&lt;p&gt;Questions about connecting your Gitea instance? Contact &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or reach us on &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;Twitter/X&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>git</category>
      <category>tutorials</category>
      <category>gitea</category>
      <category>deployhq</category>
    </item>
    <item>
      <title>How to Deploy EmDash with DeployHQ</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Thu, 09 Apr 2026 16:28:31 +0000</pubDate>
      <link>https://dev.to/deployhq/how-to-deploy-emdash-with-deployhq-218c</link>
      <guid>https://dev.to/deployhq/how-to-deploy-emdash-with-deployhq-218c</guid>
      <description>&lt;p&gt;EmDash is a full-stack TypeScript CMS built on Astro — often described as the spiritual successor to WordPress. It combines a modern admin interface with Astro's rendering performance, Portable Text for structured content, and passkey-first authentication. If you're building a content-driven site with EmDash and need reliable, repeatable deployments from Git, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; makes the process straightforward.&lt;/p&gt;

&lt;p&gt;In this guide, you'll set up an EmDash project for Node.js deployment, connect it to &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, configure your build pipeline, and deploy to a VPS or cloud server with zero manual file transfers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is EmDash?
&lt;/h2&gt;

&lt;p&gt;EmDash is an open-source CMS that runs as an &lt;a href="https://astro.build/" rel="noopener noreferrer"&gt;Astro&lt;/a&gt; integration. Unlike traditional CMSs that bolt a content API onto a separate frontend, EmDash ships everything in one project — the admin panel, content editor, authentication, media management, and your frontend templates.&lt;/p&gt;

&lt;p&gt;A few things that set EmDash apart:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Astro-native&lt;/strong&gt; : Your site is an Astro project. EmDash adds the CMS layer as an integration, so you keep full control over routing, components, and rendering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portable Text editor&lt;/strong&gt; : Content is stored as structured JSON (via TipTap), not raw HTML. This makes content reusable across different presentation formats.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passkey-first auth&lt;/strong&gt; : No passwords by default. Users authenticate with WebAuthn passkeys, with OAuth and magic links as fallbacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema-in-database&lt;/strong&gt; : Content collections are defined in the database, not in code files. This means editors can create new collections without touching the codebase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin system&lt;/strong&gt; : Extensible via a &lt;code&gt;definePlugin()&lt;/code&gt; API with sandboxed execution on Cloudflare or in-process on Node.js.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;EmDash supports multiple deployment targets — Cloudflare Workers, Docker containers, or plain Node.js servers. This guide focuses on Node.js deployment via &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, which covers VPS hosting, cloud instances, and dedicated servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before starting, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node.js 22.12.0 or later&lt;/strong&gt; installed on both your local machine and your deployment server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Git repository&lt;/strong&gt; (GitHub, GitLab, or Bitbucket) containing your EmDash project&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; account&lt;/strong&gt; — you can &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;sign up for a free trial&lt;/a&gt; if you don't have one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH access to your server&lt;/strong&gt; with Node.js 22+ installed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A process manager&lt;/strong&gt; like PM2 or systemd configured on your server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you haven't created an EmDash project yet, scaffold one with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm create emdash@latest my-site
&lt;span class="nb"&gt;cd &lt;/span&gt;my-site
npm &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates an Astro project with EmDash pre-configured. The scaffolder lets you choose from blog, marketing, or portfolio templates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparing EmDash for Production
&lt;/h2&gt;

&lt;p&gt;EmDash requires server-side rendering — it's a full-stack CMS, not a static site generator. Your Astro config must use the Node.js adapter in standalone mode.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure the Astro Adapter
&lt;/h3&gt;

&lt;p&gt;Open your &lt;code&gt;astro.config.mjs&lt;/code&gt; and verify the Node.js adapter is set to &lt;code&gt;standalone&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// astro.config.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@astrojs/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;emdash&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emdash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;node&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;standalone&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;integrations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;emdash&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;standalone&lt;/code&gt; mode is critical — it produces a self-contained server entry point at &lt;code&gt;./dist/server/entry.mjs&lt;/code&gt; that you can run directly with Node.js. The alternative &lt;code&gt;middleware&lt;/code&gt; mode is for platforms like Express where you manage the HTTP server yourself, but standalone is simpler for &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; deployments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generate Authentication Secrets
&lt;/h3&gt;

&lt;p&gt;EmDash needs two secrets for production: one for signing session cookies and another for preview URLs. Generate them locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx emdash auth secret

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This outputs a cryptographically secure random string. Run it twice — once for &lt;code&gt;EMDASH_AUTH_SECRET&lt;/code&gt; and once for &lt;code&gt;EMDASH_PREVIEW_SECRET&lt;/code&gt;. Save both values; you'll add them as environment variables in &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choose Your Database
&lt;/h3&gt;

&lt;p&gt;EmDash supports several database backends. For a Node.js deployment via &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, the practical options are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;Single-server deployments&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;DATABASE_PATH&lt;/code&gt; to a persistent directory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;Multi-server or production-critical sites&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;DATABASE_URL&lt;/code&gt; connection string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;libSQL (Turso)&lt;/td&gt;
&lt;td&gt;Remote SQLite with replication&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;LIBSQL_DATABASE_URL&lt;/code&gt; and &lt;code&gt;LIBSQL_AUTH_TOKEN&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SQLite is the simplest choice for a single VPS — no external database server needed. Just make sure the database file lives on a persistent volume, not in the deployment directory (since &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; replaces files on each deploy).&lt;/p&gt;

&lt;p&gt;For SQLite, create a data directory outside your deployment path:&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;# On your server, create a persistent data directory&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/data/emdash
&lt;span class="nb"&gt;chown &lt;/span&gt;deploy:deploy /var/data/emdash

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then set the environment variable to point there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DATABASE_PATH&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/data/emdash/emdash.db&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add a Start Script
&lt;/h3&gt;

&lt;p&gt;Verify your &lt;code&gt;package.json&lt;/code&gt; includes a start script:&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;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"astro dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"astro build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node ./dist/server/entry.mjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bootstrap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"emdash init &amp;amp;&amp;amp; emdash seed"&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;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;The &lt;code&gt;build&lt;/code&gt; command compiles your Astro project into the &lt;code&gt;dist/&lt;/code&gt; directory. The &lt;code&gt;start&lt;/code&gt; command runs the compiled server. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; will execute &lt;code&gt;build&lt;/code&gt; during deployment and your process manager will run &lt;code&gt;start&lt;/code&gt; on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up DeployHQ
&lt;/h2&gt;

&lt;p&gt;With your EmDash project in Git and your server ready, connect everything through &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a New Project
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Log in to &lt;a href="https://www.deployhq.com/" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; and click &lt;strong&gt;New Project&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Name it (e.g., &lt;q&gt;EmDash Blog&lt;/q&gt;) and select the server region closest to your deployment target&lt;/li&gt;
&lt;li&gt;Connect your Git repository — &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; supports GitHub, GitLab, Bitbucket, and self-hosted Git servers&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Configure the Server
&lt;/h3&gt;

&lt;p&gt;Add your deployment server under &lt;strong&gt;Servers&lt;/strong&gt; in the project settings:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;New Server&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;SSH/SFTP&lt;/strong&gt; as the protocol&lt;/li&gt;
&lt;li&gt;Enter your server hostname, SSH port, and the deploy user credentials&lt;/li&gt;
&lt;li&gt;Set the &lt;strong&gt;Deployment Path&lt;/strong&gt; to where your application lives, for example &lt;code&gt;/var/www/emdash&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Test the connection to verify &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; can reach your server&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Configure the Build Pipeline
&lt;/h3&gt;

&lt;p&gt;This is where &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; handles your Node.js build. Navigate to &lt;strong&gt;Build Pipeline&lt;/strong&gt; in your project settings:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable the &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;build pipeline&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Node.js 22&lt;/strong&gt; as the build environment&lt;/li&gt;
&lt;li&gt;Add the build commands:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm ci
npm run build

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;npm ci&lt;/code&gt; instead of &lt;code&gt;npm install&lt;/code&gt; ensures a clean, reproducible install from your lockfile — important for consistent deployments.&lt;/p&gt;

&lt;p&gt;DeployHQ's build pipeline runs these commands in an isolated container, compiles your Astro project, and then deploys only the output files to your server. This means your server doesn't need to run &lt;code&gt;npm install&lt;/code&gt; or &lt;code&gt;npm run build&lt;/code&gt; — it just receives the compiled application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set Environment Variables
&lt;/h3&gt;

&lt;p&gt;Under &lt;strong&gt;Environment Variables&lt;/strong&gt; in your project settings, add:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EMDASH_AUTH_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your generated auth secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EMDASH_PREVIEW_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your generated preview secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATABASE_PATH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/var/data/emdash/emdash.db&lt;/code&gt; (for SQLite)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HOST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.0.0.0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PORT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;4321&lt;/code&gt; (or your preferred port)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you're using PostgreSQL instead, replace &lt;code&gt;DATABASE_PATH&lt;/code&gt; with &lt;code&gt;DATABASE_URL&lt;/code&gt; pointing to your connection string.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; encrypts environment variables at rest, so your secrets are protected. These variables are available during the build step and can also be written to a &lt;code&gt;.env&lt;/code&gt; file on the server if your application reads from one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure Deployment Exclusions
&lt;/h3&gt;

&lt;p&gt;Not every file needs to reach your server. Under &lt;strong&gt;Excluded Files&lt;/strong&gt; , add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
tests/
node_modules/
.git/
tsconfig.json
astro.config.mjs
README.md

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait — you actually need &lt;code&gt;node_modules&lt;/code&gt; on the server because &lt;code&gt;dist/server/entry.mjs&lt;/code&gt; requires runtime dependencies. There are two approaches:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approach A — &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;Deploy&lt;/a&gt; node_modules (simpler):&lt;/strong&gt; Don't exclude &lt;code&gt;node_modules&lt;/code&gt;. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; transfers the full project including dependencies. This is slower but requires no server-side commands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approach B — Install on server (cleaner):&lt;/strong&gt; Exclude &lt;code&gt;node_modules&lt;/code&gt; and add a &lt;a href="https://www.deployhq.com/support/projects-and-%E0%AE%9Fservers/ssh-commands" rel="noopener noreferrer"&gt;post-deploy SSH command&lt;/a&gt; to install production dependencies:&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="nb"&gt;cd&lt;/span&gt; /var/www/emdash &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm ci &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Approach B is preferred for larger projects because it only installs production dependencies, keeping the server lean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying EmDash
&lt;/h2&gt;

&lt;h3&gt;
  
  
  First Deployment
&lt;/h3&gt;

&lt;p&gt;Your first deployment needs extra setup because the database doesn't exist yet. After the initial deploy completes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SSH into your server&lt;/li&gt;
&lt;li&gt;Navigate to your deployment directory&lt;/li&gt;
&lt;li&gt;Run the bootstrap command to initialise the database:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /var/www/emdash
npm run bootstrap

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates the database schema and optionally seeds sample content. You only need to run this once.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start the application:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Using PM2 (recommended)&lt;/span&gt;
pm2 start dist/server/entry.mjs &lt;span class="nt"&gt;--name&lt;/span&gt; emdash

&lt;span class="c"&gt;# Or using systemd (see next section)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Visit &lt;code&gt;http://your-server:4321/_emdash/admin&lt;/code&gt; to complete the setup wizard — set your site title, tagline, and register your admin passkey.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Set Up a Process Manager
&lt;/h3&gt;

&lt;p&gt;For production, use PM2 or systemd to keep EmDash running and restart it on crashes or reboots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PM2 ecosystem file&lt;/strong&gt; (&lt;code&gt;ecosystem.config.cjs&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ecosystem.config.cjs&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;apps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emdash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./dist/server/entry.mjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;HOST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.0.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4321&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_PATH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/var/data/emdash/emdash.db&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;autorestart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_memory_restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;512M&lt;/span&gt;&lt;span class="dl"&gt;'&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;Note: if you're using SQLite, keep &lt;code&gt;instances: 1&lt;/code&gt;. SQLite doesn't handle concurrent writes from multiple processes well. For multi-instance deployments, switch to PostgreSQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;systemd service&lt;/strong&gt; (&lt;code&gt;/etc/systemd/system/emdash.service&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;EmDash CMS&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;deploy&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/www/emdash&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/node dist/server/entry.mjs&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;NODE_ENV=production&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;HOST=0.0.0.0&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;PORT=4321&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;DATABASE_PATH=/var/data/emdash/emdash.db&lt;/span&gt;
&lt;span class="py"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/www/emdash/.env&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Automate Restarts with Post-Deploy Commands
&lt;/h3&gt;

&lt;p&gt;After each deployment, EmDash needs to restart so it picks up the new code. Add a post-deploy SSH command in &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; under &lt;strong&gt;SSH Commands → After Deployment&lt;/strong&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="nb"&gt;cd&lt;/span&gt; /var/www/emdash &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pm2 restart emdash

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or for systemd:&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="nb"&gt;sudo &lt;/span&gt;systemctl restart emdash

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures every successful deployment automatically restarts the application without manual intervention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Subsequent Deployments
&lt;/h3&gt;

&lt;p&gt;After the initial setup, the deployment workflow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Push code changes to your Git repository&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; detects the push (via webhook) and starts a deployment&lt;/li&gt;
&lt;li&gt;The build pipeline runs &lt;code&gt;npm ci &amp;amp;&amp;amp; npm run build&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Compiled files are transferred to your server&lt;/li&gt;
&lt;li&gt;The post-deploy command restarts the application&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can also configure &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; to &lt;a href="https://www.deployhq.com/features/automatic-deployments" rel="noopener noreferrer"&gt;deploy automatically on every push&lt;/a&gt; to your main branch, or keep deployments manual and trigger them from the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment Architecture
&lt;/h2&gt;

&lt;p&gt;Here's how the pieces fit together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[Git Push] --&amp;gt; B[DeployHQ]
    B --&amp;gt; C[Build Pipeline&amp;lt;br/&amp;gt;npm ci + astro build]
    C --&amp;gt; D[Transfer Files&amp;lt;br/&amp;gt;via SSH]
    D --&amp;gt; E[VPS / Cloud Server]
    E --&amp;gt; F[PM2 / systemd&amp;lt;br/&amp;gt;Restart]
    F --&amp;gt; G[EmDash Running&amp;lt;br/&amp;gt;port 4321]
    G --&amp;gt; H[Reverse Proxy&amp;lt;br/&amp;gt;nginx / Caddy]
    H --&amp;gt; I[Public Traffic&amp;lt;br/&amp;gt;HTTPS on port 443]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; handles steps A through F. You configure the reverse proxy once during initial server setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up a Reverse Proxy
&lt;/h2&gt;

&lt;p&gt;In production, you'll want a reverse proxy in front of EmDash to handle HTTPS, compression, and static asset caching. Here's a minimal nginx configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;your-domain.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;ssl_certificate&lt;/span&gt; &lt;span class="n"&gt;/etc/letsencrypt/live/your-domain.com/fullchain.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_certificate_key&lt;/span&gt; &lt;span class="n"&gt;/etc/letsencrypt/live/your-domain.com/privkey.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:4321&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;'upgrade'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_cache_bypass&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&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;If you prefer Caddy, the configuration is even simpler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your-domain.com {
    reverse_proxy localhost:4321
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caddy handles TLS certificates automatically via Let's Encrypt — no manual certificate setup needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Build Fails with &lt;q&gt;Cannot find module&lt;/q&gt; Errors
&lt;/h3&gt;

&lt;p&gt;This usually means dependencies aren't installed correctly. Make sure your &lt;code&gt;package-lock.json&lt;/code&gt; is committed to Git. DeployHQ's build pipeline runs &lt;code&gt;npm ci&lt;/code&gt;, which requires a lockfile. If you're using pnpm locally, either switch &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; to use &lt;code&gt;pnpm install &amp;amp;&amp;amp; pnpm build&lt;/code&gt; or add a &lt;code&gt;package-lock.json&lt;/code&gt; alongside.&lt;/p&gt;

&lt;h3&gt;
  
  
  Application Starts but Admin Panel Returns 404
&lt;/h3&gt;

&lt;p&gt;EmDash's admin routes live under &lt;code&gt;/_emdash/admin&lt;/code&gt;. Make sure your Astro config has &lt;code&gt;output: 'server'&lt;/code&gt; — if it's set to &lt;code&gt;'static'&lt;/code&gt; or &lt;code&gt;'hybrid'&lt;/code&gt;, the server-side routes won't work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database Errors After Deployment
&lt;/h3&gt;

&lt;p&gt;If you're using SQLite and see &lt;q&gt;database is locked&lt;/q&gt; or &lt;q&gt;no such table&lt;/q&gt; errors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify &lt;code&gt;DATABASE_PATH&lt;/code&gt; points to a directory outside the deployment path (so it persists between deployments)&lt;/li&gt;
&lt;li&gt;Make sure the deploy user has write permissions to the database directory&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;npm run bootstrap&lt;/code&gt; if the database hasn't been initialised yet&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Passkey Authentication Fails
&lt;/h3&gt;

&lt;p&gt;Passkeys require HTTPS. If you're testing over plain HTTP, passkey registration and login will fail. Set up your reverse proxy with TLS first, or use the dev bypass endpoint for initial testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://your-domain.com/_emdash/api/setup/dev-bypass?redirect=/_emdash/admin

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove this bypass in production by ensuring &lt;code&gt;NODE_ENV=production&lt;/code&gt; is set.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment Succeeds but Site Shows Old Content
&lt;/h3&gt;

&lt;p&gt;The post-deploy restart command might not be running. Check the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; deployment log for SSH command output. Also verify that PM2 or systemd is actually restarting the correct process — run &lt;code&gt;pm2 list&lt;/code&gt; or &lt;code&gt;systemctl status emdash&lt;/code&gt; on the server to confirm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I use DeployHQ's atomic deployments with EmDash?
&lt;/h3&gt;

&lt;p&gt;Yes. DeployHQ's &lt;a href="https://www.deployhq.com/features/zero-downtime-deployments" rel="noopener noreferrer"&gt;zero downtime deployments&lt;/a&gt; work well with EmDash. Each deployment goes into a new directory, and a symlink switches to the new version atomically. Just make sure your &lt;code&gt;DATABASE_PATH&lt;/code&gt; and any uploaded media point to a shared directory outside the release path so they persist across deployments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does EmDash work with DeployHQ's build pipeline caching?
&lt;/h3&gt;

&lt;p&gt;The build pipeline caches &lt;code&gt;node_modules&lt;/code&gt; between deployments, so subsequent builds are faster since npm only installs changed dependencies. Astro's build output isn't cached (it rebuilds each time), but the dependency caching alone can cut build times significantly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I deploy EmDash to shared hosting via FTP?
&lt;/h3&gt;

&lt;p&gt;EmDash requires a long-running Node.js process, which most shared hosting plans don't support. You need a VPS, cloud instance, or container hosting where you can run &lt;code&gt;node dist/server/entry.mjs&lt;/code&gt; persistently. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; supports &lt;a href="https://www.deployhq.com/support/projects-and-servers/server-types" rel="noopener noreferrer"&gt;FTP and SFTP deployment&lt;/a&gt; protocols, but the server itself must support Node.js 22+.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I handle database migrations during deployment?
&lt;/h3&gt;

&lt;p&gt;EmDash automatically runs migrations when the application starts (for SQLite, libSQL, and PostgreSQL). There's no separate migration step needed in your deployment pipeline. The restart via PM2 or systemd triggers the migration check on boot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can multiple team members deploy independently?
&lt;/h3&gt;

&lt;p&gt;Yes. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; supports team collaboration with role-based permissions. You can restrict who can trigger deployments while giving everyone read access to deployment logs. Combined with DeployHQ's &lt;a href="https://www.deployhq.com/features" rel="noopener noreferrer"&gt;deployment approvals&lt;/a&gt;, you can require sign-off before production deployments go live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;You now have a production EmDash deployment powered by Git-based automation through &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;. Every push to your repository triggers a clean build and deployment — no manual file transfers, no SSH-ing into servers to run commands.&lt;/p&gt;

&lt;p&gt;From here, you might want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up &lt;a href="https://www.deployhq.com/support/projects-and-servers/notifications" rel="noopener noreferrer"&gt;deployment notifications&lt;/a&gt; to get Slack or email alerts on each deploy&lt;/li&gt;
&lt;li&gt;Configure a staging server in &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; to test changes before they hit production&lt;/li&gt;
&lt;li&gt;Explore EmDash's plugin system to extend your CMS with custom functionality&lt;/li&gt;
&lt;li&gt;Add a &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;build pipeline&lt;/a&gt; step for running tests before deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ready to automate your EmDash deployments? &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Sign up for DeployHQ&lt;/a&gt; and connect your repository in minutes.&lt;/p&gt;




&lt;p&gt;If you have questions about deploying EmDash or any other framework with &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, reach out to us at &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or on &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;Twitter/X @deployhq&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>node</category>
      <category>tutorials</category>
      <category>wordpress</category>
      <category>emdash</category>
    </item>
    <item>
      <title>OpenClaw Skills: How to Find, Install, and Build Your Own</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Tue, 07 Apr 2026 06:13:28 +0000</pubDate>
      <link>https://dev.to/deployhq/openclaw-skills-how-to-find-install-and-build-your-own-52aj</link>
      <guid>https://dev.to/deployhq/openclaw-skills-how-to-find-install-and-build-your-own-52aj</guid>
      <description>&lt;p&gt;If you followed our &lt;a href="https://dev.to/theqadiariesforyou/how-to-deploy-and-configure-openclaw-on-a-vps-4h8e-temp-slug-2543902"&gt;guide to deploying OpenClaw on a VPS&lt;/a&gt;, you've got a self-hosted AI assistant running on infrastructure you control. Out of the box it can chat, browse the web, run terminal commands, and remember context across sessions. Skills are what turn it from a capable chatbot into an automation engine that handles the repetitive parts of your workflow without being asked twice.&lt;/p&gt;

&lt;p&gt;This post covers what Skills are, how to find and install them, a tour of the most useful directories, and how to build one from scratch to automate a real task. We'll also walk through troubleshooting common issues and best practices for writing skills that work reliably across environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Skills?
&lt;/h2&gt;

&lt;p&gt;A Skill is a directory containing a &lt;code&gt;SKILL.md&lt;/code&gt; file — a Markdown document with YAML frontmatter — that teaches OpenClaw a repeatable procedure. When OpenClaw starts, it reads all eligible skills and injects compressed descriptions of them into its system prompt. The agent then knows which skills are available and invokes them automatically when a user request matches.&lt;/p&gt;

&lt;p&gt;The simplest skill is just a natural-language runbook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;skills/
└── my-skill/
    └── SKILL.md

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More advanced skills can include supporting scripts and configuration, but the &lt;code&gt;SKILL.md&lt;/code&gt; is always the entry point. Unlike traditional plugins, which require the host application to define an API surface, Skills work by giving the LLM instructions — the model figures out how to use them in context.&lt;/p&gt;

&lt;p&gt;Three locations are checked when loading skills, in precedence order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;workspace&amp;gt;/skills&lt;/code&gt; — workspace-specific, highest priority&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.openclaw/skills&lt;/code&gt; — user-level, shared across all agents on the machine&lt;/li&gt;
&lt;li&gt;Built-in bundled skills — shipped with the install, lowest priority&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means you can override a bundled skill just by dropping a folder with the same name into your workspace directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A["Workspace Skills\n(./skills)"] --&amp;gt;|highest priority| D["OpenClaw\nSystem Prompt"]
    B["User-Level Skills\n(~/.openclaw/skills)"] --&amp;gt;|medium priority| D
    C["Bundled Skills\n(built-in)"] --&amp;gt;|lowest priority| D
    D --&amp;gt; E["Agent Ready\n— skills injected"]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Finding Skills: The ClawHub Registry
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/openclaw/clawhub" rel="noopener noreferrer"&gt;ClawHub&lt;/a&gt; is the official public skills registry, hosting over 13,700 community-built skills. Use the CLI to search and install:&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;# Search by keyword&lt;/span&gt;
clawhub search &lt;span class="s2"&gt;"github pull request"&lt;/span&gt;

&lt;span class="c"&gt;# Install a skill&lt;/span&gt;
clawhub &lt;span class="nb"&gt;install &lt;/span&gt;pr-summary

&lt;span class="c"&gt;# Update all installed skills&lt;/span&gt;
clawhub update &lt;span class="nt"&gt;--all&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default &lt;code&gt;clawhub install&lt;/code&gt; places the skill in &lt;code&gt;./skills&lt;/code&gt; under your current directory. To install globally (visible to all your OpenClaw workspaces):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clawhub &lt;span class="nb"&gt;install &lt;/span&gt;pr-summary &lt;span class="nt"&gt;--global&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The community has also published a curated list — &lt;a href="https://github.com/VoltAgent/awesome-openclaw-skills" rel="noopener noreferrer"&gt;awesome-openclaw-skills&lt;/a&gt; — which filters the registry down to 5,400+ vetted skills organised into categories. It's the faster way to browse if you don't already know what you're looking for.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Security note&lt;/strong&gt; : Treat third-party skills as untrusted code. Read the &lt;code&gt;SKILL.md&lt;/code&gt; before enabling a skill, especially if it requires API keys or system binaries. A skill that phones home with your environment variables is possible to write — the registry has moderation, but it's not a guarantee.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A Tour of the Skill Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Coding &amp;amp; Development&lt;/strong&gt; (1,200+ skills) — the largest category by far. Highlights include skills for searching academic papers via OpenAlex, generating hash-chained audit logs for agent actions, and full GitHub workflow automation (open PRs, review diffs, triage issues). If your team uses &lt;a href="https://www.deployhq.com/deploy-from-github" rel="noopener noreferrer"&gt;GitHub-based deployment workflows&lt;/a&gt;, skills in this category can trigger deploys directly from a conversation — push a branch, open a PR, and let your CI pipeline handle the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DevOps &amp;amp; Cloud&lt;/strong&gt; (400+ skills) — Docker container management, process health monitoring, cloud provider CLIs. The &lt;code&gt;agentic-devops&lt;/code&gt; skill handles common operations like restarting a crashed container or tailing logs from a service, triggered by a plain-language message. Teams running &lt;a href="https://www.deployhq.com/features/zero-downtime-deployments" rel="noopener noreferrer"&gt;zero downtime deployments&lt;/a&gt; can pair these skills with health-check monitoring to automatically roll back a release if the new container fails readiness probes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser &amp;amp; Automation&lt;/strong&gt; (335 skills) — web scraping, UI testing, form automation. Useful for monitoring pages that don't expose APIs or automating login-gated workflows. A common pattern is combining a browser skill with a notification skill to alert you when a competitor changes their pricing page or when your own staging environment returns errors after a deploy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Productivity &amp;amp; Tasks&lt;/strong&gt; (200+ skills) — Notion CRUD, calendar management, email triage. The &lt;code&gt;better-notion&lt;/code&gt; skill gives full create/read/update/delete access to Notion pages and databases from any messaging channel. Other standout skills in this category include time-tracking integrations that log hours against Jira tickets and daily digest generators that summarise activity across multiple project management tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Communication&lt;/strong&gt; (149 skills) — Slack, Discord, Teams, and email integrations. Post to channels, summarise threads, draft replies. The Slack skills are particularly mature — you can configure them to post deployment summaries, standup reminders, or incident alerts directly into the relevant channel without leaving the OpenClaw conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Git &amp;amp; GitHub&lt;/strong&gt; (170 skills) — beyond the basics, there are skills for commit message generation, changelog drafting, and coordinating multi-repo releases. The &lt;code&gt;release-coordinator&lt;/code&gt; skill is worth highlighting: it tags a release, generates a changelog from merged PRs, and posts the release notes to Slack in a single invocation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building Your Own Skill
&lt;/h2&gt;

&lt;p&gt;The quickest way to understand Skills is to build one. Here's a practical example: a &lt;strong&gt;daily standup note generator&lt;/strong&gt; that reads your recent git commits and drafts your standup update, so you never have to manually trawl through &lt;code&gt;git log&lt;/code&gt; before your morning call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Create the folder
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.openclaw/skills/standup-notes

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2 — Write the SKILL.md
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano ~/.openclaw/skills/standup-notes/SKILL.md


&lt;span class="nt"&gt;---&lt;/span&gt;
name: standup-notes
description: Generates a standup update from git commits &lt;span class="k"&gt;in &lt;/span&gt;the last 24 hours across &lt;span class="nb"&gt;local &lt;/span&gt;repositories
user-invocable: &lt;span class="nb"&gt;true
&lt;/span&gt;metadata: &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"openclaw"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"requires"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"bins"&lt;/span&gt;:[&lt;span class="s2"&gt;"git"&lt;/span&gt;&lt;span class="o"&gt;]}}}&lt;/span&gt;
&lt;span class="nt"&gt;---&lt;/span&gt;

&lt;span class="c"&gt;# Standup Notes Generator&lt;/span&gt;

When the user asks &lt;span class="k"&gt;for &lt;/span&gt;standup notes, a standup summary, or &lt;span class="s2"&gt;"what did I work on yesterday"&lt;/span&gt;, follow these steps:

&lt;span class="c"&gt;## Steps&lt;/span&gt;

1. Ask the user which directory their repositories live &lt;span class="k"&gt;in&lt;/span&gt;, or use &lt;span class="sb"&gt;`&lt;/span&gt;~/repos&lt;span class="sb"&gt;`&lt;/span&gt; as the default &lt;span class="k"&gt;if &lt;/span&gt;they don&lt;span class="s1"&gt;'t specify.

2. Find all git repositories in that directory:
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
bash&lt;br&gt;
   find ~/repos -maxdepth 2 -name ".git" -type d | sed 's|/.git||'&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
1. For each repository found, get commits from the last 24 hours authored by the current git user:

2. Collect all results. Ignore repos with no recent commits.

3. Group the commits by repository name and draft a standup update in this format:

4. Keep the language concise and past-tense. Replace jargon-heavy commit messages with plain descriptions where possible.

5. Output the draft in a code block so it's easy to copy.

## Example trigger phrases

- &amp;lt;q&amp;gt;Give me my standup notes&amp;lt;/q&amp;gt;
- &amp;lt;q&amp;gt;What did I work on yesterday?&amp;lt;/q&amp;gt;
- &amp;lt;q&amp;gt;Draft my standup for today&amp;lt;/q&amp;gt;
- &amp;lt;q&amp;gt;Standup summary&amp;lt;/q&amp;gt;```



### Step 3 — Test it

Restart the OpenClaw gateway to pick up the new skill:



```shell
openclaw gateway restart

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then send your bot a message:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;q&gt;Give me my standup notes&lt;/q&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;OpenClaw will run the git commands, collect the output, and return a formatted standup summary. Because the instructions are plain English, you can iterate on the output by replying — &lt;q&gt;make it shorter&lt;/q&gt; or &lt;q&gt;use first person&lt;/q&gt; — without editing the skill.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — Extend it with an automatic post
&lt;/h3&gt;

&lt;p&gt;Once the notes look right, extend the skill to post them directly to your team's Slack channel. Add a section to the SKILL.md:&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;## Posting to Slack (optional)&lt;/span&gt;

If the user asks to &lt;span class="s2"&gt;"post"&lt;/span&gt; or &lt;span class="s2"&gt;"send"&lt;/span&gt; the standup, use the Slack skill &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if &lt;/span&gt;installed&lt;span class="o"&gt;)&lt;/span&gt; to post the formatted message to the &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="c"&gt;#standup` channel. If the Slack skill is not available, output the message and tell the user to copy it manually.&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the full workflow — generate, review, post — happens in one conversation turn.&lt;/p&gt;




&lt;h2&gt;
  
  
  Skill SKILL.md Reference
&lt;/h2&gt;

&lt;p&gt;Here are the frontmatter fields you'll use most often:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Unique identifier (used by clawhub)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;One-line summary injected into the system prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user-invocable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;boolean&lt;/td&gt;
&lt;td&gt;Exposes the skill as a &lt;code&gt;/skill-name&lt;/code&gt; slash command (default: &lt;code&gt;true&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;disable-model-invocation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;boolean&lt;/td&gt;
&lt;td&gt;Excludes the skill from model prompts — use for slash-command-only tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;metadata&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSON (single line)&lt;/td&gt;
&lt;td&gt;Gating, emoji, OS requirements, installer specs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;metadata.openclaw.requires&lt;/code&gt; object gates the skill so it only loads when its dependencies are met:&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="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-tool&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Does something with jq and an API key&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openclaw"&lt;/span&gt;&lt;span class="pi"&gt;:{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requires"&lt;/span&gt;&lt;span class="pi"&gt;:{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bins"&lt;/span&gt;&lt;span class="pi"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jq"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl"&lt;/span&gt;&lt;span class="pi"&gt;],&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;env"&lt;/span&gt;&lt;span class="pi"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MY_API_KEY"&lt;/span&gt;&lt;span class="pi"&gt;]}}}&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;jq&lt;/code&gt; isn't on &lt;code&gt;PATH&lt;/code&gt; or &lt;code&gt;MY_API_KEY&lt;/code&gt; isn't set, OpenClaw skips the skill entirely — it won't show up in the agent's prompt or the UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tips for Writing Effective Skills
&lt;/h2&gt;

&lt;p&gt;Writing a skill that works on your machine is straightforward. Writing one that works reliably across environments and for other users takes a bit more discipline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be explicit about prerequisites.&lt;/strong&gt; Always declare required binaries and environment variables in the &lt;code&gt;metadata.openclaw.requires&lt;/code&gt; block. A skill that silently fails because &lt;code&gt;jq&lt;/code&gt; isn't installed wastes debugging time. If your skill needs a specific version of a tool, mention it in the instructions — the requires block only checks for presence, not version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write instructions for the model, not for humans.&lt;/strong&gt; The LLM reads your SKILL.md, so phrase steps as direct commands rather than explanations. &lt;q&gt;Run &lt;code&gt;docker ps&lt;/code&gt; and parse the output&lt;/q&gt; is better than &lt;q&gt;You might want to check which containers are running.&lt;/q&gt; Ambiguity in instructions leads to inconsistent behaviour across different models and context windows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep descriptions short and specific.&lt;/strong&gt; The &lt;code&gt;description&lt;/code&gt; field is injected into the system prompt on every conversation. A verbose description eats tokens that could be used for reasoning. Aim for 10-15 words that clearly state what the skill does and when to use it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handle failure gracefully.&lt;/strong&gt; Include instructions for what the agent should do when a command returns an error or produces unexpected output. &lt;q&gt;If the API returns a 401, tell the user their token may have expired&lt;/q&gt; prevents the agent from guessing or hallucinating a recovery path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test with a clean environment.&lt;/strong&gt; Before publishing, test your skill on a machine that doesn't have your personal dotfiles, aliases, or globally installed packages. What works in your shell might not work on a fresh Ubuntu server where the only tools available are what's declared in the skill's requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope each skill to one task.&lt;/strong&gt; A skill that tries to do everything — search, filter, format, post, and archive — is harder to maintain and more likely to confuse the model. Break complex workflows into smaller skills that compose together. The standup-notes example above delegates posting to the Slack skill rather than reimplementing Slack integration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Troubleshooting Common Issues
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Skill not appearing in the agent's prompt.&lt;/strong&gt; The most common cause is a missing or malformed &lt;code&gt;metadata.openclaw.requires&lt;/code&gt; block. If the skill requires a binary that isn't installed or an environment variable that isn't set, OpenClaw silently skips it. Run &lt;code&gt;openclaw skills list&lt;/code&gt; to see which skills loaded and which were skipped, along with the reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skill loads but doesn't trigger.&lt;/strong&gt; Check the &lt;code&gt;description&lt;/code&gt; field — if it's too vague, the model may not recognise when to invoke it. A description like &lt;q&gt;Useful helper tool&lt;/q&gt; gives the LLM almost nothing to work with. Rewrite it to match the kind of phrases users actually say: &lt;q&gt;Generates daily standup notes from recent git commits.&lt;/q&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permission denied errors.&lt;/strong&gt; Skills that run shell commands inherit the permissions of the OpenClaw process. If you're running OpenClaw as a non-root user (which you should be), commands like &lt;code&gt;systemctl restart nginx&lt;/code&gt; will fail. Either configure passwordless sudo for specific commands, or have the skill check permissions first and return a clear message instead of a raw error trace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependency not found on remote servers.&lt;/strong&gt; A skill that works locally may fail on your VPS because a binary (e.g., &lt;code&gt;jq&lt;/code&gt;, &lt;code&gt;gh&lt;/code&gt;, &lt;code&gt;docker&lt;/code&gt;) isn't installed there. Add all system-level dependencies to your server provisioning script or Dockerfile. Declaring them in the skill's &lt;code&gt;requires.bins&lt;/code&gt; block prevents the skill from loading at all, which is better than it loading and then crashing mid-execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conflicting skills.&lt;/strong&gt; If two skills have overlapping trigger patterns, the model may pick the wrong one. The easiest fix is to make the &lt;code&gt;description&lt;/code&gt; fields more distinct. If both skills live in the same precedence tier (e.g., both in &lt;code&gt;~/.openclaw/skills&lt;/code&gt;), the one loaded first alphabetically wins — rename the folder to control order if needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Changes not taking effect after editing.&lt;/strong&gt; OpenClaw reads skills at gateway startup. After editing a &lt;code&gt;SKILL.md&lt;/code&gt;, you need to run &lt;code&gt;openclaw gateway restart&lt;/code&gt; for the changes to take effect. Hot-reloading is on the roadmap but not yet implemented.&lt;/p&gt;




&lt;h2&gt;
  
  
  Publishing to ClawHub
&lt;/h2&gt;

&lt;p&gt;When your skill works reliably and you want to share it:&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;# From the skills directory&lt;/span&gt;
clawhub publish ./standup-notes &lt;span class="nt"&gt;--slug&lt;/span&gt; standup-notes &lt;span class="nt"&gt;--version&lt;/span&gt; 1.0.0

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Publishing requires a GitHub account at least a week old (abuse prevention). The registry runs automated checks for obviously malicious patterns, but passes it to you to write a clear README and document what environment variables the skill uses and why.&lt;/p&gt;

&lt;p&gt;For updates, bump the version and republish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clawhub publish ./standup-notes &lt;span class="nt"&gt;--slug&lt;/span&gt; standup-notes &lt;span class="nt"&gt;--version&lt;/span&gt; 1.1.0

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Keeping Skills in Sync Across Servers with DeployHQ
&lt;/h2&gt;

&lt;p&gt;If you're running OpenClaw on a VPS — or across multiple servers — keeping skills in sync manually is error-prone. The same Git-based approach from the VPS deployment guide applies here: store your custom skills in a repository and let an &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;automated deployment platform&lt;/a&gt; push changes to every server automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-openclaw-config/
├── config.json
└── skills/
    ├── standup-notes/
    │ └── SKILL.md
    └── deploy-digest/
        └── SKILL.md

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set up &lt;a href="https://www.deployhq.com/features/automatic-deployments" rel="noopener noreferrer"&gt;Git deployment automation&lt;/a&gt; so that every push to your main branch triggers a deployment. Add a post-deployment command using &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;build pipelines&lt;/a&gt; to copy the skills into place and restart the gateway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rsync &lt;span class="nt"&gt;-av&lt;/span&gt; skills/ ~/.openclaw/skills/
openclaw gateway restart

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time you add or update a skill and push to your repository, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; deploys it to all connected servers — no SSH sessions required. If a skill breaks something, use &lt;a href="https://www.deployhq.com/features/one-click-rollback" rel="noopener noreferrer"&gt;one-click rollback&lt;/a&gt; to revert to the previous working version from the dashboard.&lt;/p&gt;

&lt;p&gt;For teams with larger budgets or multiple environments (staging, production), DeployHQ's &lt;a href="https://www.deployhq.com/pricing" rel="noopener noreferrer"&gt;pricing plans&lt;/a&gt; include multiple server targets per project — deploy to staging first, verify the skill works, then promote to production.&lt;/p&gt;




&lt;p&gt;The Skills ecosystem is what makes OpenClaw genuinely composable. Start with a few skills from ClawHub, modify them to match your workflow, and build custom ones for the tasks that are specific to your team. The bar to entry is low — if you can write a clear step-by-step process in plain English, you can write a skill.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Sign up for DeployHQ&lt;/a&gt; to automate skill deployments across your VPS fleet, or drop us a line at &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; if you have questions. Find us on Twitter at &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;@deployhq&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>openclaw</category>
      <category>vps</category>
      <category>skills</category>
    </item>
    <item>
      <title>SOC 2 Compliance for Deployment Workflows: What Auditors Look For</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Thu, 02 Apr 2026 17:38:52 +0000</pubDate>
      <link>https://dev.to/deployhq/soc-2-compliance-for-deployment-workflows-what-auditors-look-for-g1e</link>
      <guid>https://dev.to/deployhq/soc-2-compliance-for-deployment-workflows-what-auditors-look-for-g1e</guid>
      <description>&lt;p&gt;Every tool in your deployment pipeline is now under scrutiny. If your team is pursuing SOC 2 Type II certification — or any compliance framework that governs how software changes reach production — your deployment workflow is no longer &lt;q&gt;just DevOps.&lt;/q&gt; It's an audit surface.&lt;/p&gt;

&lt;p&gt;This guide breaks down what SOC 2 auditors actually look for in deployment tooling, which controls matter most, and how to configure your pipeline so it passes muster without slowing your team down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Your Deployment Pipeline Is an Audit Target
&lt;/h2&gt;

&lt;p&gt;SOC 2's Trust Service Criteria (TSC) don't mention &lt;q&gt;CI/CD&lt;/q&gt; or &lt;q&gt;deployment tools&lt;/q&gt; by name. But several controls map directly to how code moves from a developer's machine to production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CC6.1–CC6.2 (Logical Access)&lt;/strong&gt;: Who can trigger deployments? How are they authenticated?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CC6.3 (Role-Based Access)&lt;/strong&gt;: Can a junior developer deploy to production? Should they be able to?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CC7.1–CC7.2 (System Monitoring)&lt;/strong&gt;: Do you have immutable logs of every deployment — who, what, when, where?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CC8.1 (Change Management)&lt;/strong&gt;: Are production changes reviewed and approved before they go live?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CC9.2 (Vendor Risk)&lt;/strong&gt;: Does your deployment tool vendor meet SOC 2 requirements themselves?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The uncomfortable truth: &lt;strong&gt;68% of SOC 2 qualified opinions (failures) stem from access control weaknesses&lt;/strong&gt; in CC6.1–CC6.8. Your deployment tool is often the weakest link because it has direct write access to production servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Six Controls Auditors Actually Check
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Authentication: SSO and MFA
&lt;/h3&gt;

&lt;p&gt;Auditors want to see that access to deployment tooling is consolidated through your identity provider (IdP). This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single Sign-On (SSO)&lt;/strong&gt; via SAML or OIDC — so access is governed by your IdP (Okta, Azure AD, Google Workspace), not a separate username/password&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-Factor Authentication (MFA)&lt;/strong&gt; enforced at the organisation level, not optional per user&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Centralised deprovisioning&lt;/strong&gt; — when someone leaves the team, their deployment access is revoked automatically through the IdP, not manually by an admin who might forget&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters&lt;/strong&gt; : If your deployment tool uses standalone credentials, every audit cycle requires manual evidence that offboarded employees had their access revoked. SSO makes this automatic and auditable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DeployHQ approach&lt;/strong&gt; : &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; supports team-based access control with configurable permissions per project and server group. For teams requiring SSO, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; integrates with enterprise identity providers to centralise authentication.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Role-Based Access Control (RBAC)
&lt;/h3&gt;

&lt;p&gt;Not everyone who can view a project should be able to deploy to production. Auditors look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Separation of duties&lt;/strong&gt; — developers can deploy to staging; only designated leads or automated pipelines deploy to production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Granular permissions&lt;/strong&gt; — per-environment, per-server, per-action (view, deploy, configure)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documented access policies&lt;/strong&gt; — who has what access and why
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[Developer] --&amp;gt;|Can deploy| B[Staging]
    A -.-&amp;gt;|Cannot deploy| C[Production]
    D[Team Lead] --&amp;gt;|Can deploy| B
    D --&amp;gt;|Can deploy| C
    E[CI Pipeline] --&amp;gt;|Auto-deploy| B
    E --&amp;gt;|Requires approval| C

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;DeployHQ approach&lt;/strong&gt; : DeployHQ's permission system lets you assign roles per project — restricting who can deploy to which server groups. You can set up configurations where staging deploys happen automatically on push while production deploys require manual trigger from authorised users.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Immutable Audit Trails
&lt;/h3&gt;

&lt;p&gt;Every deployment must produce a tamper-evident log entry containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who&lt;/strong&gt; triggered the deployment (authenticated identity, not just &lt;q&gt;admin&lt;/q&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What&lt;/strong&gt; was deployed (commit SHA, branch, changed files)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When&lt;/strong&gt; the deployment occurred (timestamp)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where&lt;/strong&gt; it was deployed to (server, environment)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whether&lt;/strong&gt; it succeeded or failed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What changed&lt;/strong&gt; — a diff or file manifest of what was actually transferred&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These logs must be &lt;strong&gt;immutable&lt;/strong&gt; — no one should be able to delete or modify deployment records after the fact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DeployHQ approach&lt;/strong&gt; : &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; maintains a complete deployment history for every project, including the triggering user, commit reference, target servers, transferred files, and build output. This history is accessible via the dashboard and API for audit evidence collection.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Change Management and Approval Gates
&lt;/h3&gt;

&lt;p&gt;SOC 2's CC8.1 requires that changes to production are authorised before they take effect. In practice, this means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Branch protection rules&lt;/strong&gt; — production deployments only from protected branches (e.g., &lt;code&gt;main&lt;/code&gt;) that require pull request reviews&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual approval gates&lt;/strong&gt; — someone must explicitly approve production deploys, even if staging is automated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emergency change procedures&lt;/strong&gt; — a documented process for hotfixes that bypasses normal review, with after-the-fact review requirements
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Code Push] --&amp;gt; B{Branch?}
    B --&amp;gt;|feature/*| C[Run Tests]
    C --&amp;gt; D[Deploy to Staging]
    B --&amp;gt;|main| E{PR Approved?}
    E --&amp;gt;|Yes| F[Deploy to Production]
    E --&amp;gt;|No| G[Block Deployment]
    F --&amp;gt; H[Log Deployment]
    D --&amp;gt; H

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;DeployHQ approach&lt;/strong&gt; : &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; supports conditional deployments — you can configure automatic deployments for staging branches while requiring manual triggers for production. Combined with your Git provider's branch protection, this creates a documented approval chain from code review to production deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Environment Separation
&lt;/h3&gt;

&lt;p&gt;Auditors verify that development, staging, and production environments are isolated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Separate credentials&lt;/strong&gt; — production server credentials are not shared with staging&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network isolation&lt;/strong&gt; — staging cannot accidentally write to production databases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent configurations&lt;/strong&gt; — environment variables, API keys, and secrets are managed per environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DeployHQ approach&lt;/strong&gt; : DeployHQ's server groups allow you to define separate deployment targets with independent credentials, paths, and configurations for each environment. Each server group has its own deployment history and access controls.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Vendor Compliance (Your Tools Are Part of Your Audit)
&lt;/h3&gt;

&lt;p&gt;Under CC9.2, auditors assess the compliance posture of your third-party vendors. For deployment tools, this means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the vendor have their own SOC 2 Type II report?&lt;/li&gt;
&lt;li&gt;What data does the vendor access? (Server credentials, source code, environment variables)&lt;/li&gt;
&lt;li&gt;How does the vendor store and protect sensitive data?&lt;/li&gt;
&lt;li&gt;What happens if the vendor has a security incident?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What to ask your deployment tool vendor&lt;/strong&gt; :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Do you have a current SOC 2 Type II report?&lt;/li&gt;
&lt;li&gt;How are server credentials stored? (At rest encryption? Key management?)&lt;/li&gt;
&lt;li&gt;Who at your organisation can access customer deployment configurations?&lt;/li&gt;
&lt;li&gt;What's your incident response process?&lt;/li&gt;
&lt;li&gt;Do you support data residency requirements?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Beyond SOC 2: Other Compliance Frameworks
&lt;/h2&gt;

&lt;p&gt;SOC 2 isn't the only framework that cares about your deployment pipeline. Here's how the requirements map across frameworks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;SOC 2&lt;/th&gt;
&lt;th&gt;ISO 27001&lt;/th&gt;
&lt;th&gt;HIPAA&lt;/th&gt;
&lt;th&gt;PCI DSS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SSO/MFA&lt;/td&gt;
&lt;td&gt;CC6.1–CC6.2&lt;/td&gt;
&lt;td&gt;A.9.4.2&lt;/td&gt;
&lt;td&gt;§164.312(d)&lt;/td&gt;
&lt;td&gt;Req 8.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RBAC&lt;/td&gt;
&lt;td&gt;CC6.3&lt;/td&gt;
&lt;td&gt;A.9.2.3&lt;/td&gt;
&lt;td&gt;§164.312(a)(1)&lt;/td&gt;
&lt;td&gt;Req 7.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit trails&lt;/td&gt;
&lt;td&gt;CC7.1–CC7.2&lt;/td&gt;
&lt;td&gt;A.12.4.1&lt;/td&gt;
&lt;td&gt;§164.312(b)&lt;/td&gt;
&lt;td&gt;Req 10.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change management&lt;/td&gt;
&lt;td&gt;CC8.1&lt;/td&gt;
&lt;td&gt;A.14.2.2&lt;/td&gt;
&lt;td&gt;§164.312(e)(1)&lt;/td&gt;
&lt;td&gt;Req 6.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vendor management&lt;/td&gt;
&lt;td&gt;CC9.2&lt;/td&gt;
&lt;td&gt;A.15.1.1&lt;/td&gt;
&lt;td&gt;§164.308(b)(1)&lt;/td&gt;
&lt;td&gt;Req 12.8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The good news: if your deployment pipeline meets SOC 2 requirements, you're largely covered for these other frameworks too. The controls are complementary, not conflicting.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Compliance Checklist for Your Deployment Pipeline
&lt;/h2&gt;

&lt;p&gt;Use this checklist to assess your current deployment workflow against SOC 2 requirements:&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication &amp;amp; Access
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[] SSO enabled for all deployment tool access&lt;/li&gt;
&lt;li&gt;[] MFA enforced at the organisation level (not optional)&lt;/li&gt;
&lt;li&gt;[] Automated deprovisioning when team members leave&lt;/li&gt;
&lt;li&gt;[] No shared credentials or service accounts used for interactive access&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Permissions &amp;amp; Roles
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[] Production deployment restricted to authorised roles&lt;/li&gt;
&lt;li&gt;[] Staging and production use separate permission sets&lt;/li&gt;
&lt;li&gt;[] Access reviews conducted quarterly (documented)&lt;/li&gt;
&lt;li&gt;[] Principle of least privilege applied to all roles&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Audit &amp;amp; Logging
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[] Every deployment logged with user identity, commit, timestamp, and target&lt;/li&gt;
&lt;li&gt;[] Deployment logs are immutable (cannot be deleted or modified)&lt;/li&gt;
&lt;li&gt;[] Logs retained for the audit period (typically 12 months)&lt;/li&gt;
&lt;li&gt;[] Failed deployments logged with error details&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Change Management
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[] Production deployments require prior approval (PR review or manual gate)&lt;/li&gt;
&lt;li&gt;[] Emergency change process documented and followed&lt;/li&gt;
&lt;li&gt;[] Rollback procedures documented and tested&lt;/li&gt;
&lt;li&gt;[] All deployments traceable to a specific code change (commit SHA)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Environment Isolation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[] Separate credentials for each environment&lt;/li&gt;
&lt;li&gt;[] Production configuration not accessible from staging&lt;/li&gt;
&lt;li&gt;[] Environment-specific secrets management&lt;/li&gt;
&lt;li&gt;[] Network segmentation between environments&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Vendor Management
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[] Deployment tool vendor has current SOC 2 Type II report&lt;/li&gt;
&lt;li&gt;[] Vendor security review conducted annually&lt;/li&gt;
&lt;li&gt;[] Data processing agreement (DPA) in place&lt;/li&gt;
&lt;li&gt;[] Incident notification procedures agreed upon&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;If your team is beginning the SOC 2 journey, here's a practical order of operations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Audit your current state&lt;/strong&gt; — map out who has access to what, how deployments are triggered, and where the gaps are&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix authentication first&lt;/strong&gt; — SSO and MFA have the highest ROI because they address the most common audit failures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable audit logging&lt;/strong&gt; — make sure every deployment is logged with enough detail for an auditor to reconstruct the event&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement approval gates&lt;/strong&gt; — start with production, then extend to other sensitive environments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document everything&lt;/strong&gt; — auditors care as much about documented policies as they do about technical controls&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal isn't to make deployments slower. It's to make them &lt;strong&gt;provably secure&lt;/strong&gt; — so that when an auditor asks &lt;q&gt;who deployed this change and why?&lt;/q&gt;, you have a clear, automatic answer.&lt;/p&gt;




&lt;p&gt;If you're evaluating how &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; fits into your compliance workflow, you can explore our &lt;a href="https://www.deployhq.com/features" rel="noopener noreferrer"&gt;features&lt;/a&gt; or &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;start a free trial&lt;/a&gt; to test the deployment controls against your audit requirements.&lt;/p&gt;

&lt;p&gt;For questions about enterprise features including SSO, contact us at &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or reach out on &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;X (@deployhq)&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devopsinfrastructure</category>
      <category>security</category>
      <category>soc</category>
      <category>audits</category>
    </item>
  </channel>
</rss>
