<?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 Vaultwarden (Bitwarden) on a VPS with Docker</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Tue, 07 Jul 2026 07:18:28 +0000</pubDate>
      <link>https://dev.to/deployhq/self-host-vaultwarden-bitwarden-on-a-vps-with-docker-p9i</link>
      <guid>https://dev.to/deployhq/self-host-vaultwarden-bitwarden-on-a-vps-with-docker-p9i</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/dani-garcia/vaultwarden" rel="noopener noreferrer"&gt;Vaultwarden&lt;/a&gt; is an open-source, Rust-rewritten implementation of the Bitwarden server. It speaks the official Bitwarden protocol, so every Bitwarden client (web, desktop, mobile, browser extensions, CLI) works against it unchanged — but you run it on your own server, store your own data, and stop paying $10 a month for premium features that the official Bitwarden Free tier holds back.&lt;/p&gt;

&lt;p&gt;This guide takes you from a fresh VPS to a hardened Vaultwarden instance running at &lt;code&gt;vault.yourdomain.com&lt;/code&gt;, with TLS, an admin panel, push notifications for mobile clients, and a backup strategy that doesn't lose your entire vault on a bad disk. It is opinionated about the production setup the official quickstart leaves out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why self-host Vaultwarden
&lt;/h2&gt;

&lt;p&gt;The trade-off vs Bitwarden's hosted service is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; Bitwarden Free is genuinely useful but capped (no TOTP, no organization sharing, no premium reports). Bitwarden Premium is $10/year per individual or $40/year for families. Vaultwarden gives you all of those features — TOTP, attachments, emergency access, organizations, premium reports — for the cost of a $5 VPS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sovereignty.&lt;/strong&gt; Your encrypted vault never leaves a server you control. Bitwarden's hosted service is highly trusted (zero-knowledge architecture, audited), but if &lt;q&gt;no third party ever touches the encrypted blob&lt;/q&gt; is your bar, self-hosting is the only answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance and latency.&lt;/strong&gt; A small VPS in your region serves the API faster than Bitwarden's transatlantic infrastructure. Mobile autofill feels snappier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single source for your team.&lt;/strong&gt; Agencies and small companies often want a private vault without paying per-seat. Vaultwarden organizations cover that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-offs are also honest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You operate it.&lt;/strong&gt; Bitwarden handles backups, uptime, security patches, and key rotation. With Vaultwarden, that is on you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No iOS app review parity.&lt;/strong&gt; The mobile clients connect to your server fine, but you do not get Bitwarden's enterprise compliance attestations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backups are non-negotiable.&lt;/strong&gt; Lose the SQLite or Postgres data file without backups and every password is gone. There is no recovery.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can commit to backups and a 30-minute monthly upgrade window, Vaultwarden is one of the highest-leverage self-hosted services you can run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Docker
&lt;/h2&gt;

&lt;p&gt;Vaultwarden ships an official Docker image (&lt;code&gt;vaultwarden/server:latest&lt;/code&gt;) that the project itself recommends as the supported install path. The native binary install is documented but second-class — every release ships a Docker image first. The image is small (~50 MB), the data lives in a single mounted volume, and upgrades are a &lt;code&gt;docker compose pull &amp;amp;&amp;amp; docker compose up -d&lt;/code&gt; away.&lt;/p&gt;

&lt;p&gt;If you have followed our &lt;a href="https://dev.to/deployhq/self-hosting-paperclip-on-a-vps-with-docker-and-continuous-deployment-4hh5-temp-slug-5506684"&gt;self-host Paperclip with Docker&lt;/a&gt;, &lt;a href="https://www.deployhq.com/blog/deploying-n8n-on-alibaba-cloud-using-docker" rel="noopener noreferrer"&gt;n8n on Alibaba Cloud Linux 3&lt;/a&gt;, or &lt;a href="https://www.deployhq.com/blog/self-host-nextcloud-like-a-pro-deployhq-contabo-vps-tutorial" rel="noopener noreferrer"&gt;self-host Nextcloud on a VPS&lt;/a&gt; guides, the playbook here is identical — only the image and env vars change.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A VPS with at least 1 vCPU and 1 GB RAM (Vaultwarden is genuinely lightweight). 25 GB SSD covers years of vault growth.&lt;/li&gt;
&lt;li&gt;A domain or subdomain (&lt;code&gt;vault.yourdomain.com&lt;/code&gt;) with DNS pointing to the VPS.&lt;/li&gt;
&lt;li&gt;Docker Engine and the Compose plugin on the VPS — see &lt;a href="https://dev.to/deployhq/whats-the-easiest-way-to-deploy-on-a-vps-21af"&gt;the easiest way to deploy on a VPS&lt;/a&gt; for the broader setup pattern.&lt;/li&gt;
&lt;li&gt;An SMTP service (Postmark, SendGrid, Amazon SES, or your provider) for invitations, password hints, and 2FA fallback emails. Free tiers are fine.&lt;/li&gt;
&lt;li&gt;A reverse proxy on the VPS — we use Caddy in this guide. Concepts are covered in our &lt;a href="https://www.deployhq.com/blog/what-is-a-reverse-proxy-nginx-apache-and-caddy-explained" rel="noopener noreferrer"&gt;reverse proxy 101&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install Docker on a fresh Ubuntu VPS:&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 | &lt;span class="nb"&gt;sudo &lt;/span&gt;sh
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="nv"&gt;$USER&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Log out and back in for the group change to apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compose stack
&lt;/h2&gt;

&lt;p&gt;Vaultwarden's official quickstart shows a one-line &lt;code&gt;docker run&lt;/code&gt;. For production, the Compose file below adds Postgres (the SQLite default works but is not what you want for a multi-user setup), proper volume management, healthchecks, and an internal-only port binding so Caddy is the only thing on the public internet.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;/opt/vaultwarden/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;vaultwarden&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;vaultwarden/server:1.32-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vaultwarden&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;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:8080:80"&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;DOMAIN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://vault.${DOMAIN_BASE}"&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql://vault:${DB_PASSWORD}@db:5432/vaultwarden"&lt;/span&gt;
      &lt;span class="na"&gt;ADMIN_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${ADMIN_TOKEN}"&lt;/span&gt;
      &lt;span class="na"&gt;SIGNUPS_ALLOWED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false"&lt;/span&gt;
      &lt;span class="na"&gt;INVITATIONS_ALLOWED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
      &lt;span class="na"&gt;WEBSOCKET_ENABLED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
      &lt;span class="na"&gt;SENDS_ALLOWED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
      &lt;span class="na"&gt;EMERGENCY_ACCESS_ALLOWED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
      &lt;span class="na"&gt;SMTP_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${SMTP_HOST}"&lt;/span&gt;
      &lt;span class="na"&gt;SMTP_FROM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${SMTP_FROM}"&lt;/span&gt;
      &lt;span class="na"&gt;SMTP_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;587"&lt;/span&gt;
      &lt;span class="na"&gt;SMTP_SECURITY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;starttls"&lt;/span&gt;
      &lt;span class="na"&gt;SMTP_USERNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${SMTP_USERNAME}"&lt;/span&gt;
      &lt;span class="na"&gt;SMTP_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${SMTP_PASSWORD}"&lt;/span&gt;
      &lt;span class="na"&gt;LOG_LEVEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warn"&lt;/span&gt;
      &lt;span class="na"&gt;EXTENDED_LOGGING&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&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;vw-data:/data&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;db&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;db&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:17-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vaultwarden-db&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;vault&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;vaultwarden&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${DB_PASSWORD}"&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;pgdata:/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;vault"&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;5s&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;10&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;vw-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Three details worth flagging.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;port 80 binds to &lt;code&gt;127.0.0.1&lt;/code&gt;, not &lt;code&gt;0.0.0.0&lt;/code&gt;&lt;/strong&gt;. Without that prefix, Docker exposes Vaultwarden directly on the public internet, bypassing your &lt;code&gt;ufw&lt;/code&gt;/&lt;code&gt;firewalld&lt;/code&gt; rules. Caddy on the same host proxies inbound HTTPS to it.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;the image is pinned to a version tag&lt;/strong&gt; (&lt;code&gt;1.32-alpine&lt;/code&gt;), not &lt;code&gt;:latest&lt;/code&gt;. With a password manager, &lt;q&gt;the version that happened to be in the registry when Watchtower last ran&lt;/q&gt; is the wrong upgrade strategy. Pin and upgrade deliberately.&lt;/p&gt;

&lt;p&gt;Third, &lt;strong&gt;&lt;code&gt;SIGNUPS_ALLOWED: "false"&lt;/code&gt;&lt;/strong&gt; is critical for any vault that lives on the public internet. Without it, anyone who finds the URL can register and start using your server's storage. Use &lt;code&gt;INVITATIONS_ALLOWED: "true"&lt;/code&gt; and invite users via the admin panel.&lt;/p&gt;

&lt;p&gt;Generate a strong &lt;code&gt;ADMIN_TOKEN&lt;/code&gt; — this protects the admin panel at &lt;code&gt;/admin&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;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 64

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

&lt;/div&gt;



&lt;p&gt;Vaultwarden recommends an Argon2-hashed token in 1.30+. Generate 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;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; vaultwarden/server:1.32-alpine /vaultwarden &lt;span class="nb"&gt;hash&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Paste the resulting &lt;code&gt;$argon2id$...&lt;/code&gt; string into &lt;code&gt;ADMIN_TOKEN&lt;/code&gt; (escape any &lt;code&gt;$&lt;/code&gt; if you put it in a shell-evaluated &lt;code&gt;.env&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Save the secrets to &lt;code&gt;/opt/vaultwarden/.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;DOMAIN_BASE&lt;/span&gt;=&lt;span class="n"&gt;yourdomain&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
&lt;span class="n"&gt;DB_PASSWORD&lt;/span&gt;=$(&lt;span class="n"&gt;openssl&lt;/span&gt; &lt;span class="n"&gt;rand&lt;/span&gt; -&lt;span class="n"&gt;hex&lt;/span&gt; &lt;span class="m"&gt;32&lt;/span&gt;)
&lt;span class="n"&gt;ADMIN_TOKEN&lt;/span&gt;=&amp;lt;&lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;argon2&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;above&lt;/span&gt;&amp;gt;
&lt;span class="n"&gt;SMTP_HOST&lt;/span&gt;=&lt;span class="n"&gt;smtp&lt;/span&gt;.&lt;span class="n"&gt;postmarkapp&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
&lt;span class="n"&gt;SMTP_FROM&lt;/span&gt;=&lt;span class="n"&gt;vault&lt;/span&gt;@&lt;span class="n"&gt;yourdomain&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
&lt;span class="n"&gt;SMTP_USERNAME&lt;/span&gt;=&amp;lt;&lt;span class="n"&gt;your&lt;/span&gt; &lt;span class="n"&gt;postmark&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&amp;gt;
&lt;span class="n"&gt;SMTP_PASSWORD&lt;/span&gt;=&amp;lt;&lt;span class="n"&gt;your&lt;/span&gt; &lt;span class="n"&gt;postmark&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&amp;gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;chmod 600 /opt/vaultwarden/.env&lt;/code&gt;. This file is the keys to the kingdom.&lt;/p&gt;

&lt;h2&gt;
  
  
  TLS via Caddy
&lt;/h2&gt;

&lt;p&gt;Vaultwarden absolutely requires HTTPS. The Bitwarden clients refuse to connect over plain HTTP, and any password manager served over an unencrypted channel is malpractice anyway.&lt;/p&gt;

&lt;p&gt;Install Caddy (see &lt;a href="https://dev.to/deployhq/nginx-vs-apache-vs-caddy-choosing-the-right-web-server-19en"&gt;our Caddy/Nginx/Apache comparison&lt;/a&gt; for why we usually pick Caddy) and write &lt;code&gt;/etc/caddy/Caddyfile&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;vault.yourdomain.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;reverse_proxy&lt;/span&gt; &lt;span class="nf"&gt;127.0.0.1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;

    &lt;span class="c1"&gt;# WebSocket endpoint for live notifications&lt;/span&gt;
    &lt;span class="s"&gt;@websockets&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;path&lt;/span&gt; &lt;span class="n"&gt;/notifications/hub&lt;/span&gt;
    &lt;span class="err"&gt;}&lt;/span&gt;
    &lt;span class="s"&gt;reverse_proxy&lt;/span&gt; &lt;span class="s"&gt;@websockets&lt;/span&gt; &lt;span class="nf"&gt;127.0.0.1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;

    &lt;span class="s"&gt;encode&lt;/span&gt; &lt;span class="s"&gt;gzip&lt;/span&gt;
    &lt;span class="s"&gt;log&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;output&lt;/span&gt; &lt;span class="s"&gt;file&lt;/span&gt; &lt;span class="n"&gt;/var/log/caddy/vaultwarden.log&lt;/span&gt;
        &lt;span class="s"&gt;format&lt;/span&gt; &lt;span class="s"&gt;json&lt;/span&gt;
    &lt;span class="err"&gt;}&lt;/span&gt;

    &lt;span class="s"&gt;header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;Strict-Transport-Security&lt;/span&gt; &lt;span class="s"&gt;"max-age=63072000"&lt;/span&gt;
        &lt;span class="s"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt;
        &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s"&gt;"DENY"&lt;/span&gt;
        &lt;span class="s"&gt;Referrer-Policy&lt;/span&gt; &lt;span class="s"&gt;"no-referrer"&lt;/span&gt;
    &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sudo systemctl reload caddy&lt;/code&gt;. Caddy fetches a Let's Encrypt cert on first request and renews automatically.&lt;/p&gt;

&lt;p&gt;Start the stack:&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; /opt/vaultwarden
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; vaultwarden

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

&lt;/div&gt;



&lt;p&gt;Visit &lt;code&gt;https://vault.yourdomain.com&lt;/code&gt;. You should see the Bitwarden login screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  First-run: create your account, lock the door
&lt;/h2&gt;

&lt;p&gt;Because we set &lt;code&gt;SIGNUPS_ALLOWED: "false"&lt;/code&gt;, you cannot register via the public form. Two options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A — temporarily allow signups.&lt;/strong&gt; Set &lt;code&gt;SIGNUPS_ALLOWED: "true"&lt;/code&gt;, restart, register your account, then flip it back to &lt;code&gt;false&lt;/code&gt; and restart again. Crude but quick.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B — invite yourself via the admin panel.&lt;/strong&gt; Visit &lt;code&gt;https://vault.yourdomain.com/admin&lt;/code&gt;, paste your &lt;code&gt;ADMIN_TOKEN&lt;/code&gt;, click &lt;q&gt;Users → Invite&lt;/q&gt;, enter your email. Click the link in the email and finish account setup.&lt;/p&gt;

&lt;p&gt;Option B is the operationally clean version — admin invitations leave an audit trail, and you never have public signups enabled even briefly.&lt;/p&gt;

&lt;p&gt;Once you're in, install the Bitwarden clients on your devices and configure the server URL (in the app, before logging in: &lt;q&gt;Self-hosted environment → Server URL: &lt;a href="https://vault.yourdomain.com" rel="noopener noreferrer"&gt;https://vault.yourdomain.com&lt;/a&gt;&lt;/q&gt;). Your existing Bitwarden vault export imports cleanly via &lt;q&gt;Tools → Import data&lt;/q&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Push notifications for mobile clients
&lt;/h2&gt;

&lt;p&gt;Without push, your iOS and Android Bitwarden apps get vault changes by polling — which means stale data and slow autofill. To get real push:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Register for a free Bitwarden push subscription at &lt;a href="https://bitwarden.com/host/" rel="noopener noreferrer"&gt;bitwarden.com/host/&lt;/a&gt; (the same form Bitwarden uses for self-hosted Bitwarden Server).&lt;/li&gt;
&lt;li&gt;They email you &lt;code&gt;installation_id&lt;/code&gt; and &lt;code&gt;installation_key&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add to &lt;code&gt;.env&lt;/code&gt;:&lt;code&gt;
PUSH_ENABLED=true
PUSH_INSTALLATION_ID=&amp;lt;your id&amp;gt;
PUSH_INSTALLATION_KEY=&amp;lt;your key&amp;gt;
PUSH_RELAY_URI=https://push.bitwarden.com
&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Restart Vaultwarden. Mobile clients now receive push notifications via Bitwarden's relay (your encrypted blobs never go through it; only the &lt;q&gt;vault changed&lt;/q&gt; trigger does).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Backups: the part nobody covers
&lt;/h2&gt;

&lt;p&gt;A Vaultwarden instance without backups is a time bomb. The single Postgres volume (&lt;code&gt;pgdata&lt;/code&gt;) plus the &lt;code&gt;vw-data&lt;/code&gt; volume (which holds attachments, the RSA keys, and the Sends store) ARE your vault. Lose them, lose everything.&lt;/p&gt;

&lt;p&gt;Minimum viable backup:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# /usr/local/bin/vaultwarden-backup.sh&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/backups/vaultwarden
&lt;span class="nv"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d-%H%M%S&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Postgres dump&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;vaultwarden-db pg_dump &lt;span class="nt"&gt;-U&lt;/span&gt; vault vaultwarden | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/db-&lt;/span&gt;&lt;span class="nv"&gt;$TIMESTAMP&lt;/span&gt;&lt;span class="s2"&gt;.sql.gz"&lt;/span&gt;

&lt;span class="c"&gt;# Data volume (RSA keys, attachments, sends)&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; vaultwarden_vw-data:/data &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_DIR&lt;/span&gt;&lt;span class="s2"&gt;:/backup"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  alpine &lt;span class="nb"&gt;tar &lt;/span&gt;czf &lt;span class="s2"&gt;"/backup/vw-data-&lt;/span&gt;&lt;span class="nv"&gt;$TIMESTAMP&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz"&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; /data &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Retain last 30 days&lt;/span&gt;
find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-mtime&lt;/span&gt; +30 &lt;span class="nt"&gt;-delete&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Schedule it via cron at &lt;code&gt;0 3 * * *&lt;/code&gt;. &lt;strong&gt;Then ship the backups off-host&lt;/strong&gt; — to S3, B2, or another VPS — using &lt;code&gt;restic&lt;/code&gt; or &lt;code&gt;rclone&lt;/code&gt;. A backup that lives only on the server it backs up is not a backup; it is a hostage situation waiting for a disk failure.&lt;/p&gt;

&lt;p&gt;Test restoration once a quarter. The first time you discover your backup script silently failed for six months should not be the day your VPS dies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational concerns
&lt;/h2&gt;

&lt;p&gt;A few things worth setting up before you forget about the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fail2ban for the admin panel&lt;/strong&gt; and the login endpoint. Vaultwarden ships logs with explicit &lt;q&gt;[ERROR][...login attempt failed]&lt;/q&gt; markers; the &lt;a href="https://github.com/dani-garcia/vaultwarden/wiki/Fail2Ban-Setup" rel="noopener noreferrer"&gt;Vaultwarden wiki has a ready-made jail config&lt;/a&gt;. Without this, the &lt;code&gt;/admin&lt;/code&gt; endpoint is a brute-force target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate &lt;code&gt;ADMIN_TOKEN&lt;/code&gt; quarterly.&lt;/strong&gt; Roll a new Argon2 hash, update &lt;code&gt;.env&lt;/code&gt;, restart. Cheap, and it limits blast radius if the token leaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disable &lt;code&gt;WEBSOCKET_ENABLED&lt;/code&gt; only if you have to.&lt;/strong&gt; It powers real-time vault updates across logged-in clients; turning it off is a noticeable downgrade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor disk on the data volume.&lt;/strong&gt; Attachments and the Sends feature can grow over time. Set a &lt;code&gt;df&lt;/code&gt; alert at 80% to avoid waking up to a full disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patch on a schedule.&lt;/strong&gt; Subscribe to the &lt;a href="https://github.com/dani-garcia/vaultwarden/releases" rel="noopener noreferrer"&gt;Vaultwarden releases feed&lt;/a&gt;. Pin to a specific patch tag (&lt;code&gt;1.32.7-alpine&lt;/code&gt;), test upgrades on a staging VPS first, then promote.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Continuous deployment with DeployHQ
&lt;/h2&gt;

&lt;p&gt;If you maintain a fork of the Vaultwarden repo (custom branding, patched defaults, additional adapters) or simply want every upgrade to be a &lt;code&gt;git push&lt;/code&gt; instead of an SSH session, the same continuous-deployment pattern from our Paperclip walkthrough above applies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fork &lt;a href="https://github.com/dani-garcia/vaultwarden" rel="noopener noreferrer"&gt;&lt;code&gt;dani-garcia/vaultwarden&lt;/code&gt;&lt;/a&gt; (or just keep your &lt;code&gt;docker-compose.yml&lt;/code&gt; and &lt;code&gt;Caddyfile&lt;/code&gt; in a private deploy repo — for most users this is enough).&lt;/li&gt;
&lt;li&gt;Connect the repo to &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; and configure a &lt;a href="https://www.deployhq.com/features/docker-builds" rel="noopener noreferrer"&gt;Docker build environment&lt;/a&gt; — or skip the build and have &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; deploy only the compose file via SSH.&lt;/li&gt;
&lt;li&gt;Add the VPS as an SSH server.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;Deploy&lt;/a&gt; command: &lt;code&gt;docker compose -f /opt/vaultwarden/docker-compose.yml --env-file /opt/vaultwarden/.env pull &amp;amp;&amp;amp; docker compose -f /opt/vaultwarden/docker-compose.yml --env-file /opt/vaultwarden/.env up -d&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Push to your &lt;code&gt;production&lt;/code&gt; branch when you want to upgrade. Vaultwarden 1.32.6 → 1.32.7 is now a &lt;code&gt;git push&lt;/code&gt; away, with the upgrade visible in DeployHQ's audit log.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;You now have Vaultwarden on a VPS with TLS, admin panel access, push notifications, and a backup strategy that survives a disk failure. For under $10/month in VPS plus SMTP costs, you have an unlimited-user Bitwarden-compatible password manager that nobody else can read or revoke.&lt;/p&gt;

&lt;p&gt;If you're running this for a small team or family, &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 so the next Vaultwarden upgrade is a &lt;code&gt;git push&lt;/code&gt; instead of an SSH session. Pricing is on the &lt;a href="https://www.deployhq.com/pricing" rel="noopener noreferrer"&gt;plans page&lt;/a&gt;; the &lt;a href="https://www.deployhq.com/for-agencies" rel="noopener noreferrer"&gt;agency plan&lt;/a&gt; covers running Vaultwarden plus a dozen other self-hosted apps across multiple client VPSes.&lt;/p&gt;

&lt;p&gt;Questions about Vaultwarden, backup strategy, or wiring up automated upgrades? Email us at &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or ping &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;@deployhq&lt;/a&gt; on X.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>security</category>
      <category>tutorials</category>
      <category>vps</category>
    </item>
    <item>
      <title>How to Let AI Query Your Database Without Deleting It: Read-Only Guardrails for Claude Code</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Mon, 06 Jul 2026 09:04:36 +0000</pubDate>
      <link>https://dev.to/deployhq/how-to-let-ai-query-your-database-without-deleting-it-read-only-guardrails-for-claude-code-7j3</link>
      <guid>https://dev.to/deployhq/how-to-let-ai-query-your-database-without-deleting-it-read-only-guardrails-for-claude-code-7j3</guid>
      <description>&lt;p&gt;A story made the rounds recently that put a chill down the spine of every developer experimenting with AI coding tools: someone gave an AI assistant live access to a production database, asked it to help clean things up, and watched it issue destructive commands that wiped real data. The specifics vary depending on who's telling it, but the shape of the story is always the same — an autonomous agent, a real connection string, write permissions, and no seatbelt.&lt;/p&gt;

&lt;p&gt;Here's the uncomfortable truth: the AI wasn't malfunctioning. It did exactly what it was designed to do — generate and execute a plausible-looking command to accomplish the goal it was given. The failure wasn't the model. The failure was the access. We handed a probabilistic text generator the same keys we'd hesitate to give a brand-new junior engineer on their first day, and then acted surprised when it used them.&lt;/p&gt;

&lt;p&gt;The good news is that this is an entirely solvable problem, and the solution isn't &lt;q&gt;never let AI touch your database.&lt;/q&gt; AI is genuinely useful for exploring schemas, drafting queries, and debugging slow reports. The fix is to make destructive actions &lt;em&gt;impossible&lt;/em&gt;, not just discouraged. This guide walks through the concrete guardrails — real &lt;code&gt;GRANT&lt;/code&gt; statements, MCP-layer flags, sandbox patterns, and connection scoping — that let an AI query your database all day long without ever being able to delete it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why write access to prod goes wrong
&lt;/h2&gt;

&lt;p&gt;Before the recipe, it's worth being precise about the failure mode, because &lt;q&gt;the AI went rogue&lt;/q&gt; is the wrong mental model and leads to the wrong fixes.&lt;/p&gt;

&lt;p&gt;When you connect an AI coding assistant to a database — usually through a &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;Model Context Protocol server that bridges the model and your data&lt;/a&gt; — the model doesn't &lt;q&gt;understand&lt;/q&gt; your data the way you do. It pattern-matches. Ask it to &lt;q&gt;remove the test accounts&lt;/q&gt; and, if the connection it's been handed can run &lt;code&gt;DELETE&lt;/code&gt; and &lt;code&gt;DROP&lt;/code&gt;, it may generate a statement that's syntactically valid, semantically reasonable-looking, and catastrophically wrong: a missing &lt;code&gt;WHERE&lt;/code&gt; clause, a &lt;code&gt;TRUNCATE&lt;/code&gt; on the wrong table, a schema-altering migration it invented on the spot.&lt;/p&gt;

&lt;p&gt;Three factors compound the risk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Autonomy.&lt;/strong&gt; Agentic tools chain actions together. A single prompt can trigger a sequence of tool calls with no human confirming each SQL statement before it runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence without comprehension.&lt;/strong&gt; The model has no model of &lt;q&gt;this data is irreplaceable.&lt;/q&gt; It weighs a &lt;code&gt;DROP TABLE&lt;/code&gt; the same way it weighs a &lt;code&gt;SELECT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overprivileged connections.&lt;/strong&gt; Most developers connect using their personal credentials or the app's connection string — both of which typically carry full read/write/DDL rights.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix the third factor and the first two stop mattering. If the connection physically cannot execute a &lt;code&gt;DELETE&lt;/code&gt;, it does not matter how confidently the AI generates one. That's the principle everything below is built on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The read-only principle: dedicated roles, not shared credentials
&lt;/h2&gt;

&lt;p&gt;The single highest-leverage guardrail is also the oldest one in the database security playbook: &lt;strong&gt;give the AI its own database role, and grant that role only &lt;code&gt;SELECT&lt;/code&gt;.&lt;/strong&gt; No &lt;code&gt;INSERT&lt;/code&gt;, no &lt;code&gt;UPDATE&lt;/code&gt;, no &lt;code&gt;DELETE&lt;/code&gt;, no &lt;code&gt;DROP&lt;/code&gt;, no DDL. If the tool's only job is to read and analyze, it never needs anything else.&lt;/p&gt;

&lt;p&gt;This is defense the database itself enforces. It doesn't rely on the AI behaving well, on a system prompt being obeyed, or on a middleware layer catching a bad command. The permission simply isn't there.&lt;/p&gt;

&lt;p&gt;Here's the recipe for each of the big three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt; — create a login role, grant connect and read, and (critically) set default privileges so future tables are covered too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Create a dedicated, least-privilege login role&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;LOGIN&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'use-a-strong-secret-from-your-vault'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Allow it to connect and read the schema&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;CONNECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;myapp&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. Grant SELECT on all *existing* tables&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 4. Make sure tables created *later* are also readable (this is the step people forget)&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;
  &lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&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;ALTER DEFAULT PRIVILEGES&lt;/code&gt; line matters: without it, the AI role silently loses visibility every time you add a table, and someone &lt;q&gt;fixes&lt;/q&gt; it later by granting something broader. Lock it down once, correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MySQL / MariaDB&lt;/strong&gt; — the same idea, scoped to &lt;code&gt;SELECT&lt;/code&gt; only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'ai_readonly'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'use-a-strong-secret-from-your-vault'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;myapp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'ai_readonly'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;FLUSH&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;GRANT SELECT ON myapp.*&lt;/code&gt; is scoped at the database level, it automatically applies to tables you create later — no default-privileges dance required. Tighten the host portion (&lt;code&gt;'ai_readonly'@'10.0.%'&lt;/code&gt;) to your network rather than &lt;code&gt;'%'&lt;/code&gt; wherever you can.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQL Server&lt;/strong&gt; — this one is almost too easy, because it ships with a built-in role for exactly this purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;LOGIN&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'use-a-strong-secret-from-your-vault'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;LOGIN&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- db_datareader = SELECT on every current and future table in the database&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;db_datareader&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="n"&gt;MEMBER&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;db_datareader&lt;/code&gt; grants &lt;code&gt;SELECT&lt;/code&gt; on all user tables and views, current and future, and nothing else. It's the SQL Server answer to &lt;q&gt;read everything, change nothing.&lt;/q&gt;&lt;/p&gt;

&lt;p&gt;Whatever engine you're on, verify the guardrail the fun way: connect as the new role and try to break something. Run a &lt;code&gt;DELETE&lt;/code&gt; or a &lt;code&gt;DROP&lt;/code&gt;. You want a permission-denied error. If you get one, the AI will too — and no prompt injection, no hallucinated migration, no missing &lt;code&gt;WHERE&lt;/code&gt; clause can get past it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Setting up a least-privilege role is a one-time job that pays off every time an AI — or a script, or a new hire — touches your data. If you're already thinking about who can change what and when, that same discipline belongs in your deployment process: &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;DeployHQ's build pipelines&lt;/a&gt; let you gate exactly which commands run against an environment, so schema changes go through review instead of an ad-hoc terminal session. More on that below.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Guardrails at the MCP layer
&lt;/h2&gt;

&lt;p&gt;The read-only role is your backstop. The MCP server is your second line of defense, and it's worth configuring even when the role is already locked down — belt &lt;em&gt;and&lt;/em&gt; suspenders.&lt;/p&gt;

&lt;p&gt;Most AI database access flows through an MCP server that translates the model's requests into real queries. If you're new to the pattern, our &lt;a href="https://www.deployhq.com/blog/build-your-first-mcp-server-model-context-protocol-guide" rel="noopener noreferrer"&gt;complete guide to building an MCP server&lt;/a&gt; covers how these bridges work, and our roundup of the &lt;a href="https://dev.to/deployhq/best-mcp-servers-for-web-developers-in-2026-setup-guide-for-claude-code-cursor-and-windsurf-l7d-temp-slug-4689154"&gt;best MCP servers for web developers&lt;/a&gt; covers the ecosystem. For databases specifically, DBHub is a common choice, and it exposes two flags that matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--readonly&lt;/code&gt;&lt;/strong&gt; — DBHub refuses to execute any query that isn't a read. Even if the AI generates a &lt;code&gt;DELETE&lt;/code&gt;, the server rejects it before it reaches the database. This is a software-layer mirror of your read-only role, and having both means a misconfiguration in one doesn't expose you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--max-rows&lt;/code&gt;&lt;/strong&gt; — caps the number of rows any single query can return. This defends against a different failure: not destruction, but a runaway &lt;code&gt;SELECT * FROM events&lt;/code&gt; on a billion-row table that pins your database's CPU and memory. Set a sane ceiling (a few thousand rows) so an over-eager query degrades gracefully instead of taking prod down.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A typical hardened DBHub invocation looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dbhub &lt;span class="nt"&gt;--transport&lt;/span&gt; stdio &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dsn&lt;/span&gt; &lt;span class="s2"&gt;"postgres://ai_readonly:...@db-host:5432/myapp?sslmode=require"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--readonly&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-rows&lt;/span&gt; 5000

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

&lt;/div&gt;



&lt;p&gt;Note the DSN uses the &lt;code&gt;ai_readonly&lt;/code&gt; role from the previous section, not your admin credentials. The flags and the role reinforce each other. (DBHub's flags are documented in the &lt;a href="https://github.com/bytebase/dbhub" rel="noopener noreferrer"&gt;DBHub repository&lt;/a&gt; — worth reading before you wire it up.)&lt;/p&gt;

&lt;p&gt;If you're running this inside Claude Code, the &lt;a href="https://www.deployhq.com/cheatsheets/claude-code" rel="noopener noreferrer"&gt;Claude Code cheatsheet&lt;/a&gt; is a handy reference for the CLI flags and permission settings you'll touch while setting this up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sandbox pattern: give it a copy it can't hurt
&lt;/h2&gt;

&lt;p&gt;Read-only access covers the vast majority of what you'd actually want an AI to do with a database — explore, analyze, draft queries. But sometimes you genuinely need it to &lt;em&gt;write&lt;/em&gt;: test a migration, populate seed data, validate that a &lt;code&gt;DELETE&lt;/code&gt; does what you expect. For anything destructive, the rule is simple: &lt;strong&gt;never do it against a database you care about.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The pattern is a disposable copy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot or restore&lt;/strong&gt; a copy of the relevant data into a throwaway database — a local container, an ephemeral schema, or a restored backup on a scratch instance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Point the AI at the copy.&lt;/strong&gt; Give it a full read/write role &lt;em&gt;on that copy only&lt;/em&gt;. Now it can &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;TRUNCATE&lt;/code&gt;, and &lt;code&gt;ALTER&lt;/code&gt; to its heart's content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throw it away&lt;/strong&gt; when you're done. Nothing the AI did touched production.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A Docker-based scratch database is the fastest version of this:&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;# Spin up a disposable Postgres the AI can freely mutate&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; ai-sandbox &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sandbox &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 5433:5432 postgres:16

&lt;span class="c"&gt;# Load a sanitized dump into it&lt;/span&gt;
pg_restore &lt;span class="nt"&gt;-h&lt;/span&gt; localhost &lt;span class="nt"&gt;-p&lt;/span&gt; 5433 &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-d&lt;/span&gt; postgres ./sanitized-dump.dump

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

&lt;/div&gt;



&lt;p&gt;Two things make this safe rather than just convenient. First, the &lt;code&gt;--rm&lt;/code&gt; flag means the container — and everything the AI did to it — evaporates when you stop it. Second, and non-negotiable for anything derived from production: &lt;strong&gt;sanitize the dump.&lt;/strong&gt; Strip or mask PII before it lands anywhere an AI (and, via the model provider, potentially a third party) can read it. A sandbox that leaks real customer data isn't a safe sandbox.&lt;/p&gt;

&lt;p&gt;A file-based engine makes this pattern even cheaper: with a local SQLite database, a &lt;q&gt;disposable copy&lt;/q&gt; is a single &lt;code&gt;cp&lt;/code&gt; command — you experiment where undo is free, then promote.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connection scoping: SSH tunnels, TLS, and env-not-config
&lt;/h2&gt;

&lt;p&gt;Even a perfectly permissioned read-only role is a liability if the credentials leak or the connection is exposed. Scope the connection itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't expose the database to the internet.&lt;/strong&gt; The AI tool should reach your database over an SSH tunnel or a private network, not a publicly routable port. An SSH tunnel forwards a local port through an authenticated, encrypted channel:&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;# Forward local :5432 to the DB, reachable only via the bastion host&lt;/span&gt;
ssh &lt;span class="nt"&gt;-L&lt;/span&gt; 5432:db-internal:5432 deploy@bastion.example.com &lt;span class="nt"&gt;-N&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Then point the AI at &lt;code&gt;localhost:5432&lt;/code&gt;. The database's real port never faces the public internet. If you're already comfortable running deploy commands over SSH, this will feel familiar — our &lt;a href="https://www.deployhq.com/support/ssh-commands" rel="noopener noreferrer"&gt;SSH commands reference&lt;/a&gt; covers the connection patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Require TLS.&lt;/strong&gt; Encrypt the connection so credentials and query results can't be sniffed in transit. In Postgres that's &lt;code&gt;?sslmode=require&lt;/code&gt; (or stricter — &lt;code&gt;verify-full&lt;/code&gt; — if you've got the CA set up) on the DSN; MySQL and SQL Server have equivalents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep credentials in the environment, not in config files.&lt;/strong&gt; This is where a lot of otherwise-careful setups fall down. A connection string with a password committed to a &lt;code&gt;mcp.json&lt;/code&gt;, a &lt;code&gt;.env&lt;/code&gt; that's accidentally tracked, or a config file synced to a dotfiles repo is a credential leak waiting to happen. Reference secrets from environment variables or a secrets manager, and make sure the config that &lt;em&gt;does&lt;/em&gt; get committed contains only variable references:&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;good:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;file&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;"dsn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${DATABASE_URL_READONLY}"&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;If you're setting up Claude Code for the first time, our &lt;a href="https://dev.to/deployhq/getting-started-with-claude-code-the-ai-coding-assistant-for-your-terminal-4cba"&gt;getting-started guide for the terminal AI assistant&lt;/a&gt; walks through where its config lives and how to keep secrets out of it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Guardrails are worth setting up once and reusing everywhere. If you'd rather your infrastructure enforce these boundaries by default — scoped credentials, encrypted connections, reviewed changes — that's exactly the discipline &lt;a href="https://www.deployhq.com/features" rel="noopener noreferrer"&gt;DeployHQ's deployment features&lt;/a&gt; are built around. Wire your database changes into a pipeline instead of a prompt.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Where deploys fit: schema changes belong in a pipeline
&lt;/h2&gt;

&lt;p&gt;Here's the part most &lt;q&gt;AI database safety&lt;/q&gt; advice skips entirely. Once you've locked the AI down to read-only, a fair question is: &lt;em&gt;how do the actual, intended changes get made?&lt;/em&gt; Schema migrations, new indexes, column additions — those still need to happen.&lt;/p&gt;

&lt;p&gt;The answer is emphatically &lt;strong&gt;not&lt;/strong&gt; &lt;q&gt;temporarily grant the AI write access and let it run the migration over SSH.&lt;/q&gt; That reintroduces every risk you just eliminated, plus a new one: an unreviewed, unlogged, unrepeatable change to production with no rollback path.&lt;/p&gt;

&lt;p&gt;Schema changes belong in a &lt;strong&gt;reviewed build pipeline&lt;/strong&gt; , the same way application code does. The migration is written (with the AI's help, drafting against a sandbox is great) and committed to version control. It's reviewed by a human. It runs as a deploy step, in order, with the change logged and the previous state recoverable. This is exactly the model in our guide to &lt;a href="https://dev.to/deployhq/database-migration-strategies-for-zero-downtime-deployments-a-step-by-step-guide-2kpj"&gt;database migration strategies for zero-downtime deployments&lt;/a&gt;, and the broader practice of &lt;a href="https://www.deployhq.com/blog/database-deployments-made-easy-with-deployhq" rel="noopener noreferrer"&gt;making database deployments repeatable and reversible&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The contrast is stark:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ad-hoc AI SSH session&lt;/th&gt;
&lt;th&gt;Reviewed deploy pipeline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runs immediately, no review&lt;/td&gt;
&lt;td&gt;Human reviews before it runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No record of what changed&lt;/td&gt;
&lt;td&gt;Every change logged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No rollback&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.deployhq.com/features/one-click-rollback" rel="noopener noreferrer"&gt;One-click rollback&lt;/a&gt; to the prior state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs as an over-privileged user&lt;/td&gt;
&lt;td&gt;Runs as a scoped deploy identity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not repeatable across environments&lt;/td&gt;
&lt;td&gt;Same migration, staging then prod&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When a schema change is a version-controlled, reviewed deploy step, the fact that an AI helped &lt;em&gt;write&lt;/em&gt; it is no longer scary — because the AI never &lt;em&gt;executed&lt;/em&gt; it against prod. A human did, through a system built to make that action safe, observable, and reversible. Rolling migrations into a broader &lt;a href="https://dev.to/deployhq/zero-downtime-deployments-keeping-your-application-running-smoothly-hia"&gt;zero-downtime deployment strategy&lt;/a&gt; means even the intended changes never take the site down.&lt;/p&gt;

&lt;p&gt;This is the whole philosophy in one line: &lt;strong&gt;let AI draft, let a pipeline deploy.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The copy-paste safety checklist
&lt;/h2&gt;

&lt;p&gt;Print this, pin it, run through it before you connect any AI tool to any database that has data you'd miss.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI + DATABASE ACCESS — SAFETY CHECKLIST

ROLE
[] AI connects via a DEDICATED database role, not personal or app credentials
[] That role has SELECT only — no INSERT/UPDATE/DELETE/DROP/DDL
[] Postgres: ALTER DEFAULT PRIVILEGES set so future tables stay read-only
[] SQL Server: role added to db_datareader (nothing broader)
[] Verified by connecting as the role and confirming DELETE/DROP is denied

MCP LAYER
[] MCP server (e.g. DBHub) launched with --readonly
[] --max-rows set to a sane ceiling to prevent runaway result sets
[] DSN uses the read-only role, never admin credentials

DESTRUCTIVE WORK
[] Any write/migration testing happens on a DISPOSABLE copy, never prod
[] Copies are PII-sanitized before the AI can read them

CONNECTION
[] Database is NOT exposed on a public port (SSH tunnel or private network)
[] TLS/SSL required on the connection (sslmode=require or stricter)
[] Credentials live in env vars / secrets manager, NOT in committed config

DEPLOYS
[] Schema changes go through a reviewed, version-controlled pipeline
[] No temporary write grants for "just this one migration"
[] Every change is logged and has a rollback path

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

&lt;/div&gt;



&lt;p&gt;Work down that list and the viral horror story simply can't happen to you. Not because your AI is smarter or better-behaved than the one in the story — but because you removed its ability to do harm in the first place. That's what security by design looks like: the safe path is the only path available.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the first article in our series on working safely with AI and databases, with companion deep-dives on MySQL-specific connection gotchas and a disposable-file SQLite workflow to follow.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Ready to move database changes out of ad-hoc terminal sessions and into a reviewed, reversible pipeline? &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Start deploying with&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; for free and give your schema changes the same guardrails you just gave your AI.&lt;/p&gt;




&lt;p&gt;Questions or want to share how you've locked down AI database access? Email us 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 at @deployhq&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>tipstricks</category>
    </item>
    <item>
      <title>DeployHQ Static Hosting vs Cloudflare Pages: Same Edge, Different Workflows</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Fri, 03 Jul 2026 05:38:32 +0000</pubDate>
      <link>https://dev.to/deployhq/deployhq-static-hosting-vs-cloudflare-pages-same-edge-different-workflows-1f6m</link>
      <guid>https://dev.to/deployhq/deployhq-static-hosting-vs-cloudflare-pages-same-edge-different-workflows-1f6m</guid>
      <description>&lt;p&gt;Choosing between &lt;a href="https://www.deployhq.com/hosting/static" rel="noopener noreferrer"&gt;DeployHQ Static Hosting&lt;/a&gt; and Cloudflare Pages is unusual in this category — both products serve your static site from the &lt;em&gt;same&lt;/em&gt; Cloudflare edge network. The HTML, CSS, JavaScript, and images travel through the same 305+ POPs, get cached the same way, and exit at the same latencies. So the comparison isn't about CDN performance. It's about everything that happens &lt;em&gt;before&lt;/em&gt; the file lands on the edge — the build pipeline, what else you can deploy from the same project, and how tightly you want to be coupled to the Cloudflare ecosystem.&lt;/p&gt;

&lt;p&gt;This guide compares the two head to head: where the products diverge despite the shared edge, current pricing, and when to pick which.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;If you're already deep in the Cloudflare stack — Workers for compute, R2 for object storage, KV for caching, D1 for SQL, Cloudflare DNS — Cloudflare Pages is the obvious answer. The native integration is excellent and the free tier is unusually generous (unlimited bandwidth, unlimited static requests).&lt;/p&gt;

&lt;p&gt;If you also deploy backend code somewhere other than Cloudflare (a Laravel API on a VPS, a Rails app on a managed server, a WordPress install on shared hosting), &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting puts the static site on the same Cloudflare edge that Pages uses while the same &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project ships your backend code to wherever it actually runs. One pipeline, one billing relationship, no Cloudflare lock-in for the parts of your stack that don't belong there.&lt;/p&gt;

&lt;p&gt;For broader context across the category, our &lt;a href="https://dev.to/deployhq/best-software-deployment-tools-in-2026-3g9o"&gt;roundup of the best software deployment tools in 2026&lt;/a&gt; covers Cloudflare Pages, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, Vercel, and Netlify alongside the wider toolset. For the direct head-to-heads against the other edge hosts, see &lt;a href="https://dev.to/deployhq/deployhq-static-hosting-vs-vercel-features-pricing-and-which-to-choose-2li3-temp-slug-7537336"&gt;DeployHQ Static Hosting vs Vercel&lt;/a&gt; and &lt;a href="https://dev.to/deployhq/deployhq-static-hosting-vs-netlify-features-pricing-and-which-to-choose-4obn-temp-slug-1028277"&gt;DeployHQ Static Hosting vs Netlify&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  At a glance: feature comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;DeployHQ Static Hosting&lt;/th&gt;
&lt;th&gt;Cloudflare Pages&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Edge network&lt;/td&gt;
&lt;td&gt;Cloudflare's global edge&lt;/td&gt;
&lt;td&gt;Cloudflare's global edge (same network)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Framework auto-detection&lt;/td&gt;
&lt;td&gt;Yes (rule + AI fallback)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Atomic deploys&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom domains + SSL&lt;/td&gt;
&lt;td&gt;Yes (automatic)&lt;/td&gt;
&lt;td&gt;Yes (automatic)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free tier bandwidth&lt;/td&gt;
&lt;td&gt;Trial site included&lt;/td&gt;
&lt;td&gt;Unlimited bandwidth (all tiers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build minutes&lt;/td&gt;
&lt;td&gt;Bundled with DeployHQ plan&lt;/td&gt;
&lt;td&gt;500/month Free, 5,000/month Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrent builds&lt;/td&gt;
&lt;td&gt;1 (parallelism via separate projects)&lt;/td&gt;
&lt;td&gt;1 Free, 5 Pro, 20 Business&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPA mode (client-side routing)&lt;/td&gt;
&lt;td&gt;Yes (toggle)&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;_redirects&lt;/code&gt; or &lt;code&gt;_routes.json&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server-side rendering&lt;/td&gt;
&lt;td&gt;No (static only)&lt;/td&gt;
&lt;td&gt;Limited via Workers (Pages Functions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge functions&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (Pages Functions, Workers-based)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native integrations&lt;/td&gt;
&lt;td&gt;DeployHQ deployment pipeline&lt;/td&gt;
&lt;td&gt;Workers, R2, KV, D1, Queues, AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend deploys in same pipeline&lt;/td&gt;
&lt;td&gt;Yes (VPS, shared host, cloud, S3)&lt;/td&gt;
&lt;td&gt;Workers / Pages-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing model&lt;/td&gt;
&lt;td&gt;Fixed monthly per site&lt;/td&gt;
&lt;td&gt;Free / $20/mo Pro / $200/mo Business&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lock-in level&lt;/td&gt;
&lt;td&gt;Low (artifact moves anywhere)&lt;/td&gt;
&lt;td&gt;Medium (Pages Functions ↔ Workers tied)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same edge. Different everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  When DeployHQ Static Hosting is the right choice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You already deploy a backend with &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; — or are planning to.&lt;/strong&gt; This is the strongest case. If you're shipping a static site plus a Laravel/Rails/Node backend, having both in one &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project means one set of deploy keys, one rollback surface, one billing relationship. The same workflow that ships your Hugo marketing site can ship your Express API to a &lt;a href="https://www.deployhq.com/hosting/managed-vps" rel="noopener noreferrer"&gt;DeployHQ Managed VPS&lt;/a&gt; — or to your own Hetzner box, or to shared hosting, depending on what fits. Cloudflare Pages can only deploy to Cloudflare; everything outside the Cloudflare ecosystem still needs a separate deployment workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You want the same &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; pipeline running your build.&lt;/strong&gt; DeployHQ's build pipeline is intentionally generic — install dependencies, run tests, compile assets, set environment variables, run pre-deploy hooks. The build step doesn't care whether the destination is Static Hosting, a VPS, or an S3 bucket. If you've already tuned that pipeline for your project, deploying to Static Hosting requires no changes to it. See &lt;a href="https://www.deployhq.com/guides/sveltekit" rel="noopener noreferrer"&gt;the SvelteKit deployment guide&lt;/a&gt; for a representative pipeline example.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You want predictable per-site pricing.&lt;/strong&gt; Cloudflare Pages' free tier is the most generous in the category — unlimited bandwidth, unlimited static requests. That's hard to beat for a hobby project. But the Pro tier ($20/mo annual, $25/mo monthly) caps build minutes at 5,000/mo and concurrent builds at 5; Business jumps to $200/mo. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting charges a fixed monthly rate per site on top of your &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; plan — useful when you're running multiple client sites and want a flat per-site cost regardless of build complexity. &lt;a href="https://www.deployhq.com/pricing" rel="noopener noreferrer"&gt;Compare current&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; plans for the full breakdown. For broader trade-offs across hosting models, our &lt;a href="https://dev.to/deployhq/shared-hosting-vs-vps-a-comprehensive-guide-for-junior-developers-g8c"&gt;shared hosting vs VPS guide for junior developers&lt;/a&gt; covers the wider category.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You want low lock-in.&lt;/strong&gt; Pages Functions are written in the Workers runtime — if you migrate away, you rewrite them. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting serves a generic build artifact; if you ever want to leave, your build still produces the same files and works anywhere else that serves static content.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Cloudflare Pages is the right choice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You're all-in on the Cloudflare stack.&lt;/strong&gt; Workers for compute, R2 for object storage, KV for caching, D1 for SQL, Queues for messaging — Cloudflare Pages is the natural front door to that ecosystem. Direct bindings to Workers, R2, and KV from Pages Functions remove a lot of glue code. If your architecture is &lt;q&gt;everything on Cloudflare,&lt;/q&gt; Pages is the lowest-friction static-hosting choice you can make.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need the free tier specifically.&lt;/strong&gt; Pages' free tier with unlimited bandwidth and unlimited static requests is unique in this category. For high-traffic hobby projects, marketing pages with viral spikes, or open-source documentation sites, that pricing structure can't be matched. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting doesn't have a directly comparable &lt;q&gt;free forever&lt;/q&gt; tier — the trial includes a site and paid plans start from there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You want Pages Functions for per-request logic.&lt;/strong&gt; Geo-routing, A/B testing at the edge, authentication, header manipulation, simple APIs — Pages Functions running on Workers handles those cleanly. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting doesn't run code at request time; you'd put Cloudflare Workers in front of it to get the same behavior (which works, but isn't bundled).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You're not deploying anything outside Cloudflare.&lt;/strong&gt; If your entire architecture lives in Cloudflare's ecosystem and you don't have backend services on VPS, shared hosting, or cloud platforms that &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; would otherwise handle, the workflow-consolidation argument for &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; doesn't apply. Stay on Pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing: side by side
&lt;/h2&gt;

&lt;p&gt;As of June 2026 — check vendor pages for current numbers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare Pages Free&lt;/strong&gt; is $0 with 1 concurrent build, 500 builds/month, 100 custom domains per project, unlimited bandwidth, unlimited static requests. Genuinely the most generous free tier in the category.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare Pages Pro&lt;/strong&gt; is $20/month annual or $25/month monthly with 5 concurrent builds, 5,000 builds/month, 250 custom domains per project, unlimited bandwidth, unlimited static requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare Pages Business&lt;/strong&gt; is $200/month annual or $250/month monthly with 20 concurrent builds, 20,000 builds/month, 500 custom domains per project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DeployHQ Static Hosting&lt;/strong&gt; charges a fixed monthly rate per site, shown on the Hosted Resources page in your account's currency. Pricing rolls into your &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; plan on a single monthly invoice. Trial accounts can provision one Static Hosting site at no charge.&lt;/p&gt;

&lt;p&gt;The honest framing: for a single static site with no backend, Cloudflare Pages Free is structurally cheaper than any &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; tier because Free is $0. The &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; math gets favorable when you're already paying for &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; to deploy backend code somewhere — at that point the static site is bundled into a plan you're already running, and the marginal cost of adding Static Hosting is the per-site rate, not a new vendor relationship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving between them (it's not all-or-nothing)
&lt;/h2&gt;

&lt;p&gt;Both platforms serve the same static artifact, so migration in either direction is mostly DNS plus repo reconfiguration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From Cloudflare Pages to &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting&lt;/strong&gt;: connect the repository in &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, point Static Hosting at the same build output directory Pages used, keep the build command identical, switch DNS once verified. If you depend on Pages Functions, plan replacements first — Cloudflare Workers in front of Static Hosting can serve the same role, but you maintain them separately rather than co-located with the static project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting to Cloudflare Pages&lt;/strong&gt;: even simpler — connect the repo to Pages, set the same build command and output directory, switch DNS. Nothing DeployHQ-specific in your code to unwind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Or: use both.&lt;/strong&gt; Run your static frontend on Cloudflare Pages, keep &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; as your deployment pipeline for the backend you ship to a VPS or shared host. This combination is common — Pages handles the frontend exceptionally well, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; handles everything else. The trade-off is two deployment surfaces instead of one; whether that's worth it depends on how much your team values consolidation.&lt;/p&gt;

&lt;p&gt;For broader background on legacy hosting and modern alternatives, &lt;a href="https://www.deployhq.com/blog/is-ftp-dead-a-look-at-its-continued-use-in-deployment" rel="noopener noreferrer"&gt;is FTP dead?&lt;/a&gt; covers the wider shift in deployment patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shared-edge dimension
&lt;/h2&gt;

&lt;p&gt;Both products use Cloudflare's edge. In practice this means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt; : identical from a given user's location. There's no advantage to either product on raw edge performance for static content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DDoS protection&lt;/strong&gt; : included on both via Cloudflare's standard protection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTPS certificates&lt;/strong&gt; : provisioned automatically by Cloudflare in both cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache invalidation&lt;/strong&gt; : works at deploy time (atomic deploys flip the served version) in both products.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's &lt;em&gt;different&lt;/em&gt; despite the shared edge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare account requirement&lt;/strong&gt; : Pages is part of your Cloudflare account; you log in there, your billing lives there, your custom domains route through Cloudflare DNS. Static Hosting doesn't require a Cloudflare account — it manages the Cloudflare infrastructure on your behalf.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workers bindings&lt;/strong&gt; : Pages Functions can bind directly to Workers, R2, KV, D1, Queues. Static Hosting doesn't expose these bindings — if you need them, you front the Static Hosting site with your own Worker that has the bindings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-edge build pipeline&lt;/strong&gt; : DeployHQ's build runs on DeployHQ's servers with its own pipeline UI, env-var management, and rollback history. Cloudflare Pages runs builds on Cloudflare's infrastructure with its own UI. They produce the same artifact but the pre-edge experience differs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; also exposes &lt;a href="https://www.deployhq.com/features/one-click-rollback" rel="noopener noreferrer"&gt;one-click rollback&lt;/a&gt; — useful when you ship something broken and need to revert without rebuilding. Pages supports a similar flow through its deployment history UI, but the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; rollback is the same one-click motion you'd use for a Managed VPS deploy or a BYO-server deploy. One mental model across all targets.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Are there any latency differences between &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting and Cloudflare Pages?&lt;/strong&gt;No. Both serve from Cloudflare's global edge network. Latency to a given user depends on their geography and the nearest Cloudflare POP — which is identical for both products.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting use my own Cloudflare account?&lt;/strong&gt;No. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; manages the Cloudflare infrastructure on your behalf. You don't need a Cloudflare account to use Static Hosting; subdomains under &lt;code&gt;deployhq-sites.com&lt;/code&gt; work out of the box, and custom domains use a CNAME from your DNS provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use Cloudflare Workers in front of &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting?&lt;/strong&gt;Yes. If you need edge-function behavior (geo-routing, edge auth, request rewriting), set up a Cloudflare Worker on your custom domain that proxies to your Static Hosting subdomain. The Worker runs at the edge with full Worker capabilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about Hugo, Astro, Eleventy, and other static-site generators?&lt;/strong&gt; Both products support all major static-site generators since they serve the build artifact, not the source. See &lt;a href="https://www.deployhq.com/guides/hugo" rel="noopener noreferrer"&gt;the Hugo deployment guide&lt;/a&gt; for a representative &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; pipeline example.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if I outgrow Static Hosting and need a runtime?&lt;/strong&gt; Move the project to &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS Hosting (for full Linux + Node/Python/Ruby), or front Static Hosting with Cloudflare Workers for lightweight edge logic. Same &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; pipeline ships to either target.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;

&lt;p&gt;If you're already on &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, enable beta features under &lt;strong&gt;Settings &amp;gt; Beta Features&lt;/strong&gt; and add a new Static Hosting server to any project. The &lt;a href="https://dev.to/deployhq/static-hosting-on-deployhq-deploy-and-host-jamstack-sites-from-one-dashboard-40hi-temp-slug-2214878"&gt;Static Hosting on&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; pillar guide walks through the end-to-end provisioning flow.&lt;/p&gt;

&lt;p&gt;If you're new, &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;start a free trial&lt;/a&gt; and the included Static Hosting site lets you compare it head-to-head against Cloudflare Pages for the same project. For the wider picture of where Static Hosting fits among the five hosting types &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; supports, see &lt;a href="https://www.deployhq.com/blog/deployhq-your-universal-deployment-platform-for-all-hosting-types" rel="noopener noreferrer"&gt;your universal deployment and hosting platform&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Both &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting and Cloudflare Pages serve from the same edge, and both do the static-hosting job well. The right answer depends almost entirely on what's around your static site. If your stack is all-Cloudflare, Pages wins on integration and free-tier economics. If you also ship backend code outside Cloudflare — and especially if you want one deployment pipeline for the whole stack — &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting wins on workflow consolidation.&lt;/p&gt;

&lt;p&gt;For more detail on the product, the &lt;a href="https://www.deployhq.com/support/servers/static-hosting" rel="noopener noreferrer"&gt;Static Hosting support library&lt;/a&gt; has the full provisioning, framework-detection, and lifecycle documentation.&lt;/p&gt;




&lt;p&gt;Questions or feedback on Static Hosting vs Cloudflare Pages? Email &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or follow &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;@deployhq&lt;/a&gt; on X for product updates.&lt;/p&gt;

</description>
      <category>devopsinfrastructure</category>
      <category>frontend</category>
      <category>tutorials</category>
    </item>
    <item>
      <title>Free Static Hosting for v0, Bolt, Lovable, and Firebase Studio Apps</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Tue, 30 Jun 2026 07:39:28 +0000</pubDate>
      <link>https://dev.to/deployhq/free-static-hosting-for-v0-bolt-lovable-and-firebase-studio-apps-379h</link>
      <guid>https://dev.to/deployhq/free-static-hosting-for-v0-bolt-lovable-and-firebase-studio-apps-379h</guid>
      <description>&lt;p&gt;You describe an app in a chat window. Twenty seconds later you're staring at a working React UI that does roughly what you asked for. v0, Bolt, Lovable, and Firebase Studio have made the first hour of building a web app look almost magical — and the next hour is when reality lands. Where does this thing actually live? Whose name is on the URL? Who pays the bill?&lt;/p&gt;

&lt;p&gt;The defaults each of these tools nudge you toward all share one trait: lock-in. v0 ships you to Vercel. Bolt has a one-click button to Netlify. Lovable hosts at &lt;code&gt;&amp;lt;project&amp;gt;.lovable.app&lt;/code&gt; until you upgrade. Firebase Studio assumes Firebase Hosting. Each of those is a fine first step — they're free, they work in two clicks, they give you a URL to share. They also bind you to one ecosystem's pricing, runtime, and account terms the moment you want to move past the demo.&lt;/p&gt;

&lt;p&gt;This isn't a hypothetical migration — it's a pattern users are already documenting. The &lt;a href="https://www.reddit.com/r/lovable/comments/1qupsrk/sorry_lovable_but_i_moved_on/" rel="noopener noreferrer"&gt;r/lovable thread &lt;q&gt;sorry Lovable, but I moved on&lt;/q&gt;&lt;/a&gt; is one recent example: OP and the top commenters describe the same workflow shift — keep the AI builder for the initial scaffold, then continue editing in your own editor with &lt;a href="https://www.deployhq.com/guides/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; or &lt;a href="https://www.deployhq.com/guides/cursor" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt;, with &lt;a href="https://www.deployhq.com/guides/supabase" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt; still doing the backend, and &lt;q&gt;free hosting on top.&lt;/q&gt; That last bullet is the one this post addresses. Most migrants land at Vercel because it's the most visible free option; this is the alternative.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com/hosting/static" rel="noopener noreferrer"&gt;DeployHQ Static Hosting&lt;/a&gt; is the lock-in-free alternative for the static frontend these tools produce. It runs on Cloudflare's global edge, framework-detects your build (Next.js, Vite, Astro, Angular, SvelteKit, Flutter web), and atomically deploys every Git push. It's free during beta. And it doesn't touch the backend you've already wired up — Supabase, Firebase, your own API, whichever the tool of your choice put behind your UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: where each AI builder leaves you
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Builder&lt;/th&gt;
&lt;th&gt;Default deploy target&lt;/th&gt;
&lt;th&gt;What you can move&lt;/th&gt;
&lt;th&gt;What you can't&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v0&lt;/td&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;The Next.js project (with static export configured)&lt;/td&gt;
&lt;td&gt;Server actions, App Router server components, route handlers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bolt&lt;/td&gt;
&lt;td&gt;Netlify&lt;/td&gt;
&lt;td&gt;Vite + React/Vue/Svelte, Astro, SvelteKit static, Next.js export&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;server/&lt;/code&gt; directory in full-stack starters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lovable&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;*.lovable.app&lt;/code&gt; (custom domains paid)&lt;/td&gt;
&lt;td&gt;The Vite + React frontend&lt;/td&gt;
&lt;td&gt;Custom Node servers you've added beyond Supabase Edge Functions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firebase Studio&lt;/td&gt;
&lt;td&gt;Firebase Hosting&lt;/td&gt;
&lt;td&gt;Next.js export, Angular, Vite, Astro, Flutter web&lt;/td&gt;
&lt;td&gt;Firebase App Hosting (Cloud Run SSR), &lt;code&gt;firebase.json&lt;/code&gt; rewrites&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern: anything that compiles to a flat output directory — &lt;code&gt;dist/&lt;/code&gt;, &lt;code&gt;out/&lt;/code&gt;, &lt;code&gt;build/&lt;/code&gt; — moves cleanly to Static Hosting. Anything that needs a Node runtime at request time — server actions, SSR, route handlers, your own Express/Hono backend — doesn't, and shouldn't try to.&lt;/p&gt;

&lt;p&gt;For the parts that don't fit static, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS is the upgrade path (covered in detail below). Same &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project, same build pipeline, different deploy target. You don't have to choose one or the other at the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four how-to guides
&lt;/h2&gt;

&lt;p&gt;Each of the tools has its own export flow, framework defaults, and a handful of gotchas worth knowing before the first push. Walk through whichever guide matches your starting point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;v0&lt;/strong&gt; : &lt;a href="https://dev.to/deployhq/how-to-deploy-a-v0-app-to-deployhq-static-hosting-1ehf-temp-slug-6272978"&gt;How to&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;Deploy&lt;/a&gt; a v0 App to &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting — covers exporting from v0 to GitHub, switching Next.js into static-export mode, and the server-action gotchas v0 sometimes generates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bolt&lt;/strong&gt; : &lt;a href="https://dev.to/deployhq/how-to-deploy-a-bolt-app-to-deployhq-static-hosting-3gaj-temp-slug-5825656"&gt;How to&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;Deploy&lt;/a&gt; a Bolt App to &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting — works across every framework Bolt scaffolds. Includes the WebContainer path-cleanup gotcha and the framework→output-directory mapping for Vite, Astro, SvelteKit, Next, and Remix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lovable&lt;/strong&gt; : &lt;a href="https://dev.to/deployhq/how-to-deploy-a-lovable-frontend-to-deployhq-static-hosting-43m-temp-slug-4650058"&gt;How to&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;Deploy&lt;/a&gt; a Lovable Frontend to &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting — keeps the Supabase backend intact and walks through updating Supabase Auth redirect URLs for the new domain. The &lt;q&gt;static frontend, your Supabase backend keeps doing the work&lt;/q&gt; path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase Studio&lt;/strong&gt; : &lt;a href="https://dev.to/deployhq/how-to-deploy-a-firebase-studio-app-to-deployhq-static-hosting-2bh6-temp-slug-970294"&gt;How to&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;Deploy&lt;/a&gt; a Firebase Studio App to &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting — covers the framework-agnostic build configuration, the Firebase Auth authorized-domains update, and the carve-out around Firebase App Hosting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four follow the same shape: export to GitHub, point &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; at the repo, configure the build pipeline with the right framework env-var prefixes, ship the first deploy, then update auth allowlists if the backend you're calling has one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What stays the same when you move
&lt;/h2&gt;

&lt;p&gt;The biggest reason people stall on moving an AI-built app off its default host is the worry that auth and data will break. They almost never do.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Supabase-backed projects&lt;/strong&gt; (Lovable's default and a common pattern in v0/Bolt) keep working because Supabase's API and Auth flows are hosted on Supabase. You set &lt;code&gt;VITE_SUPABASE_URL&lt;/code&gt; and &lt;code&gt;VITE_SUPABASE_ANON_KEY&lt;/code&gt; in DeployHQ's environment-variable UI, add the new domain to Supabase's redirect-URL allowlist, and the rest is unchanged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase-backed projects&lt;/strong&gt; (Firebase Studio's default, sometimes appears in other builders) keep working because Firebase Auth, Firestore, Storage, and Cloud Functions all run on Google's infrastructure. You add the new domain to Firebase Auth's authorized-domains list and the SDK keeps talking to the same backend it always did.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apps calling third-party APIs directly&lt;/strong&gt; (Stripe checkout, public REST APIs, scraped data sources) keep working because those calls leave the browser regardless of where the bundle is served from.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only things that genuinely move are the build pipeline (now run by &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; instead of Vercel/Netlify/Firebase) and the public URL (now Cloudflare's edge instead of Vercel/Netlify/Firebase Hosting). Everything between user and database stays put.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is worth the 15 minutes
&lt;/h2&gt;

&lt;p&gt;The first deploy from an AI builder's default host is essentially free. The second one — when the demo becomes a real project — is where the trade-offs start mattering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lock-in.&lt;/strong&gt; Each builder's default host is owned by a different company. Moving later is harder than picking your own host up front, because URLs accumulate users, integrations, and SEO equity that don't migrate cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing trajectory.&lt;/strong&gt; Vercel's bandwidth and serverless-execution pricing scales with traffic. Netlify's build-minute caps bite earlier than you'd expect on a chatty Vite project — the &lt;a href="https://dev.to/deployhq/deployhq-static-hosting-vs-netlify-features-pricing-and-which-to-choose-4obn-temp-slug-1028277"&gt;DeployHQ Static Hosting vs Netlify comparison&lt;/a&gt; covers where those thresholds land. &lt;code&gt;*.lovable.app&lt;/code&gt; is fine for demos but custom domains are a paid Lovable plan. Firebase Hosting traffic is metered against the same Firebase Spark/Blaze quotas as your database calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime constraints you didn't agree to.&lt;/strong&gt; Vercel's static export still goes through Vercel's edge-function billing model. Netlify's bandwidth tier resets monthly. Firebase Hosting can serve static files but pulls you into the broader Firebase plan structure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting is free during the beta, and the post-beta plan structure is straightforward enough to put on a single &lt;a href="https://www.deployhq.com/pricing" rel="noopener noreferrer"&gt;DeployHQ pricing page&lt;/a&gt; — covered in the same way the rest of the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; catalog is priced, with no per-invocation runtime metering on a static bundle.&lt;/p&gt;

&lt;p&gt;For the head-to-head against the most common alternatives, read the &lt;a href="https://dev.to/deployhq/deployhq-static-hosting-vs-vercel-features-pricing-and-which-to-choose-2li3-temp-slug-7537336"&gt;DeployHQ Static Hosting vs Vercel&lt;/a&gt; comparison — it covers framework support, pricing, edge network, and the trade-offs that decide which one fits your stack. The &lt;a href="https://dev.to/deployhq/static-hosting-on-deployhq-deploy-and-host-jamstack-sites-from-one-dashboard-40hi-temp-slug-2214878"&gt;Static Hosting pillar guide&lt;/a&gt; is the longer reference on framework auto-detection, SPA mode, atomic deploys, and the rest of the feature surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Static Hosting &lt;em&gt;isn't&lt;/em&gt; for
&lt;/h2&gt;

&lt;p&gt;Three categories of project don't belong on Static Hosting, and it's worth saying so up front:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Anything with required SSR at request time.&lt;/strong&gt; Next.js with server actions, Astro with server-side render, SvelteKit with the Node adapter, full Next.js App Router server components — these need a Node runtime Static Hosting doesn't provide. Move them to Managed VPS or refactor to static export.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full-stack starter templates with bundled servers.&lt;/strong&gt; Bolt's full-stack starters include an Express or Hono server. Lovable projects that have grown a custom WebSocket layer beyond Supabase Edge Functions. v0 projects you've turned into half-static, half-SSR hybrids. The static frontend half deploys fine; the server half needs to live somewhere with a runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase App Hosting projects.&lt;/strong&gt; Firebase Studio's newer App Hosting target provisions a Cloud Run instance for SSR — that runtime is fundamentally Google-bound and can't move to Static Hosting.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In all three cases, &lt;a href="https://www.deployhq.com/hosting/managed-vps" rel="noopener noreferrer"&gt;DeployHQ Managed VPS&lt;/a&gt; is the natural home — same &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project, same Git workflow, different deploy target. You can also split the difference: keep the static UI on the edge and run only the dynamic parts on Managed VPS. The build pipeline you set up adapts to either target.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to start
&lt;/h2&gt;

&lt;p&gt;If you're already an AI-app-builder user with a project ready to move:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick the guide that matches your tool (v0, Bolt, Lovable, Firebase Studio — links above)&lt;/li&gt;
&lt;li&gt;Export to GitHub if you haven't already&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Sign up free for DeployHQ&lt;/a&gt; — the beta-tier Static Hosting site is included&lt;/li&gt;
&lt;li&gt;Walk through the guide; the whole loop is under 20 minutes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're earlier in the journey and still deciding which builder to use, the framework support is the right way to choose: Static Hosting framework-detects every common output, so the question is really which builder's UX you prefer and which backend (Supabase, Firebase, your own) you want behind it. The hosting decision compounds: pick a lock-in-free host now and you keep the option to swap the builder later.&lt;/p&gt;

&lt;p&gt;For the broader &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; catalog beyond static, the &lt;a href="https://www.deployhq.com/hosting" rel="noopener noreferrer"&gt;hosting hub&lt;/a&gt; covers the four other deploy targets &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; supports — Managed VPS, SFTP, SSH/Rsync, S3 — all driven from the same project and build pipeline. And for context on the proposition across the full hosting catalog, &lt;a href="https://www.deployhq.com/blog/deployhq-your-universal-deployment-platform-for-all-hosting-types" rel="noopener noreferrer"&gt;your universal deployment and hosting platform&lt;/a&gt; is the longer read.&lt;/p&gt;

&lt;p&gt;The full product reference lives in the &lt;a href="https://www.deployhq.com/support/servers/static-hosting" rel="noopener noreferrer"&gt;Static Hosting support library&lt;/a&gt;. And for a roundup of how &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; fits alongside the rest of the deployment-tool category, &lt;a href="https://dev.to/deployhq/best-software-deployment-tools-in-2026-3g9o"&gt;best software deployment tools in 2026&lt;/a&gt; is a useful next reference.&lt;/p&gt;




&lt;p&gt;Questions, edge cases, or a builder you'd like covered that isn't in the list above? Email &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or follow &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;@deployhq&lt;/a&gt; on X — we're adding to this list as more AI builders ship credible static-export paths.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devopsinfrastructure</category>
      <category>frontend</category>
      <category>newfeatures</category>
    </item>
    <item>
      <title>How to Deploy a Firebase Studio App to DeployHQ Static Hosting</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Fri, 26 Jun 2026 14:11:37 +0000</pubDate>
      <link>https://dev.to/deployhq/how-to-deploy-a-firebase-studio-app-to-deployhq-static-hosting-3k10</link>
      <guid>https://dev.to/deployhq/how-to-deploy-a-firebase-studio-app-to-deployhq-static-hosting-3k10</guid>
      <description>&lt;p&gt;&lt;a href="https://www.deployhq.com/hosting/static" rel="noopener noreferrer"&gt;DeployHQ Static Hosting&lt;/a&gt; serves pre-built static assets from Cloudflare's global edge — a clean fit for a Firebase Studio project, where Gemini Code Assist scaffolds a Next.js, Angular, or Vite-based frontend that builds to a flat output directory. This guide walks through deploying a Firebase Studio app end to end: sync the workspace to GitHub, configure the framework for static export, provision Static Hosting, set up the build pipeline, update Firebase Auth's authorized domains, and ship the first deploy.&lt;/p&gt;

&lt;p&gt;The framing matters: Static Hosting &lt;em&gt;replaces&lt;/em&gt; the Firebase Hosting URL Firebase Studio provisions by default, not the Firebase backend behind it. Firebase Auth still issues tokens. Firestore still stores data. Cloud Functions still run on Google's infrastructure. Static Hosting is the public face — a faster, lock-in-free home for the frontend bundle that calls all of it. The one exception: if you've enabled &lt;strong&gt;Firebase App Hosting&lt;/strong&gt; (which provisions a Cloud Run instance for SSR), that runtime is bound to Google Cloud and can't move to Static Hosting. For static-export builds, you're good — keep reading.&lt;/p&gt;

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

&lt;p&gt;By the end of this guide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Firebase Studio workspace synced to your own GitHub repository&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project with Static Hosting connected to that repo&lt;/li&gt;
&lt;li&gt;A framework configured for static output (Next.js export, Vite build, Angular &lt;code&gt;--configuration production&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;A working build pipeline that runs your framework's build and uploads the output to Cloudflare's edge&lt;/li&gt;
&lt;li&gt;Firebase Auth authorized domains updated to include the new URL&lt;/li&gt;
&lt;li&gt;The site serving over HTTPS at &lt;code&gt;&amp;lt;subdomain&amp;gt;.deployhq-sites.com&lt;/code&gt; (and optionally a custom domain)&lt;/li&gt;
&lt;li&gt;Atomic deploys on every push to &lt;code&gt;main&lt;/code&gt;, with one-click rollback to any previous version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Expected time: under 20 minutes from a Firebase Studio workspace you already have set up.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A Firebase Studio workspace — created at &lt;a href="https://firebase.studio" rel="noopener noreferrer"&gt;firebase.studio&lt;/a&gt; — with a working frontend and a Firebase project providing whatever services it uses (Auth, Firestore, Storage, Functions)&lt;/li&gt;
&lt;li&gt;A GitHub account you can sync the workspace to&lt;/li&gt;
&lt;li&gt;Access to the Firebase project's console (you'll update Auth authorized domains in Step 7)&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; account with beta features enabled (enable under &lt;strong&gt;Settings &amp;gt; Beta Features&lt;/strong&gt; )&lt;/li&gt;
&lt;li&gt;Node.js installed locally if you want to test the build before pushing (optional but recommended)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don't have a &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; account yet, you can start a free trial in the next step — the trial includes one Static Hosting site at no charge while the feature is in beta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Sync your Firebase Studio workspace to GitHub
&lt;/h2&gt;

&lt;p&gt;Firebase Studio runs your workspace in a Nix-based environment in the browser, with the source code stored in a Google Cloud workspace. To deploy on &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, the code needs to live in a normal Git repository.&lt;/p&gt;

&lt;p&gt;In Firebase Studio:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the workspace you want to deploy&lt;/li&gt;
&lt;li&gt;Open the &lt;strong&gt;Source Control&lt;/strong&gt; panel from the left sidebar&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Publish to GitHub&lt;/strong&gt; (or &lt;strong&gt;Sync&lt;/strong&gt; if you've already connected a repo)&lt;/li&gt;
&lt;li&gt;Authorize Firebase Studio's GitHub integration if you haven't already&lt;/li&gt;
&lt;li&gt;Choose a repository name and visibility, then publish&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Firebase Studio commits the workspace contents — &lt;code&gt;package.json&lt;/code&gt;, framework config files, &lt;code&gt;src/&lt;/code&gt;, the &lt;code&gt;dev.nix&lt;/code&gt; environment file under &lt;code&gt;.idx/&lt;/code&gt;, and any Firebase configuration files (&lt;code&gt;firebase.json&lt;/code&gt;, &lt;code&gt;.firebaserc&lt;/code&gt;) — to the &lt;code&gt;main&lt;/code&gt; branch of the new repository.&lt;/p&gt;

&lt;p&gt;Open the repo in GitHub and confirm &lt;code&gt;package.json&lt;/code&gt; is at the root. You'll identify the build command and output directory in the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Configure the framework for static output
&lt;/h2&gt;

&lt;p&gt;Firebase Studio scaffolds projects across several frameworks. The right Static Hosting configuration depends on which template the workspace started from. Two things to verify: the build command and the output directory.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Default build command&lt;/th&gt;
&lt;th&gt;Output directory&lt;/th&gt;
&lt;th&gt;Static-ready out of the box?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Next.js (App Router)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;out/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No — set &lt;code&gt;output: 'export'&lt;/code&gt; in &lt;code&gt;next.config.js&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Angular&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dist/&amp;lt;project-name&amp;gt;/browser/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vite + React&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dist/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vite + Vue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dist/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Astro&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dist/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flutter web&lt;/td&gt;
&lt;td&gt;&lt;code&gt;flutter build web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;build/web/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For the Next.js case specifically, open &lt;code&gt;next.config.js&lt;/code&gt; (or &lt;code&gt;next.config.mjs&lt;/code&gt;) and add:&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="cm"&gt;/** @type {import('next').NextConfig} */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&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;export&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;unoptimized&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="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&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="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This switches Next.js from SSR mode to a static export written to &lt;code&gt;out/&lt;/code&gt;. Any code path using &lt;code&gt;getServerSideProps&lt;/code&gt;, server actions, route handlers, or App Router server components will fail the build — those features need a runtime Static Hosting doesn't provide. The &lt;code&gt;images.unoptimized: true&lt;/code&gt; flag disables Next.js Image Optimization, which also needs a server.&lt;/p&gt;

&lt;p&gt;If your workspace uses &lt;code&gt;firebase.json&lt;/code&gt; to define Firebase Hosting rewrites — single-page-app fallbacks, URL redirects, custom headers — those won't apply on &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;. SPA rewrite is handled by Static Hosting's SPA mode (Step 4), and Cloudflare's edge handles HTTPS and caching headers automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Verify the Firebase configuration
&lt;/h2&gt;

&lt;p&gt;Firebase Studio apps initialize the Firebase JS SDK with a config object usually pulled from environment variables (&lt;code&gt;NEXT_PUBLIC_FIREBASE_*&lt;/code&gt; for Next.js, &lt;code&gt;VITE_FIREBASE_*&lt;/code&gt; for Vite, &lt;code&gt;process.env.NG_APP_FIREBASE_*&lt;/code&gt; for Angular).&lt;/p&gt;

&lt;p&gt;Open the Firebase initialization file (&lt;code&gt;src/lib/firebase.ts&lt;/code&gt;, &lt;code&gt;src/firebase.ts&lt;/code&gt;, or wherever your workspace puts it) and confirm it reads from environment variables rather than hardcoded values. The relevant variables are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;*_FIREBASE_API_KEY&lt;/code&gt; — public API key (safe in client bundles)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;*_FIREBASE_AUTH_DOMAIN&lt;/code&gt; — typically &lt;code&gt;&amp;lt;project-id&amp;gt;.firebaseapp.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*_FIREBASE_PROJECT_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*_FIREBASE_STORAGE_BUCKET&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*_FIREBASE_MESSAGING_SENDER_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*_FIREBASE_APP_ID&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are all designed to be public — Firebase enforces security at the service level via Firestore Security Rules, Storage Rules, and Firebase Auth, not by hiding the config.&lt;/p&gt;

&lt;p&gt;If your project calls Cloud Functions, the function URLs use the Firebase project's default domain (&lt;code&gt;https://&amp;lt;region&amp;gt;-&amp;lt;project-id&amp;gt;.cloudfunctions.net&lt;/code&gt; or &lt;code&gt;https://&amp;lt;custom-domain&amp;gt;.cloudfunctions.net&lt;/code&gt;). Those calls work from any frontend host as long as the function's CORS is configured to allow your origin (covered in Step 7 and the gotchas section).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Provision a Static Hosting site in DeployHQ
&lt;/h2&gt;

&lt;p&gt;In your &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project (or create one and connect it to the Firebase Studio repository), click &lt;strong&gt;New Server&lt;/strong&gt; :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enter a name for the server — internal reference only, doesn't affect the public URL&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Static Hosting&lt;/strong&gt; from the protocol picker under &lt;strong&gt;Hosting&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Choose a subdomain — your site serves at &lt;code&gt;&amp;lt;subdomain&amp;gt;.deployhq-sites.com&lt;/code&gt;. Subdomains are unique across all &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; accounts; lowercase letters, numbers, and hyphens only&lt;/li&gt;
&lt;li&gt;Set the &lt;strong&gt;subdirectory to deploy from&lt;/strong&gt; to match your framework's output directory from Step 2 (&lt;code&gt;out/&lt;/code&gt;, &lt;code&gt;dist/&lt;/code&gt;, &lt;code&gt;dist/&amp;lt;project-name&amp;gt;/browser/&lt;/code&gt;, or &lt;code&gt;build/web/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Toggle &lt;strong&gt;SPA mode&lt;/strong&gt; based on your framework:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On&lt;/strong&gt; for Angular, Vite + React/Vue SPAs, Flutter web — these use client-side routing and need unknown paths rewritten to &lt;code&gt;index.html&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Off&lt;/strong&gt; for Next.js static export and Astro — these generate one HTML file per route at build time&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create Server&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; runs framework detection against the connected repository. If a &lt;q&gt;[Framework] detected!&lt;/q&gt; callout appears with a suggested output directory and SPA mode, the rule-based detector is doing the work for you. Accept the suggested values unless your framework config diverges from the defaults.&lt;/p&gt;

&lt;p&gt;Provisioning takes under a minute. Once the status flips to active, the site exists on Cloudflare's edge — but no code has shipped yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Configure the build pipeline
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; runs your build before transferring artifacts to the edge. For a Firebase Studio-exported project, the &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;build pipeline&lt;/a&gt; is two stages: install, then build.&lt;/p&gt;

&lt;p&gt;In the project's build settings, add:&lt;br&gt;
&lt;/p&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;For Flutter, swap to the Flutter SDK install + &lt;code&gt;flutter build web&lt;/code&gt; instead. For Angular projects with environment-specific builds, you may need &lt;code&gt;npm run build -- --configuration production&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Environment variables to set in DeployHQ's environment-variable UI — the prefix depends on your framework:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js&lt;/strong&gt; : &lt;code&gt;NEXT_PUBLIC_FIREBASE_API_KEY&lt;/code&gt;, &lt;code&gt;NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vite + React/Vue&lt;/strong&gt; : &lt;code&gt;VITE_FIREBASE_API_KEY&lt;/code&gt;, &lt;code&gt;VITE_FIREBASE_AUTH_DOMAIN&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Angular&lt;/strong&gt; : &lt;code&gt;NG_APP_FIREBASE_API_KEY&lt;/code&gt; etc. (or environment.ts files if you're using the older pattern)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you should &lt;em&gt;not&lt;/em&gt; put here: Firebase Admin SDK service-account JSON, Firebase Functions deployment tokens, any credential that grants server-side access. Those belong in Cloud Functions runtime config, never in a static bundle.&lt;/p&gt;

&lt;p&gt;If your project also uses third-party services (Stripe publishable keys, Sentry DSN, public analytics IDs), they go here too — same prefix rules apply.&lt;/p&gt;

&lt;p&gt;Ready to take it live? &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Sign up free for DeployHQ&lt;/a&gt; if you don't have an account, and the beta-tier Static Hosting site is enough to ship this guide end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: First deploy
&lt;/h2&gt;

&lt;p&gt;Push any change to the configured branch (typically &lt;code&gt;main&lt;/code&gt;), or trigger a manual deployment from the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; dashboard. DeployHQ:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clones the repo at the head of the configured branch&lt;/li&gt;
&lt;li&gt;Runs the build pipeline (&lt;code&gt;npm ci &amp;amp;&amp;amp; npm run build&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Uploads the contents of your output directory to object storage atomically&lt;/li&gt;
&lt;li&gt;Flips Cloudflare's edge routing to serve the new version&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The deployment log streams each step in real time. When it completes, visit &lt;code&gt;https://&amp;lt;subdomain&amp;gt;.deployhq-sites.com&lt;/code&gt; — the site loads, but Firebase Auth won't yet allow sign-ins from the new domain. That's Step 7.&lt;/p&gt;

&lt;p&gt;If the build fails, the log shows the exact error. Most common failures for Firebase Studio projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Next.js page or route handler using a server-only feature without &lt;code&gt;output: 'export'&lt;/code&gt; configured&lt;/li&gt;
&lt;li&gt;A missing &lt;code&gt;*_FIREBASE_*&lt;/code&gt; env var the client bundle references at build time&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;firebase.json&lt;/code&gt; declaring a Cloud Functions rewrite the static export can't fulfill&lt;/li&gt;
&lt;li&gt;An Angular project building without the production configuration, leaving development-only checks in the bundle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix the issue in the repo, commit, push — &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; runs the next build automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Update Firebase Auth authorized domains
&lt;/h2&gt;

&lt;p&gt;Firebase Auth has an allowlist of authorized domains — sign-in flows only work from domains on that list. Until your new domain is added, OAuth providers (Google, Facebook, GitHub) and email-link sign-ins will reject the authentication request.&lt;/p&gt;

&lt;p&gt;In the Firebase Console:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the project that backs this app&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Authentication &amp;gt; Settings &amp;gt; Authorized domains&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add domain&lt;/strong&gt; and enter &lt;code&gt;&amp;lt;subdomain&amp;gt;.deployhq-sites.com&lt;/code&gt; (and any custom domain you'll add in Step 8)&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Existing entries like &lt;code&gt;localhost&lt;/code&gt;, your old Firebase Hosting domain, and any previously configured custom domains stay on the list — Firebase Auth allows multiple authorized domains, so the Firebase Studio preview environment continues to work.&lt;/p&gt;

&lt;p&gt;Sign out, sign back in on the new domain, and confirm the auth flow round-trips correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Add a custom domain
&lt;/h2&gt;

&lt;p&gt;To serve from your own domain (e.g., &lt;code&gt;app.example.com&lt;/code&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In your DNS provider, add a &lt;code&gt;CNAME&lt;/code&gt; record pointing your subdomain to &lt;code&gt;&amp;lt;subdomain&amp;gt;.deployhq-sites.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Wait for DNS propagation (usually minutes; depends on your TTL)&lt;/li&gt;
&lt;li&gt;Cloudflare provisions an HTTPS certificate automatically once the CNAME resolves&lt;/li&gt;
&lt;li&gt;Add the custom domain to Firebase Auth's authorized domains list (same path as Step 7) so OAuth flows from &lt;code&gt;app.example.com&lt;/code&gt; round-trip correctly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your site now serves from &lt;code&gt;app.example.com&lt;/code&gt; over HTTPS via Cloudflare's edge, talking to the same Firebase backend Firebase Studio scaffolded for you.&lt;/p&gt;

&lt;p&gt;For an apex domain (&lt;code&gt;example.com&lt;/code&gt; with no subdomain), use an ALIAS or ANAME record if your DNS provider supports them — Cloudflare DNS, DNSimple, and Route 53 all do. Otherwise, host the apex elsewhere and CNAME a &lt;code&gt;www&lt;/code&gt; subdomain to &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;q&gt;Firebase Studio App Hosting&lt;/q&gt; isn't supported.&lt;/strong&gt; Firebase App Hosting is Firebase's newer SSR offering — it provisions a Cloud Run instance per deployment. That runtime is fundamentally Google-bound. If your workspace was scaffolded for App Hosting, you're locked into Firebase Hosting until you switch to a static-export configuration. Static Hosting is for static export only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OAuth provider rejects the new domain.&lt;/strong&gt; Google sign-in, Facebook sign-in, GitHub sign-in, and other federated providers have their own allowlists &lt;em&gt;in addition to&lt;/em&gt; Firebase Auth's authorized domains list. Open each provider's app/credentials page (Google Cloud Console, Facebook Developers, GitHub OAuth Apps) and add the new Firebase Auth callback URL — usually &lt;code&gt;https://&amp;lt;project-id&amp;gt;.firebaseapp.com/__/auth/handler&lt;/code&gt; — to the authorized redirect URIs. The error usually shows up as a &lt;code&gt;redirect_uri_mismatch&lt;/code&gt; in the provider's response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud Functions return CORS errors.&lt;/strong&gt; Cloud Functions enforce origin allowlists. Update the function's CORS configuration (typically via the &lt;code&gt;cors&lt;/code&gt; package in your function source, or via Firebase Hosting rewrites that proxy through &lt;code&gt;firebase.json&lt;/code&gt; — which doesn't apply here). For DeployHQ-hosted frontends, configure CORS directly in the function code to accept your new domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firestore queries return empty results in production.&lt;/strong&gt; Almost always a Firestore Security Rules mismatch. The Firebase Studio preview environment sometimes uses a different auth context. Check the rules on the affected collections and confirm they allow the production user's auth claims.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build succeeds but environment variables don't appear in the bundle.&lt;/strong&gt; Each framework requires a specific prefix at &lt;em&gt;build time&lt;/em&gt;: Next.js needs &lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt;, Vite needs &lt;code&gt;VITE_*&lt;/code&gt;, Angular reads from a separate &lt;code&gt;environment.production.ts&lt;/code&gt; file by default. Double-check (a) the variable names match the prefix exactly, (b) you triggered a new build after setting them, and (c) for Angular, you're building with the production configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;firebase.json&lt;/code&gt; rewrites don't apply.&lt;/strong&gt; Firebase Hosting's &lt;code&gt;rewrites&lt;/code&gt; array configures the SPA fallback, URL redirects, and Cloud Function proxying. None of that runs on Static Hosting. Use DeployHQ's SPA mode for the SPA fallback. For Cloud Function proxying, point the frontend directly at the Cloud Function URL instead of relying on a &lt;code&gt;/api/*&lt;/code&gt; rewrite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next.js &lt;code&gt;next/image&lt;/code&gt; falls back to &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;.&lt;/strong&gt; Static export disables Next.js Image Optimization. For better performance, pre-optimize at build time with &lt;code&gt;sharp&lt;/code&gt; or &lt;code&gt;next-export-optimize-images&lt;/code&gt;, or use an image CDN (Cloudflare Images, Imgix, Cloudinary).&lt;/p&gt;

&lt;h2&gt;
  
  
  What you've shipped
&lt;/h2&gt;

&lt;p&gt;You now have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Firebase Studio frontend rebuilding on every push to &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Atomic deploys with no downtime during the transfer&lt;/li&gt;
&lt;li&gt;HTTPS over Cloudflare's edge with automatic certificate management&lt;/li&gt;
&lt;li&gt;One-click rollback to any previous deployment from the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; dashboard&lt;/li&gt;
&lt;li&gt;Firebase Auth, Firestore, Storage, and Cloud Functions unchanged, doing exactly what they did before&lt;/li&gt;
&lt;li&gt;A clean separation between the static UI and Google's backend services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the broader context of how Static Hosting fits among DeployHQ's other hosting types, see the &lt;a href="https://www.deployhq.com/hosting" rel="noopener noreferrer"&gt;hosting hub&lt;/a&gt;. The &lt;a href="https://dev.to/deployhq/static-hosting-on-deployhq-deploy-and-host-jamstack-sites-from-one-dashboard-40hi-temp-slug-2214878"&gt;Static Hosting pillar guide&lt;/a&gt; covers framework auto-detection, SPA mode, and the head-to-head against the most common alternatives. If you're weighing your hosting options more broadly, our &lt;a href="https://dev.to/deployhq/deployhq-static-hosting-vs-vercel-features-pricing-and-which-to-choose-2li3-temp-slug-7537336"&gt;DeployHQ Static Hosting vs Vercel&lt;/a&gt; comparison is a useful next read for the Next.js side of Firebase Studio projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;If your Firebase Studio project outgrows static export — you need real SSR, server-rendered routes you can't move to client-side rendering, or a Node service that doesn't fit the Cloud Functions model — there are two clean upgrade paths inside the same &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Move SSR rendering to &lt;a href="https://www.deployhq.com/hosting/managed-vps" rel="noopener noreferrer"&gt;DeployHQ Managed VPS&lt;/a&gt;&lt;/strong&gt; and keep using Firebase services on the backend. The &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; pipeline ships to either target; you swap the server type, not the project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split: keep Static Hosting for the public UI, run dynamic parts on Managed VPS or BYO server.&lt;/strong&gt; The static frontend stays on Cloudflare's edge; the SSR layer runs elsewhere; Firebase services keep doing their thing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Either path, the build pipeline you set up here continues to work — DeployHQ's build is generic, the target is what swaps.&lt;/p&gt;

&lt;p&gt;Built your app with a different AI tool? Bolt, Lovable, and v0 export the same kind of static frontend, each with its own per-tool quirks — our &lt;a href="https://www.deployhq.com/blog/free-static-hosting-for-ai-built-apps" rel="noopener noreferrer"&gt;free static hosting for AI-built apps&lt;/a&gt; roundup maps where each builder leaves you and how to host the result without lock-in.&lt;/p&gt;

&lt;p&gt;For a roundup of how &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; fits alongside the rest of the deployment-tool category, our &lt;a href="https://dev.to/deployhq/best-software-deployment-tools-in-2026-3g9o"&gt;best software deployment tools in 2026&lt;/a&gt; post is a useful reference. And for context on the broader &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; proposition across all five hosting types it supports, see &lt;a href="https://www.deployhq.com/blog/deployhq-your-universal-deployment-platform-for-all-hosting-types" rel="noopener noreferrer"&gt;your universal deployment and hosting platform&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're sizing what's included before signing up, the &lt;a href="https://www.deployhq.com/pricing" rel="noopener noreferrer"&gt;DeployHQ pricing page&lt;/a&gt; lays out which plan covers what — the beta-tier Static Hosting site is enough to ship this guide end to end. For the full product reference, see the &lt;a href="https://www.deployhq.com/support/servers/static-hosting" rel="noopener noreferrer"&gt;Static Hosting support library&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Questions or feedback on deploying Firebase Studio apps to &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting? Email &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or follow &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;@deployhq&lt;/a&gt; on X for product updates.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devopsinfrastructure</category>
      <category>frontend</category>
      <category>tutorials</category>
    </item>
    <item>
      <title>How to Deploy a Bolt App to DeployHQ Static Hosting</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Fri, 19 Jun 2026 15:03:34 +0000</pubDate>
      <link>https://dev.to/deployhq/how-to-deploy-a-bolt-app-to-deployhq-static-hosting-44k3</link>
      <guid>https://dev.to/deployhq/how-to-deploy-a-bolt-app-to-deployhq-static-hosting-44k3</guid>
      <description>&lt;p&gt;&lt;a href="https://www.deployhq.com/hosting/static" rel="noopener noreferrer"&gt;DeployHQ Static Hosting&lt;/a&gt; serves pre-built static assets from Cloudflare's global edge — exactly what a Bolt.new project produces when its frontend compiles. This guide walks through deploying a Bolt app end to end: export the project to GitHub, sanity-check the build output, provision the site in &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, configure the build pipeline, and ship the first deploy.&lt;/p&gt;

&lt;p&gt;This is the &lt;em&gt;static frontend&lt;/em&gt; path. Bolt starter templates often ship with a &lt;code&gt;server/&lt;/code&gt; directory running Express, Hono, or Fastify to handle data and auth — that code needs a Node runtime Static Hosting doesn't provide. If your Bolt app has a server, you have two clean options: run only the static frontend on Static Hosting and point it at your existing API, or use DeployHQ's Managed VPS option to host both the frontend and the Node server in one place. For purely client-side Bolt apps — landing pages, marketing sites, design demos, internal tools, prototypes, anything that talks to third-party APIs from the browser — Static Hosting is the cleanest fit.&lt;/p&gt;

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

&lt;p&gt;By the end of this guide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Bolt.new project exported to your own GitHub repository&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project with Static Hosting connected to that repo&lt;/li&gt;
&lt;li&gt;A working build pipeline that runs your framework's build command and uploads the output to Cloudflare's edge&lt;/li&gt;
&lt;li&gt;The site serving over HTTPS at &lt;code&gt;&amp;lt;subdomain&amp;gt;.deployhq-sites.com&lt;/code&gt; (and optionally a custom domain)&lt;/li&gt;
&lt;li&gt;Atomic deploys on every push to &lt;code&gt;main&lt;/code&gt;, with one-click rollback to any previous version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Expected time: under 15 minutes from a Bolt project you already have generated.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A Bolt.new project — generated at &lt;a href="https://bolt.new" rel="noopener noreferrer"&gt;bolt.new&lt;/a&gt; — that you're happy with&lt;/li&gt;
&lt;li&gt;A GitHub account Bolt can push to&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; account with beta features enabled (enable under &lt;strong&gt;Settings &amp;gt; Beta Features&lt;/strong&gt; )&lt;/li&gt;
&lt;li&gt;Node.js installed locally if you want to test the build before pushing (optional but recommended)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don't have a &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; account yet, you can start a free trial in the next step — the trial includes one Static Hosting site at no charge while the feature is in beta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Export your Bolt project to GitHub
&lt;/h2&gt;

&lt;p&gt;Bolt runs your project inside a StackBlitz WebContainer in the browser. To deploy it on &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, the code needs to live in a normal Git repository.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Open the project you want to deploy&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;GitHub&lt;/strong&gt; icon in the top toolbar (or &lt;strong&gt;Export&lt;/strong&gt; depending on your Bolt version)&lt;/li&gt;
&lt;li&gt;Authorize Bolt to access your GitHub account if you haven't already&lt;/li&gt;
&lt;li&gt;Choose a repository name and visibility, then push&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bolt commits the full project — &lt;code&gt;package.json&lt;/code&gt;, framework config files, &lt;code&gt;src/&lt;/code&gt;, &lt;code&gt;public/&lt;/code&gt;, and any &lt;code&gt;.bolt/&lt;/code&gt; metadata directory — to the &lt;code&gt;main&lt;/code&gt; branch of the new repository.&lt;/p&gt;

&lt;p&gt;Open the repo in GitHub and confirm &lt;code&gt;package.json&lt;/code&gt; is at the root. You'll use it to identify the build command and output directory in the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Identify the framework and output directory
&lt;/h2&gt;

&lt;p&gt;Bolt projects are framework-agnostic. The right Static Hosting configuration depends on which framework Bolt scaffolded for you. The build output directory is what matters — Static Hosting uploads the contents of that directory to the edge.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Default build command&lt;/th&gt;
&lt;th&gt;Output directory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vite + React&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dist/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vite + Vue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dist/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vite + Svelte&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dist/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Astro&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dist/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SvelteKit (adapter-static)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;build/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js (static export)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;out/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remix (with static adapter)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;build/client/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Open &lt;code&gt;package.json&lt;/code&gt; and confirm the &lt;code&gt;build&lt;/code&gt; script matches what's in the table. Open the framework config (e.g., &lt;code&gt;vite.config.ts&lt;/code&gt;, &lt;code&gt;astro.config.mjs&lt;/code&gt;, &lt;code&gt;next.config.js&lt;/code&gt;) and note any custom output directory if it's been overridden.&lt;/p&gt;

&lt;p&gt;If your Bolt project is a Next.js starter, ensure &lt;code&gt;output: 'export'&lt;/code&gt; is set in &lt;code&gt;next.config.js&lt;/code&gt; — Bolt's default Next.js scaffold assumes a Node runtime. The &lt;a href="https://www.deployhq.com/guides/next" rel="noopener noreferrer"&gt;Next.js deployment guide&lt;/a&gt; covers the full static-export configuration.&lt;/p&gt;

&lt;p&gt;If your project includes a &lt;code&gt;server/&lt;/code&gt; directory (Express, Hono, Fastify, etc.), that code won't run on Static Hosting — make sure none of your frontend code depends on those endpoints at runtime, or refactor those calls to hit a separate API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Provision a Static Hosting site in DeployHQ
&lt;/h2&gt;

&lt;p&gt;In your &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project (or create one and connect it to the Bolt repository), click &lt;strong&gt;New Server&lt;/strong&gt; :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enter a name for the server — internal reference only, doesn't affect the public URL&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Static Hosting&lt;/strong&gt; from the protocol picker under &lt;strong&gt;Hosting&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Choose a subdomain — your site serves at &lt;code&gt;&amp;lt;subdomain&amp;gt;.deployhq-sites.com&lt;/code&gt;. Subdomains are unique across all &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; accounts; lowercase letters, numbers, and hyphens only&lt;/li&gt;
&lt;li&gt;Set the &lt;strong&gt;subdirectory to deploy from&lt;/strong&gt; to match your framework's output directory from Step 2 (&lt;code&gt;dist/&lt;/code&gt;, &lt;code&gt;out/&lt;/code&gt;, &lt;code&gt;build/&lt;/code&gt;, or &lt;code&gt;build/client/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Toggle &lt;strong&gt;SPA mode&lt;/strong&gt; based on your framework:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On&lt;/strong&gt; for Vite + React SPA, Vite + Vue SPA, Vite + Svelte SPA — these single HTML files use client-side routing and need unknown paths rewritten to &lt;code&gt;index.html&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Off&lt;/strong&gt; for Astro, SvelteKit static, and Next.js static export — these generate one HTML file per route at build time&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create Server&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; runs framework detection against the connected repository. If a &lt;q&gt;[Framework] detected!&lt;/q&gt; callout appears with a suggested output directory and SPA mode, the rule-based detector is doing the work for you. Accept the suggested values unless you have a non-default config.&lt;/p&gt;

&lt;p&gt;Provisioning takes under a minute. Once the status flips to active, the site exists on Cloudflare's edge — but no code has shipped yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Configure the build pipeline
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; runs your build before transferring artifacts to the edge. For a Bolt-exported project, the &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;build pipeline&lt;/a&gt; needs two stages: install, then build.&lt;/p&gt;

&lt;p&gt;In the project's build settings, add:&lt;br&gt;
&lt;/p&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;If your &lt;code&gt;package.json&lt;/code&gt; declares &lt;code&gt;pnpm&lt;/code&gt; or &lt;code&gt;yarn&lt;/code&gt; as the package manager (Bolt sometimes does, especially for monorepo-style starters), swap to &lt;code&gt;pnpm install --frozen-lockfile&lt;/code&gt; or &lt;code&gt;yarn install --frozen-lockfile&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Environment variables to set in DeployHQ's environment-variable UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public-prefixed env vars your framework inlines into the static bundle (Vite: &lt;code&gt;VITE_*&lt;/code&gt;, Next.js: &lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt;, Astro: &lt;code&gt;PUBLIC_*&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NODE_ENV=production&lt;/code&gt; — usually set automatically but worth confirming for deterministic builds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anything that isn't prefixed for client exposure stays out of the bundle — that's the framework's standard behavior, not a Static Hosting limitation. If your Bolt project talks to Supabase, Stripe (publishable key), or a third-party API, the URLs and public keys go here. Service-role keys, secret API tokens, and database credentials never belong in a static build.&lt;/p&gt;

&lt;p&gt;Ready to take it live? &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Sign up free for DeployHQ&lt;/a&gt; if you don't have an account, and the beta-tier Static Hosting site is enough to ship this guide end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: First deploy
&lt;/h2&gt;

&lt;p&gt;Push any change to the configured branch (typically &lt;code&gt;main&lt;/code&gt;), or trigger a manual deployment from the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; dashboard. DeployHQ:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clones the repo at the head of the configured branch&lt;/li&gt;
&lt;li&gt;Runs the build pipeline (&lt;code&gt;npm ci &amp;amp;&amp;amp; npm run build&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Uploads the contents of your output directory to object storage atomically&lt;/li&gt;
&lt;li&gt;Flips Cloudflare's edge routing to serve the new version&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The deployment log streams each step in real time. When it completes, visit &lt;code&gt;https://&amp;lt;subdomain&amp;gt;.deployhq-sites.com&lt;/code&gt; and confirm the site renders.&lt;/p&gt;

&lt;p&gt;If the build fails, the log shows the exact error. Most common failures for Bolt-exported projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A starter template that includes a &lt;code&gt;server/&lt;/code&gt; directory the frontend code depends on at build time&lt;/li&gt;
&lt;li&gt;Hardcoded paths starting with &lt;code&gt;/home/project/&lt;/code&gt; left over from the WebContainer environment&lt;/li&gt;
&lt;li&gt;A missing dependency the &lt;code&gt;package.json&lt;/code&gt; declares but Bolt didn't ship a lockfile entry for&lt;/li&gt;
&lt;li&gt;Framework-specific config overrides (custom &lt;code&gt;outDir&lt;/code&gt;, &lt;code&gt;base&lt;/code&gt;, or &lt;code&gt;publicPath&lt;/code&gt;) the build pipeline doesn't know about&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix the issue in the repo, commit, push — &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; runs the next build automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Add a custom domain
&lt;/h2&gt;

&lt;p&gt;To serve from your own domain (e.g., &lt;code&gt;app.example.com&lt;/code&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In your DNS provider, add a &lt;code&gt;CNAME&lt;/code&gt; record pointing your subdomain to &lt;code&gt;&amp;lt;subdomain&amp;gt;.deployhq-sites.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Wait for DNS propagation (usually minutes; depends on your TTL)&lt;/li&gt;
&lt;li&gt;Cloudflare provisions an HTTPS certificate automatically once the CNAME resolves&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your site now serves from &lt;code&gt;app.example.com&lt;/code&gt; over HTTPS via Cloudflare's edge.&lt;/p&gt;

&lt;p&gt;For an apex domain (&lt;code&gt;example.com&lt;/code&gt; with no subdomain), use an ALIAS or ANAME record if your DNS provider supports them — Cloudflare DNS, DNSimple, and Route 53 all do. Otherwise, host the apex elsewhere and CNAME a &lt;code&gt;www&lt;/code&gt; subdomain to &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The build pulls in &lt;code&gt;server/&lt;/code&gt; files.&lt;/strong&gt; Bolt full-stack starter templates expect the server and client to coexist. If your &lt;code&gt;vite.config.ts&lt;/code&gt; or framework config includes a path alias pointing into &lt;code&gt;server/&lt;/code&gt;, the build will try to bundle Node-only modules and fail. Either remove the alias for a pure frontend deployment, or move the server to &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardcoded WebContainer paths.&lt;/strong&gt; Bolt sometimes leaves &lt;code&gt;/home/project/&lt;/code&gt; paths in generated code, especially in scripts or test fixtures. These break in any environment that isn't a WebContainer. Search the repo for &lt;code&gt;/home/project/&lt;/code&gt; and replace with relative paths or &lt;code&gt;process.cwd()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build succeeds but the page is blank.&lt;/strong&gt; Vite + React SPAs often need &lt;code&gt;base: '/'&lt;/code&gt; set explicitly in &lt;code&gt;vite.config.ts&lt;/code&gt; when deployed at the root. If you're deploying under a subpath, set &lt;code&gt;base: '/subpath/'&lt;/code&gt; to match. Check the browser console — a 404 on &lt;code&gt;/assets/index-*.js&lt;/code&gt; is the giveaway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tailwind classes missing in production.&lt;/strong&gt; Bolt's Vite scaffold sometimes uses content paths that miss files in nested directories. Verify &lt;code&gt;tailwind.config.ts&lt;/code&gt; includes every path your components actually live under — &lt;code&gt;./src/**/*.{js,ts,jsx,tsx,vue,svelte}&lt;/code&gt; covers the standard layout, but Bolt's project structure can vary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Routes 404 on direct page load (Vite SPA).&lt;/strong&gt; This is SPA mode not being enabled in &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;. Toggle it on — Static Hosting will rewrite unknown paths to &lt;code&gt;index.html&lt;/code&gt; so client-side routing takes over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supabase auth callback fails.&lt;/strong&gt; If your Bolt app uses Supabase and the auth callback URL points to a &lt;code&gt;/auth/callback&lt;/code&gt; route handler, it'll 404 because that route handler can't run on Static Hosting. Move the auth-callback logic into client-side code that reads the URL hash, or run the callback handler on a separate backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you've shipped
&lt;/h2&gt;

&lt;p&gt;You now have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Bolt project rebuilding on every push to &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Atomic deploys with no downtime during the transfer&lt;/li&gt;
&lt;li&gt;HTTPS over Cloudflare's edge with automatic certificate management&lt;/li&gt;
&lt;li&gt;One-click rollback to any previous deployment from the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; dashboard&lt;/li&gt;
&lt;li&gt;Predictable, free hosting while Static Hosting is in beta&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the broader context of how Static Hosting fits among DeployHQ's other hosting types, see the &lt;a href="https://www.deployhq.com/hosting" rel="noopener noreferrer"&gt;hosting hub&lt;/a&gt;. The &lt;a href="https://dev.to/deployhq/static-hosting-on-deployhq-deploy-and-host-jamstack-sites-from-one-dashboard-40hi-temp-slug-2214878"&gt;Static Hosting pillar guide&lt;/a&gt; covers framework auto-detection, SPA mode, and the head-to-head against the most common alternatives. Since Bolt ships with a one-click Netlify deploy button, the &lt;a href="https://www.deployhq.com/blog/deployhq-static-hosting-vs-netlify" rel="noopener noreferrer"&gt;DeployHQ Static Hosting vs Netlify&lt;/a&gt; comparison is directly relevant when you're choosing where to host outside the built-in option.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;If your Bolt project outgrows static export — you need to run the &lt;code&gt;server/&lt;/code&gt; directory in production, add real auth, or handle dynamic data without a separate backend — there are two clean upgrade paths inside the same &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Move to &lt;a href="https://www.deployhq.com/hosting/managed-vps" rel="noopener noreferrer"&gt;DeployHQ Managed VPS&lt;/a&gt;&lt;/strong&gt; and run the full Node app — both your frontend bundle and your &lt;code&gt;server/&lt;/code&gt; code in one place. The &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; pipeline ships to either target; you swap the server type, not the project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split: keep Static Hosting for the UI, run the &lt;code&gt;server/&lt;/code&gt; directory elsewhere.&lt;/strong&gt; A separate Managed VPS or BYO server hosts the API, and the static frontend stays on Cloudflare's edge.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Either path, the build pipeline you set up here continues to work — DeployHQ's build is generic, the target is what swaps.&lt;/p&gt;

&lt;p&gt;For a roundup of how &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; fits alongside the rest of the deployment-tool category, our &lt;a href="https://dev.to/deployhq/best-software-deployment-tools-in-2026-3g9o"&gt;best software deployment tools in 2026&lt;/a&gt; post is a useful reference. And for context on the broader &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; proposition across all five hosting types it supports, see &lt;a href="https://www.deployhq.com/blog/deployhq-your-universal-deployment-platform-for-all-hosting-types" rel="noopener noreferrer"&gt;your universal deployment and hosting platform&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're sizing what's included before signing up, the &lt;a href="https://www.deployhq.com/pricing" rel="noopener noreferrer"&gt;DeployHQ pricing page&lt;/a&gt; lays out which plan covers what — the beta-tier Static Hosting site is enough to ship this guide end to end. For the full product reference, see the &lt;a href="https://www.deployhq.com/support/servers/static-hosting" rel="noopener noreferrer"&gt;Static Hosting support library&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Questions or feedback on deploying Bolt projects to &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting? Email &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or follow &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;@deployhq&lt;/a&gt; on X for product updates.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>bolt</category>
      <category>frontend</category>
      <category>tutorials</category>
    </item>
    <item>
      <title>How to Deploy a v0 App to DeployHQ Static Hosting</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Thu, 18 Jun 2026 05:45:50 +0000</pubDate>
      <link>https://dev.to/deployhq/how-to-deploy-a-v0-app-to-deployhq-static-hosting-35oi</link>
      <guid>https://dev.to/deployhq/how-to-deploy-a-v0-app-to-deployhq-static-hosting-35oi</guid>
      <description>&lt;p&gt;&lt;a href="https://www.deployhq.com/hosting/static" rel="noopener noreferrer"&gt;DeployHQ Static Hosting&lt;/a&gt; runs static builds on Cloudflare's global edge — exactly the shape v0 produces when you export a project as a Next.js codebase. This guide walks through deploying a v0 app end to end: export the project to GitHub, provision the site in &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, configure the build pipeline, and ship the first deploy.&lt;/p&gt;

&lt;p&gt;This is the &lt;em&gt;static-export&lt;/em&gt; path. If your v0 project uses Next.js server actions, App Router server components, or any backend route handlers, Static Hosting can't run that code — those parts need a Node runtime on DeployHQ's Managed VPS option, a separate serverless layer, or an existing API you call from the client. For purely static UI — marketing pages, landing pages, design demos, internal tools, prototypes — Static Hosting is the cleanest fit.&lt;/p&gt;

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

&lt;p&gt;By the end of this guide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A v0 project exported to your own GitHub repository&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project with Static Hosting connected to that repo&lt;/li&gt;
&lt;li&gt;A working build pipeline that runs &lt;code&gt;next build&lt;/code&gt; (with static export) and uploads the output to Cloudflare's edge&lt;/li&gt;
&lt;li&gt;The site serving over HTTPS at &lt;code&gt;&amp;lt;subdomain&amp;gt;.deployhq-sites.com&lt;/code&gt; (and optionally a custom domain)&lt;/li&gt;
&lt;li&gt;Atomic deploys on every push to &lt;code&gt;main&lt;/code&gt;, with one-click rollback to any previous version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Expected time: under 15 minutes from a v0 project you already have generated.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A v0 project — generated at &lt;a href="https://v0.app" rel="noopener noreferrer"&gt;v0.app&lt;/a&gt; — that you're happy with&lt;/li&gt;
&lt;li&gt;A GitHub account v0 can push to&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; account with beta features enabled (enable under &lt;strong&gt;Settings &amp;gt; Beta Features&lt;/strong&gt; )&lt;/li&gt;
&lt;li&gt;Node.js installed locally if you want to test the build before pushing (optional but recommended)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don't have a &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; account yet, you can start a free trial in the next step — the trial includes one Static Hosting site at no charge while the feature is in beta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Export your v0 project to GitHub
&lt;/h2&gt;

&lt;p&gt;v0 generates Next.js code. To deploy it anywhere outside v0's built-in Vercel target, you need the code in your own repository.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Open the project you want to deploy&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;GitHub&lt;/strong&gt; icon in the project toolbar (or &lt;strong&gt;Export&lt;/strong&gt; → &lt;strong&gt;GitHub&lt;/strong&gt; depending on your v0 version)&lt;/li&gt;
&lt;li&gt;Authorize v0 to access your GitHub account if you haven't already&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Create new repository&lt;/strong&gt; and pick a name, or &lt;strong&gt;Push to existing repository&lt;/strong&gt; if you've already exported once&lt;/li&gt;
&lt;li&gt;Confirm the push&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;v0 commits the full Next.js project — &lt;code&gt;app/&lt;/code&gt;, &lt;code&gt;components/&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;next.config.js&lt;/code&gt;, &lt;code&gt;tailwind.config.ts&lt;/code&gt;, and the rest — to the &lt;code&gt;main&lt;/code&gt; branch of the repository you selected.&lt;/p&gt;

&lt;p&gt;Open the repo in GitHub and confirm &lt;code&gt;next.config.js&lt;/code&gt; (or &lt;code&gt;next.config.mjs&lt;/code&gt;) is present. You'll edit it in the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Configure Next.js for static export
&lt;/h2&gt;

&lt;p&gt;v0's generated &lt;code&gt;next.config.js&lt;/code&gt; is set up for Vercel by default, which assumes a Node runtime. Static Hosting needs the export build instead.&lt;/p&gt;

&lt;p&gt;Edit &lt;code&gt;next.config.js&lt;/code&gt; to add &lt;code&gt;output: 'export'&lt;/code&gt; and disable Next.js Image Optimization (it requires a server):&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="cm"&gt;/** @type {import('next').NextConfig} */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&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;export&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;unoptimized&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="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&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="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;A few things this changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;next build&lt;/code&gt; writes a static export to &lt;code&gt;out/&lt;/code&gt; instead of starting a Node server&lt;/li&gt;
&lt;li&gt;Any route using &lt;code&gt;getServerSideProps&lt;/code&gt;, server actions, route handlers, or middleware will fail the build — those features need a runtime Static Hosting doesn't provide&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;next/image&lt;/code&gt; falls back to plain &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; rendering — no on-the-fly resizing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Commit and push the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add next.config.js
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Configure Next.js for static export"&lt;/span&gt;
git push origin main

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

&lt;/div&gt;



&lt;p&gt;Optionally, run &lt;code&gt;npm install &amp;amp;&amp;amp; npm run build&lt;/code&gt; locally first to catch any server-only code paths v0 emitted before &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; runs the same build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Provision a Static Hosting site in DeployHQ
&lt;/h2&gt;

&lt;p&gt;In your &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project (or create one and connect it to the v0 repository), click &lt;strong&gt;New Server&lt;/strong&gt; :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enter a name for the server — internal reference only, doesn't affect the public URL&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Static Hosting&lt;/strong&gt; from the protocol picker under &lt;strong&gt;Hosting&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Choose a subdomain — your site serves at &lt;code&gt;&amp;lt;subdomain&amp;gt;.deployhq-sites.com&lt;/code&gt;. Subdomains are unique across all &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; accounts; lowercase letters, numbers, and hyphens only&lt;/li&gt;
&lt;li&gt;Set the &lt;strong&gt;subdirectory to deploy from&lt;/strong&gt; to &lt;code&gt;out&lt;/code&gt; (the default Next.js static export directory)&lt;/li&gt;
&lt;li&gt;Leave &lt;strong&gt;SPA mode&lt;/strong&gt; &lt;em&gt;off&lt;/em&gt; — Next.js static export generates one HTML file per route, so client-side routing fallbacks aren't needed&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create Server&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; runs framework detection against the connected repository. If a &lt;q&gt;Next.js detected!&lt;/q&gt; callout appears, the rule-based detector picked up your &lt;code&gt;next.config.js&lt;/code&gt;. Accept the suggested values for a stock v0 export.&lt;/p&gt;

&lt;p&gt;Provisioning takes under a minute. Once the status flips to active, the site exists on Cloudflare's edge — but no code has shipped yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Configure the build pipeline
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; runs your build before transferring artifacts to the edge. For a v0-exported Next.js project, the &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;build pipeline&lt;/a&gt; needs two stages: install, then build.&lt;/p&gt;

&lt;p&gt;In the project's build settings, add:&lt;br&gt;
&lt;/p&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;If v0 emitted a &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; or &lt;code&gt;yarn.lock&lt;/code&gt; instead of &lt;code&gt;package-lock.json&lt;/code&gt;, swap to &lt;code&gt;pnpm install --frozen-lockfile&lt;/code&gt; or &lt;code&gt;yarn install --frozen-lockfile&lt;/code&gt; accordingly.&lt;/p&gt;

&lt;p&gt;Environment variables to set in DeployHQ's environment-variable UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt; — any public env vars Next.js inlines into the static bundle (API base URLs, public keys, feature flags)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NODE_ENV=production&lt;/code&gt; — usually set automatically but worth confirming for deterministic builds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anything not prefixed with &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; will not appear in the static bundle — that's Next.js's standard behavior, not a Static Hosting quirk.&lt;/p&gt;

&lt;p&gt;If your v0 project hits external APIs (Supabase for auth, a database backend, third-party services), the URLs and public keys go here. Secrets that should never reach the client — service-role keys, private API tokens — stay out of the static build entirely.&lt;/p&gt;

&lt;p&gt;Ready to take it live? &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Sign up free for DeployHQ&lt;/a&gt; if you don't have an account, and the beta-tier Static Hosting site is enough to ship this guide end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: First deploy
&lt;/h2&gt;

&lt;p&gt;Push any change to the configured branch (typically &lt;code&gt;main&lt;/code&gt;), or trigger a manual deployment from the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; dashboard. DeployHQ:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clones the repo at the head of the configured branch&lt;/li&gt;
&lt;li&gt;Runs the build pipeline (&lt;code&gt;npm ci &amp;amp;&amp;amp; npm run build&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Uploads the contents of &lt;code&gt;out/&lt;/code&gt; to object storage atomically&lt;/li&gt;
&lt;li&gt;Flips Cloudflare's edge routing to serve the new version&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The deployment log streams each step in real time. When it completes, visit &lt;code&gt;https://&amp;lt;subdomain&amp;gt;.deployhq-sites.com&lt;/code&gt; and confirm the site renders.&lt;/p&gt;

&lt;p&gt;If the build fails, the log shows the exact error. Most common failures for v0 exports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A page or route handler v0 generated that uses a server-only feature (server actions, &lt;code&gt;getServerSideProps&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;next/image&lt;/code&gt; used without &lt;code&gt;unoptimized: true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A missing dependency v0's &lt;code&gt;package.json&lt;/code&gt; declares but didn't ship with a lockfile entry&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix the issue in the repo, commit, push — &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; runs the next build automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Add a custom domain
&lt;/h2&gt;

&lt;p&gt;To serve from your own domain (e.g., &lt;code&gt;app.example.com&lt;/code&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In your DNS provider, add a &lt;code&gt;CNAME&lt;/code&gt; record pointing your subdomain to &lt;code&gt;&amp;lt;subdomain&amp;gt;.deployhq-sites.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Wait for DNS propagation (usually minutes; depends on your TTL)&lt;/li&gt;
&lt;li&gt;Cloudflare provisions an HTTPS certificate automatically once the CNAME resolves&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your site now serves from &lt;code&gt;app.example.com&lt;/code&gt; over HTTPS via Cloudflare's edge.&lt;/p&gt;

&lt;p&gt;For an apex domain (&lt;code&gt;example.com&lt;/code&gt; with no subdomain), use an ALIAS or ANAME record if your DNS provider supports them — Cloudflare DNS, DNSimple, and Route 53 all do. Otherwise, host the apex elsewhere and CNAME a &lt;code&gt;www&lt;/code&gt; subdomain to &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Server actions fail at build.&lt;/strong&gt; v0 sometimes generates form submissions as server actions. Static export can't run server code at request time, so those builds break. Refactor server actions into client-side fetches that hit a separate API (your existing backend, a Cloudflare Worker, Supabase, etc.).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Route handlers (&lt;code&gt;app/api/*&lt;/code&gt;) don't work.&lt;/strong&gt; Same root cause — they need a Node runtime. Move that logic to a separate backend, or call third-party APIs directly from the client when the keys are public.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Images render but look unoptimized.&lt;/strong&gt; &lt;code&gt;next/image&lt;/code&gt; skips its optimization pipeline under &lt;code&gt;unoptimized: true&lt;/code&gt;. For better performance, pre-optimize at build time with &lt;code&gt;sharp&lt;/code&gt; or &lt;code&gt;next-export-optimize-images&lt;/code&gt;, or use an image CDN (Cloudflare Images, Imgix, Cloudinary).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build succeeds but routes 404 on the live site.&lt;/strong&gt; v0 dynamic routes (&lt;code&gt;[slug].tsx&lt;/code&gt;) need &lt;code&gt;generateStaticParams&lt;/code&gt; to enumerate which paths to render at build time. Without that, the export skips the dynamic routes entirely. Add &lt;code&gt;generateStaticParams&lt;/code&gt; returning the list of slugs you want pre-rendered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tailwind classes missing in production.&lt;/strong&gt; v0 occasionally generates class names that Tailwind's content scanner misses. Double-check &lt;code&gt;tailwind.config.ts&lt;/code&gt; includes every path your components live under — &lt;code&gt;./app/ **/*.{js,ts,jsx,tsx,mdx}&lt;/code&gt; and &lt;code&gt;./components/** /*.{js,ts,jsx,tsx,mdx}&lt;/code&gt; cover most v0 projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trailing slashes change URLs mid-project.&lt;/strong&gt; If you add &lt;code&gt;trailingSlash: true&lt;/code&gt; to &lt;code&gt;next.config.js&lt;/code&gt; after the first deploy, every route URL changes. Set it intentionally at the start, or commit to a redirect plan if you switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you've shipped
&lt;/h2&gt;

&lt;p&gt;You now have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A v0 project rebuilding on every push to &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Atomic deploys with no downtime during the transfer&lt;/li&gt;
&lt;li&gt;HTTPS over Cloudflare's edge with automatic certificate management&lt;/li&gt;
&lt;li&gt;One-click rollback to any previous deployment from the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; dashboard&lt;/li&gt;
&lt;li&gt;Predictable, free hosting while Static Hosting is in beta&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the broader context of how Static Hosting fits among DeployHQ's other hosting types, see the &lt;a href="https://www.deployhq.com/hosting" rel="noopener noreferrer"&gt;hosting hub&lt;/a&gt;. The &lt;a href="https://dev.to/deployhq/static-hosting-on-deployhq-deploy-and-host-jamstack-sites-from-one-dashboard-40hi-temp-slug-2214878"&gt;Static Hosting pillar guide&lt;/a&gt; covers framework auto-detection, SPA mode, and the head-to-head against the most common alternatives — including our &lt;a href="https://dev.to/deployhq/deployhq-static-hosting-vs-vercel-features-pricing-and-which-to-choose-2li3-temp-slug-7537336"&gt;DeployHQ Static Hosting vs Vercel&lt;/a&gt; comparison, which is directly relevant for v0 users weighing where to host outside Vercel itself.&lt;/p&gt;

&lt;p&gt;For the full Next.js side of the workflow, the &lt;a href="https://www.deployhq.com/guides/next" rel="noopener noreferrer"&gt;Next.js deployment guide&lt;/a&gt; goes deeper on configuration choices that apply equally to v0-exported projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;If your v0 project outgrows static export — you add real auth, dynamic data, or server actions you can't refactor away — there are two clean upgrade paths inside the same &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Move to &lt;a href="https://www.deployhq.com/hosting/managed-vps" rel="noopener noreferrer"&gt;DeployHQ Managed VPS&lt;/a&gt;&lt;/strong&gt; and run a full Next.js Node process. The &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; pipeline you built ships to either target; you swap the server type, not the project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split: keep Static Hosting for the UI, run dynamic parts elsewhere.&lt;/strong&gt; A separate Managed VPS or BYO server handles the API/SSR, and the static frontend stays on Cloudflare's edge.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Either path, the build pipeline you set up here doesn't change.&lt;/p&gt;

&lt;p&gt;For a roundup of how &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; fits alongside the rest of the deployment-tool category, our &lt;a href="https://dev.to/deployhq/best-software-deployment-tools-in-2026-3g9o"&gt;best software deployment tools in 2026&lt;/a&gt; post is a useful reference. And for context on the broader &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; proposition across all five hosting types it supports, see &lt;a href="https://www.deployhq.com/blog/deployhq-your-universal-deployment-platform-for-all-hosting-types" rel="noopener noreferrer"&gt;your universal deployment and hosting platform&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're sizing what's included before signing up, the &lt;a href="https://www.deployhq.com/pricing" rel="noopener noreferrer"&gt;DeployHQ pricing page&lt;/a&gt; lays out which plan covers what — the beta-tier Static Hosting site is enough to ship this guide end to end. For the full product reference, see the &lt;a href="https://www.deployhq.com/support/servers/static-hosting" rel="noopener noreferrer"&gt;Static Hosting support library&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Questions or feedback on deploying v0 projects to &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting? Email &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or follow &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;@deployhq&lt;/a&gt; on X for product updates.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>v0</category>
      <category>frontend</category>
      <category>tutorials</category>
    </item>
    <item>
      <title>DeployHQ Static Hosting vs Vercel: Features, Pricing, and Which to Choose</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Mon, 15 Jun 2026 13:11:36 +0000</pubDate>
      <link>https://dev.to/deployhq/deployhq-static-hosting-vs-vercel-features-pricing-and-which-to-choose-40ke</link>
      <guid>https://dev.to/deployhq/deployhq-static-hosting-vs-vercel-features-pricing-and-which-to-choose-40ke</guid>
      <description>&lt;p&gt;Choosing between &lt;a href="https://www.deployhq.com/hosting/static" rel="noopener noreferrer"&gt;DeployHQ Static Hosting&lt;/a&gt; and Vercel comes down to one question: are you only shipping a static frontend, or do you have a backend in the picture too? Both run on globally distributed edge networks. Both auto-detect popular frameworks. Both let you push to a Git branch and have a deployed site within a minute. The differences sit one layer up — in pricing predictability, in what you can deploy alongside the static site, and in how locked-in you are to the platform's runtime model.&lt;/p&gt;

&lt;p&gt;This guide compares the two head to head: feature parity, where each one wins, current pricing, and the migration path between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;If you're building a Next.js application that uses server-side rendering, edge middleware, or ISR — choose Vercel. It's the platform built by the Next.js team and nothing else comes close for Next.js-specific features.&lt;/p&gt;

&lt;p&gt;If you're shipping a static site or SPA and you already deploy backend code somewhere (a Laravel API on a VPS, a Rails app on a managed server, a WordPress install on shared hosting), &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting gives you one workflow and one bill for the whole stack. The static frontend rides on the same Cloudflare edge that Pages uses, while the same &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project can ship your backend code to wherever it actually runs.&lt;/p&gt;

&lt;p&gt;For a broader view of where each tool sits in the deployment landscape, our roundup of &lt;a href="https://dev.to/deployhq/best-software-deployment-tools-in-2026-3g9o"&gt;the best software deployment tools in 2026&lt;/a&gt; covers Vercel, Netlify, Cloudflare Pages, and &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; alongside the rest of the category.&lt;/p&gt;

&lt;h2&gt;
  
  
  At a glance: feature comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;DeployHQ Static Hosting&lt;/th&gt;
&lt;th&gt;Vercel&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Edge network&lt;/td&gt;
&lt;td&gt;Cloudflare's global edge&lt;/td&gt;
&lt;td&gt;Vercel Edge Network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Framework auto-detection&lt;/td&gt;
&lt;td&gt;Yes (rule + AI fallback)&lt;/td&gt;
&lt;td&gt;Best-in-class for Next.js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Atomic deploys&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom domains + SSL&lt;/td&gt;
&lt;td&gt;Yes (automatic)&lt;/td&gt;
&lt;td&gt;Yes (automatic)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPA mode (client-side routing)&lt;/td&gt;
&lt;td&gt;Yes (toggle)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server-side rendering&lt;/td&gt;
&lt;td&gt;No (static only)&lt;/td&gt;
&lt;td&gt;Yes (full SSR + ISR)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge functions / middleware&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image optimization&lt;/td&gt;
&lt;td&gt;No (use a CDN proxy)&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy previews per branch&lt;/td&gt;
&lt;td&gt;Per-environment in same project&lt;/td&gt;
&lt;td&gt;Per pull request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend deploys in same pipeline&lt;/td&gt;
&lt;td&gt;Yes (VPS, shared, cloud, S3)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing model&lt;/td&gt;
&lt;td&gt;Fixed monthly per site&lt;/td&gt;
&lt;td&gt;Tiered + usage (bandwidth, functions, builds)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hobby/free tier&lt;/td&gt;
&lt;td&gt;Trial includes 1 site&lt;/td&gt;
&lt;td&gt;Hobby (free, single seat, 100 GB transfer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vendor lock-in&lt;/td&gt;
&lt;td&gt;Low (move the build elsewhere any time)&lt;/td&gt;
&lt;td&gt;Higher (forms, image opt, edge fns tied to Vercel)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both products are mature on the basics. The real divergence is in (a) whether SSR matters to your project, and (b) whether you have non-static code that needs deploying too.&lt;/p&gt;

&lt;h2&gt;
  
  
  When DeployHQ Static Hosting is the right choice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You're deploying a static site or SPA, not a server-rendered Next.js app.&lt;/strong&gt; Static Hosting is built for compiled output — Hugo sites, Jekyll documentation, Astro marketing pages, statically-exported Next.js builds, SvelteKit pre-renders, plain React/Vue/Angular SPAs. If your build produces a folder of HTML/CSS/JS and assets, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting will serve it from Cloudflare's edge with the same latency profile Cloudflare Pages offers. See &lt;a href="https://www.deployhq.com/guides/next" rel="noopener noreferrer"&gt;the Next.js deployment guide&lt;/a&gt; for the static-export workflow specifically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You already deploy backend code with &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; — or are planning to.&lt;/strong&gt; This is the workflow-consolidation case. Most teams shipping a marketing site or web app have &lt;em&gt;something&lt;/em&gt; on the backend: a Laravel API on a VPS, a Node service on Hetzner, a WordPress install on shared hosting. If that backend lives in &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, the static frontend can sit in the same project, share environments and pipelines, and bill on the same monthly invoice. If you're considering &lt;a href="https://www.deployhq.com/hosting/managed-vps" rel="noopener noreferrer"&gt;DeployHQ Managed VPS Hosting&lt;/a&gt; for the backend, the static frontend can ride along on Static Hosting in the same project without any extra setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You want predictable pricing that doesn't spike with traffic.&lt;/strong&gt; Vercel's pricing is tiered with usage-based bandwidth, function invocations, and build minutes — fine on Hobby, surprising on Pro when a post lands on Hacker News. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting charges a fixed monthly rate per site. A traffic spike is a great problem; it shouldn't also be a billing problem. &lt;a href="https://www.deployhq.com/hosting" rel="noopener noreferrer"&gt;See the full&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; hosting catalog for an overview of where Static Hosting sits alongside the other managed options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You don't want vendor lock-in.&lt;/strong&gt; Vercel's value-adds (image optimization, forms, edge middleware, analytics) are tied to Vercel. If you ever want to move to a different host, you rewrite those pieces. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting is intentionally thin — it serves your build output and that's most of it. If you outgrow it, your build still produces the same artifact and works anywhere else that serves static files.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Vercel is the right choice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You're building a Next.js application that uses SSR, ISR, or App Router server components.&lt;/strong&gt; Vercel is the platform built by the Next.js team. Server-rendered routes, incremental static regeneration, React Server Components, streaming, edge middleware — all first-class. If your Next.js app does anything beyond pure static export, Vercel is hard to beat. (If your Next.js app is &lt;em&gt;only&lt;/em&gt; using &lt;code&gt;export&lt;/code&gt;, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting works just as well and costs less; but that's a small subset of Next.js projects.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You depend on edge functions or middleware for per-request logic.&lt;/strong&gt; Need to A/B test, rewrite URLs, do geo-based routing, or run authentication at the edge? Vercel's edge runtime is built for that. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting doesn't run code at request time — you'd put Cloudflare Workers in front of it to add that layer, or move to a runtime that supports middleware natively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy previews with commenting is your team's workflow.&lt;/strong&gt; Vercel's per-PR preview links with inline comments are excellent for design and product review. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; supports per-environment deployments and you can preview each branch, but the per-PR preview-comment UX is a Vercel-specific strength.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You want analytics, image optimization, and forms in one bundled tool.&lt;/strong&gt; Vercel's product surface is broader. If you'd rather not pick a CDN, an image-optimization service, and a form backend separately, the bundle has real ergonomics value — at the cost of being more tied to Vercel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing: side by side
&lt;/h2&gt;

&lt;p&gt;Quick comparison at typical small-team scales. Numbers are accurate as of June 2026 — check vendor pricing pages for the current rates before you commit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vercel Hobby&lt;/strong&gt; is free, single developer seat, includes 100 GB fast data transfer, 1M function invocations, 4 hours of active function CPU per month. Good for hobby projects and very small sites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vercel Pro&lt;/strong&gt; is $20 per seat per month plus usage. Includes 1 TB fast data transfer and 1M function invocations included; beyond that, bandwidth, function CPU, and build minutes are usage-billed. Build minutes are priced per machine type ($0.014/min Standard, $0.028/min Enhanced, $0.105/min Turbo). A team site with moderate traffic typically lands in the $40-100/month range — sometimes much higher with bandwidth spikes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DeployHQ Static Hosting&lt;/strong&gt; is billed at a fixed monthly rate per site — the rate shown on the Hosted Resources page and on the site setup form in your account's currency, on the same monthly invoice as your &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; plan. Trial accounts can provision one Static Hosting site at no charge. No per-bandwidth, per-build-minute, or per-function-invocation overage to worry about.&lt;/p&gt;

&lt;p&gt;If you also need a backend deployed (the consolidation case), the comparison shifts — Vercel Pro plus a separate VPS plus a separate deployment tool quickly exceeds the bundled &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; price. &lt;a href="https://www.deployhq.com/pricing" rel="noopener noreferrer"&gt;See current&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; pricing for the full plan table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving between them (it's not all-or-nothing)
&lt;/h2&gt;

&lt;p&gt;Both platforms serve the same artifact — a folder of built static files — so migration in either direction is mostly a redirect. The build command and output directory stay the same; only the deployment target changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From Vercel to &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting&lt;/strong&gt;: connect the repository in &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, point Static Hosting at the same build directory Vercel used (&lt;code&gt;dist&lt;/code&gt;, &lt;code&gt;.next/out&lt;/code&gt;, &lt;code&gt;_site&lt;/code&gt;, etc.), keep the build command identical, and switch DNS once the new site is verified. If you depend on Vercel-specific features (image optimization, edge middleware, forms), plan their replacements first — Vercel image opt → Cloudflare image transformations or a third-party image CDN; Vercel forms → a third-party form service; edge middleware → Cloudflare Workers in front of Static Hosting. For the broader move from various deployment platforms, our &lt;a href="https://dev.to/deployhq/migrating-to-deployhq-a-smooth-transition-guide-151g"&gt;migrating to&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; smooth transition guide covers the general pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting to Vercel&lt;/strong&gt;: even simpler — connect the repo to Vercel, set the same build command and output directory, switch DNS. No DeployHQ-specific features to unwind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Or: mix them.&lt;/strong&gt; &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; supports bring-your-own deployment targets alongside its managed hosting. You can keep a Vercel-hosted Next.js app and deploy your Laravel API or your Rails backend with &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; to a VPS in the same project. There's no requirement to consolidate the static side.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about the Cloudflare edge?
&lt;/h2&gt;

&lt;p&gt;Both &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting and Cloudflare Pages run on Cloudflare's global edge network. Vercel runs on Vercel's own edge network, built on AWS regions plus their own POPs. In practice the latency profile is comparable for most users — within tens of milliseconds depending on geography — but the underlying networks are different. Cloudflare's network is broader (305+ POPs, last published count); Vercel's is narrower but tuned tightly for Next.js workloads.&lt;/p&gt;

&lt;p&gt;For pure static content this rarely matters. For edge-function workloads with strict latency SLAs, run your own benchmark from the regions your users actually live in.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I use a custom domain with &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting?&lt;/strong&gt;Yes. Add a CNAME record at your DNS provider pointing to your &lt;code&gt;&amp;lt;subdomain&amp;gt;.deployhq-sites.com&lt;/code&gt; host. HTTPS certificates are provisioned automatically by Cloudflare. Custom domains are supported on all tiers including trial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting support Next.js?&lt;/strong&gt;Yes, for statically-exported Next.js builds (&lt;code&gt;next export&lt;/code&gt;, or App Router pages with &lt;code&gt;output: 'export'&lt;/code&gt;). If your app uses SSR, ISR, server components with dynamic rendering, or edge middleware, Static Hosting isn't the right surface — those need a runtime. Use &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS Hosting and run a Node process there, or stay on Vercel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I get deploy previews per pull request?&lt;/strong&gt; &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; projects support multiple servers (and therefore multiple environments) inside one project, so you can have a staging Static Hosting site that deploys from a &lt;code&gt;staging&lt;/code&gt; branch. Per-PR preview links with inline comments are a Vercel-specific feature — &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; doesn't replicate that exact UX.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is there a free tier on &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting?&lt;/strong&gt;Trial accounts can provision one Static Hosting site at no charge. Paid plans support additional sites. The trial is what you'd use to evaluate the product before committing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if I outgrow Static Hosting?&lt;/strong&gt; You can move to BYO hosting (deploy to your own S3 bucket, Cloudflare R2, or any S3-compatible storage) in the same &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project, or move to Managed VPS if your site picked up a backend. Same pipeline, just a different target. If you're outgrowing the static-only model entirely and need a runtime, our &lt;a href="https://dev.to/deployhq/shared-hosting-vs-vps-a-comprehensive-guide-for-junior-developers-g8c"&gt;shared hosting vs VPS comprehensive guide for junior developers&lt;/a&gt; walks through the broader infrastructure trade-offs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;

&lt;p&gt;If you're already on &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; and want to add a static site to an existing project, enable beta features under &lt;strong&gt;Settings &amp;gt; Beta Features&lt;/strong&gt; , then add a new Static Hosting server to your project. The &lt;a href="https://dev.to/deployhq/static-hosting-on-deployhq-deploy-and-host-jamstack-sites-from-one-dashboard-40hi-temp-slug-2214878"&gt;Static Hosting on&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; pillar guide walks through the provisioning flow end-to-end.&lt;/p&gt;

&lt;p&gt;If you're new, &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;start a free trial&lt;/a&gt; and the trial includes one Static Hosting site so you can compare the workflow against your current Vercel setup before deciding. For the broader picture of where Static Hosting fits among the five hosting types &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; supports, see &lt;a href="https://www.deployhq.com/blog/deployhq-your-universal-deployment-platform-for-all-hosting-types" rel="noopener noreferrer"&gt;your universal deployment and hosting platform&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Both &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting and Vercel are good at what they're built for. Pick based on what's actually around your static site — if it's all static or you're consolidating workflows for a backend you also ship, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting fits. If you're deep in Next.js SSR territory, stay on Vercel and don't fix what isn't broken.&lt;/p&gt;

&lt;p&gt;For more detail on the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Static Hosting product itself, the &lt;a href="https://www.deployhq.com/support/servers/static-hosting" rel="noopener noreferrer"&gt;Static Hosting support library&lt;/a&gt; has the full provisioning, framework-detection, and lifecycle documentation. And the &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;build pipeline feature page&lt;/a&gt; covers what runs on the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; side before the upload step fires.&lt;/p&gt;




&lt;p&gt;Questions or feedback on Static Hosting vs Vercel? Email &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or follow &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;@deployhq&lt;/a&gt; on X for product updates.&lt;/p&gt;

</description>
      <category>vercel</category>
      <category>frontend</category>
      <category>tutorials</category>
      <category>statichosting</category>
    </item>
    <item>
      <title>Tie every Sentry error to the DeployHQ deploy that caused it</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Tue, 02 Jun 2026 06:41:16 +0000</pubDate>
      <link>https://dev.to/deployhq/tie-every-sentry-error-to-the-deployhq-deploy-that-caused-it-hfm</link>
      <guid>https://dev.to/deployhq/tie-every-sentry-error-to-the-deployhq-deploy-that-caused-it-hfm</guid>
      <description>&lt;h2&gt;
  
  
  The 2 AM problem: errors without deploy context
&lt;/h2&gt;

&lt;p&gt;It's 2 AM. Sentry pages you — your production error rate just spiked 4x. You roll out of bed, open the Sentry dashboard, and stare at a wall of new issues. The team shipped five deploys today.&lt;/p&gt;

&lt;p&gt;Which one broke it?&lt;/p&gt;

&lt;p&gt;You open &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; in another tab. You squint at timestamps. You cross-reference commit SHAs against the stack traces. Maybe you guess. Maybe you roll back the wrong release. The next morning, the team has to reconstruct what actually changed — and the on-call has a coffee, not a story.&lt;/p&gt;

&lt;p&gt;This isn't a Sentry problem or a &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; problem. Each tool is excellent in isolation. The gap is the missing join: which deploy introduced this error?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; closes that gap. Every shipped deploy gets posted to Sentry as a release with its commit SHA, environment, and timestamp. From then on, Sentry doesn't just tell you something broke — it tells you what shipped, when, and by whom. Issues get attributed to the release that was live. Regressions surface against the release that caused them. &lt;a href="https://www.deployhq.com/features/one-click-rollback" rel="noopener noreferrer"&gt;One-click rollback in DeployHQ&lt;/a&gt; becomes the obvious next move instead of a guess.&lt;/p&gt;

&lt;p&gt;The cost of &lt;em&gt;not&lt;/em&gt; wiring this up compounds. Longer mean time to recovery. Blame games. And the cultural drift where deploys quietly become scary events that get pushed to Tuesday afternoon &lt;q&gt;to be safe.&lt;/q&gt; If you've ever stalled a release on a Friday because the team was too tired to handle a regression, you've felt this gap. For the broader principle, see our take on &lt;a href="https://dev.to/deployhq/monitoring-your-deployments-with-deployhq-ensuring-smooth-releases-3954"&gt;what you should be monitoring on every deployment&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The deploy → release link in plain terms
&lt;/h2&gt;

&lt;p&gt;Two views of the same event, one shared key.&lt;/p&gt;

&lt;p&gt;In Sentry, a &lt;strong&gt;release&lt;/strong&gt; is a version of your code deployed to an environment, tagged with a commit SHA. When issues come in, Sentry attributes each one to the release that was live when it occurred. New issues that first appear right after a release are the ones you care about most — they're regressions, not background noise.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, a &lt;strong&gt;deploy&lt;/strong&gt; is the act of building that commit and pushing it to a specific environment. Each deploy has a SHA, a target environment (&lt;code&gt;production&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;), and a timestamp.&lt;/p&gt;

&lt;p&gt;The join key is the &lt;strong&gt;commit SHA&lt;/strong&gt;. Not timestamps — clock drift between systems will burn you. Not deploy IDs — those are internal to each tool. Just the SHA. If both systems know the SHA of the code that's running, they can answer &lt;q&gt;which deploy introduced this error?&lt;/q&gt; without anyone guessing.&lt;/p&gt;

&lt;p&gt;This matters more once you're shipping continuously. With a release a week, you can hold the last five SHAs in your head. With five a day, you can't — and you shouldn't have to. If that part of the picture is fuzzy, our primer on &lt;a href="https://www.deployhq.com/blog/what-is-continuous-deployment" rel="noopener noreferrer"&gt;continuous deployment workflows&lt;/a&gt; walks through the moving parts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting up the Sentry integration in DeployHQ
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; ships a native Sentry integration — no shell scripts, no &lt;code&gt;sentry-cli&lt;/code&gt; calls, no cron jobs.&lt;/p&gt;

&lt;p&gt;Open your &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project, click &lt;strong&gt;Integrations&lt;/strong&gt; in the left sidebar, then &lt;strong&gt;New Integration&lt;/strong&gt;. Select &lt;strong&gt;Sentry&lt;/strong&gt; from the service picker and fill in the configuration.&lt;/p&gt;

&lt;p&gt;You'll need four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Organization Slug&lt;/strong&gt; : the URL-safe identifier of your Sentry organization. Find it in the URL when you're signed into Sentry (&lt;code&gt;sentry.io/organizations/&amp;lt;your-slug&amp;gt;/&lt;/code&gt;) or under &lt;strong&gt;Settings → General&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project Name&lt;/strong&gt; : the project inside that organization where releases should be posted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment&lt;/strong&gt; : must exactly match the environment string your Sentry SDK reports — typically &lt;code&gt;production&lt;/code&gt; or &lt;code&gt;staging&lt;/code&gt;. Mismatches are the most common reason releases don't line up with issues, so double-check the spelling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth Token&lt;/strong&gt; : a Sentry auth token with the &lt;code&gt;project:releases&lt;/code&gt; scope. Create it in Sentry under &lt;strong&gt;Settings → Account → API → Auth Tokens&lt;/strong&gt; (or, for organization-level tokens, &lt;strong&gt;Settings → Auth Tokens&lt;/strong&gt; ). Per &lt;a href="https://docs.sentry.io/product/releases/" rel="noopener noreferrer"&gt;Sentry's current Releases documentation&lt;/a&gt;, &lt;code&gt;project:releases&lt;/code&gt; is the scope required to create releases for the project.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're running self-hosted Sentry, also fill in the &lt;strong&gt;Installation URL&lt;/strong&gt; — the base URL of your internal Sentry instance. Everything else is identical.&lt;/p&gt;

&lt;p&gt;Pick the events that should trigger the integration (a successful deployment is the common choice), select the servers you want it to fire for, and click &lt;strong&gt;Create Integration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The full setup steps live in our &lt;a href="https://www.deployhq.com/support/integrations/sentry" rel="noopener noreferrer"&gt;Sentry integration documentation&lt;/a&gt; if you need a reference while clicking through.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tracking releases after the integration is live
&lt;/h2&gt;

&lt;p&gt;Once configured, run a deployment to any server in your project. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; posts a new release event to your Sentry project, where it'll appear in the &lt;strong&gt;Releases&lt;/strong&gt; area for the matching environment.&lt;/p&gt;

&lt;p&gt;From that point forward, issues reported to the project gain release context: each event is attributed to the release that was live when it happened.&lt;/p&gt;

&lt;p&gt;The Sentry dashboard gains a few things that didn't exist before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Filter issues by release&lt;/strong&gt; so you can see only what regressed since the last shipment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-release timelines&lt;/strong&gt; showing issue counts against release events, so spikes line up visually with deploys&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regression detection&lt;/strong&gt; — Sentry surfaces issues that reappear in a new release after being marked resolved&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crash-free release tracking&lt;/strong&gt; for applications using Sentry's session tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a release goes wrong, you have everything you need to act: the offending commit SHA, the release that introduced it, and the engineer who shipped it. From there it's a one-click rollback in &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; — no SSH session, no rerun-from-master gymnastics, no &lt;a href="https://www.deployhq.com/features/zero-downtime-deployments" rel="noopener noreferrer"&gt;zero-downtime deployment&lt;/a&gt; reversed by hand.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Start tracking releases against Sentry issues today&lt;/strong&gt; — &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;sign up for DeployHQ&lt;/a&gt; and wire up the Sentry integration in under five minutes.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Which servers should trigger notifications?
&lt;/h2&gt;

&lt;p&gt;By default, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; fires the integration for every server in the project. Keep this on if your Sentry project tracks a single environment — usually &lt;code&gt;production&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two patterns worth knowing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One Sentry project per environment&lt;/strong&gt; (the cleanest setup). Add two &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; integrations — one mapped to your production servers with environment &lt;code&gt;production&lt;/code&gt;, another mapped to your staging servers with &lt;code&gt;staging&lt;/code&gt;. Each Sentry project then sees only its own releases, and you can alert independently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single Sentry project, multiple environments&lt;/strong&gt;. Use one integration but set up server-group filtering so only deploys to specific server groups fire the notifier. Then rely on Sentry's environment filter to separate releases inside the &lt;strong&gt;Releases&lt;/strong&gt; view.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A common gotcha: if your Sentry SDK reports &lt;code&gt;environment&lt;/code&gt; as &lt;code&gt;production&lt;/code&gt; but the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; integration sends &lt;code&gt;prod&lt;/code&gt; (or &lt;code&gt;live&lt;/code&gt;, or &lt;code&gt;prd&lt;/code&gt;), Sentry treats those as separate environments and silently drops the release attribution. Pick one canonical spelling and use it everywhere — the SDK config, the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; integration, your environment variables.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this unlocks beyond MTTR
&lt;/h2&gt;

&lt;p&gt;Faster incident response is the headline win, but tying releases to issues changes day-to-day work in subtler ways too.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Change failure rate&lt;/strong&gt; becomes measurable. One of the four DORA metrics — change failure rate — is the percentage of deploys causing degraded service or requiring remediation. Without release tagging, you're estimating. With Sentry releases + &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; deploys, you have a clean ratio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploys stop being scary&lt;/strong&gt;. Teams that can pinpoint the failing release in seconds ship more often, not less. Friday afternoons stop being off-limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-incident reviews get shorter&lt;/strong&gt;. Instead of debating &lt;q&gt;did this start before or after the 14:32 deploy?&lt;/q&gt;, the timeline is the answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-author attribution&lt;/strong&gt; falls out of the SHA join. Sentry knows the release; &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; knows who triggered it. Not for blame — for routing the regression to the engineer with full context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crash-free release health&lt;/strong&gt;. Once releases are reported, Sentry's release-health features (adoption tracking, crash-free percentages, regression surfacing) light up for applications using its session tracking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your stack uses a different error tracker, the same pattern applies — we shipped &lt;a href="https://www.deployhq.com/blog/track-deployments-in-a-bugsnag-project" rel="noopener noreferrer"&gt;Bugsnag deploy tracking with DeployHQ&lt;/a&gt;, &lt;a href="https://dev.to/deployhq/tie-every-honeybadger-error-to-the-deployhq-deploy-that-caused-it-5f8n-temp-slug-1730876"&gt;Honeybadger deploy tracking with DeployHQ&lt;/a&gt;, and &lt;a href="https://www.deployhq.com/blog/rollbar-notifications" rel="noopener noreferrer"&gt;Rollbar deploy tracking with DeployHQ&lt;/a&gt; using identical mechanics. The point isn't which error tracker you use — it's that errors without deploy context cost more than they should.&lt;/p&gt;

&lt;p&gt;For teams that want every deploy log analyzed automatically (not just notified), &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; also offers &lt;a href="https://www.deployhq.com/blog/introducing-deployhq-log-intelligence-your-ai-powered-deployment-log-analyzer" rel="noopener noreferrer"&gt;AI-powered deployment log analysis&lt;/a&gt; — so the next-step decision after a Sentry spike isn't &lt;q&gt;scroll through 3,000 lines of build output&lt;/q&gt; but &lt;q&gt;read the summary.&lt;/q&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;You'll now be able to report every deployment to Sentry and tie issues back to the release that caused them — without writing a single line of integration code. Two minutes of setup; permanent context on every error.&lt;/p&gt;

&lt;p&gt;The Sentry integration has been part of &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; since 2019, when we shipped it alongside our other &lt;a href="https://www.deployhq.com/blog/deployhq-integrations" rel="noopener noreferrer"&gt;error-tracking integrations&lt;/a&gt;, and has been quietly working in customer projects ever since. Per &lt;a href="https://docs.sentry.io/product/releases/" rel="noopener noreferrer"&gt;Sentry's current Releases documentation&lt;/a&gt;, the dashboard features that benefit from this — filtering by release, regression detection, per-release timelines, crash-free tracking — only light up once releases are being reported, which is exactly what this integration does for you.&lt;/p&gt;

&lt;p&gt;If you have any questions about the Sentry integration or any other aspect of &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, please don't hesitate to email &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;@deployhq&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>sentry</category>
      <category>tipstricks</category>
      <category>tutorials</category>
    </item>
    <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>
  </channel>
</rss>
