<?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>DeployHQ Managed VPS vs Render: Real Linux vs PaaS Sandbox</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Mon, 13 Jul 2026 08:49:34 +0000</pubDate>
      <link>https://dev.to/deployhq/deployhq-managed-vps-vs-render-real-linux-vs-paas-sandbox-487e</link>
      <guid>https://dev.to/deployhq/deployhq-managed-vps-vs-render-real-linux-vs-paas-sandbox-487e</guid>
      <description>&lt;p&gt;&lt;a href="https://www.deployhq.com/hosting/managed-vps" rel="noopener noreferrer"&gt;DeployHQ Managed VPS&lt;/a&gt; and Render solve the same problem — get an app from a Git repository to a running production service — but they take opposite architectural bets. Render is fully managed PaaS: containers, autoscaling, managed databases, zero infrastructure decisions. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS is a real Linux box with full SSH access plus the deployment pipeline wired in.&lt;/p&gt;

&lt;p&gt;The right choice depends on whether your workload needs real Linux (custom services, SSH access, anything outside a PaaS sandbox) or whether the PaaS abstraction's constraints are actually freedoms — fewer decisions to make, less operational surface to maintain.&lt;/p&gt;

&lt;p&gt;This guide is a head-to-head comparison: features, pricing, lock-in, and when each one wins. (For broader Heroku-replacement framing — Render alongside Railway and Fly.io — our &lt;a href="https://dev.to/deployhq/heroku-alternatives-in-2026-deployhq-your-vps-vs-render-vs-railway-vs-flyio-347b-temp-slug-7668051"&gt;Heroku alternatives in 2026 roundup&lt;/a&gt; covers the wider migration story.)&lt;/p&gt;

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

&lt;p&gt;If your application fits a PaaS shape — stateless web service, managed Postgres, autoscaling on CPU thresholds, no need to SSH into the box — Render is the cleaner answer. The PaaS model removes a meaningful amount of operational work, and Render's product surface is mature and well-designed.&lt;/p&gt;

&lt;p&gt;If you need a real Linux box — for SSH access, custom services, cron jobs, third-party agents (monitoring, log shipping, backups), or any workload that needs to step outside a sandbox — &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS is the right product. You get a real Ubuntu VPS plus the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; deployment pipeline already wrapped around it.&lt;/p&gt;

&lt;p&gt;For broader context, 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; places Render, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, and the rest of the category side by side.&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 Managed VPS&lt;/th&gt;
&lt;th&gt;Render&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Architectural model&lt;/td&gt;
&lt;td&gt;Linux VPS + deployment pipeline&lt;/td&gt;
&lt;td&gt;PaaS containers + managed services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Root SSH access&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (PaaS sandbox)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom services / cron / daemons&lt;/td&gt;
&lt;td&gt;Yes (it's a real Linux box)&lt;/td&gt;
&lt;td&gt;Cron jobs supported; custom binaries limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Underlying infrastructure&lt;/td&gt;
&lt;td&gt;DigitalOcean&lt;/td&gt;
&lt;td&gt;AWS / GCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server sizes&lt;/td&gt;
&lt;td&gt;1-2 vCPU / 1-4 GB (beta)&lt;/td&gt;
&lt;td&gt;Container instances (Starter, Standard, Pro tiers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autoscaling&lt;/td&gt;
&lt;td&gt;No (manual sizing)&lt;/td&gt;
&lt;td&gt;Yes (CPU / memory thresholds)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed databases bundled&lt;/td&gt;
&lt;td&gt;No (run your own or use external)&lt;/td&gt;
&lt;td&gt;Yes (managed Postgres, Redis)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build pipeline&lt;/td&gt;
&lt;td&gt;DeployHQ generic pipeline&lt;/td&gt;
&lt;td&gt;Render native build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy other targets in same project&lt;/td&gt;
&lt;td&gt;Yes (VPS, static, shared, S3)&lt;/td&gt;
&lt;td&gt;No (Render-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One bill with deployment tool&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (Render bundles both)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Beta status&lt;/td&gt;
&lt;td&gt;Yes (mid-2026)&lt;/td&gt;
&lt;td&gt;GA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vendor lock-in&lt;/td&gt;
&lt;td&gt;Low (it's just Linux + SSH)&lt;/td&gt;
&lt;td&gt;Medium-high (PaaS runtime + service bindings)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Real Linux on one side. PaaS sandbox on the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  When DeployHQ Managed VPS is the right choice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You need SSH access to a real Linux box.&lt;/strong&gt; This is the structural differentiator and it's where most &lt;q&gt;I can't use Render for this&lt;/q&gt; decisions land. If your app needs to install system packages, run a background worker outside Render's supported types, set up a custom cron schedule with unusual semantics, run a third-party monitoring or log-shipping agent, or debug live with &lt;code&gt;strace&lt;/code&gt; / &lt;code&gt;tcpdump&lt;/code&gt; / &lt;code&gt;journalctl&lt;/code&gt;, PaaS locks you out. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS gives you root over SSH and you do whatever Linux lets you do. The &lt;a href="https://dev.to/deployhq/managed-vps-hosting-on-deployhq-linux-servers-with-built-in-deployments-3d7o-temp-slug-8068925"&gt;Managed VPS pillar guide&lt;/a&gt; walks through what you get out of the box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You want a generic deployment pipeline that ships to many targets.&lt;/strong&gt; Render's build-and-deploy is specific to Render — it builds, runs, and serves your code on Render. DeployHQ's pipeline ships any artifact to any target — Managed VPS, your own Hetzner box, &lt;a href="https://www.deployhq.com/guides/digitalocean" rel="noopener noreferrer"&gt;DigitalOcean Droplets&lt;/a&gt;, shared hosting, S3 buckets, &lt;a href="https://www.deployhq.com/hosting/static" rel="noopener noreferrer"&gt;DeployHQ Static Hosting&lt;/a&gt;. If your project has any non-PaaS-shaped component (a worker that needs root, a static frontend you want on a separate edge, a service that has to live on shared hosting because compliance), &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; handles all of it. Render handles the Render-shaped part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You don't want PaaS lock-in on your runtime.&lt;/strong&gt; Render's value-adds — managed Postgres bound to the app, autoscaling on dedicated tiers, the Render runtime conventions — are tightly coupled to staying on Render. If you outgrow Render's constraints or pricing, migration means re-architecting around a different runtime. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS runs stock Linux — your app already works anywhere Linux runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You want to mix targets in one project.&lt;/strong&gt; A real example: production web on Managed VPS, background jobs on a separate Managed VPS or BYO VPS, static frontend on Static Hosting, all in one &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project with one billing relationship. Render can't host the static frontend at scale (Render Static Sites exists but lives in the Render ecosystem) and can't deploy to non-Render targets. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; handles all of it from one project.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;You don't want to manage Linux at all.&lt;/strong&gt; Render is genuine PaaS — no OS to patch, no security updates to apply, no system packages to manage. If &lt;q&gt;I don't want to think about the box, I just want to push code&lt;/q&gt; describes your team, Render delivers that cleanly. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS is &lt;em&gt;less&lt;/em&gt; operational work than a raw VPS, but it's still a Linux box; if you'd rather not have that surface, Render's abstraction is what you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need autoscaling.&lt;/strong&gt; Render's dedicated tiers offer autoscaling on CPU and memory thresholds. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS is manually sized — pick a tier, resize as needed. If your workload spikes unpredictably and you want the platform to absorb it without intervention, Render is the right answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You want managed databases bundled in.&lt;/strong&gt; Render offers managed Postgres and Redis with private networking, automatic backups, point-in-time recovery, and bindings into your app. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS doesn't bundle a database — you'd run Postgres yourself on the VPS (fine for many workloads), use a separate managed service (Neon, Supabase, DigitalOcean Managed Databases), or self-host in a separate VPS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your workload genuinely fits the PaaS shape.&lt;/strong&gt; Stateless web service, reads from a managed database, writes to object storage — the canonical PaaS shape. If that describes your app and you don't need to step outside it, Render's constraints aren't really constraints. The simplicity is a feature.&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;Render&lt;/strong&gt; offers a Free tier (with limitations: services sleep after idle, build minute caps, single concurrent build), then paid web service tiers starting from a Starter price point and scaling up to dedicated instances with autoscaling. Managed Postgres starts free with row limits and scales up with paid tiers offering increased storage and high availability. The current exact tier pricing is best checked directly on render.com — Render adjusts pricing periodically and a stale number here is worse than no number.&lt;/p&gt;

&lt;p&gt;What's worth noting structurally: Render's pricing scales with usage in a few dimensions — instance size, instance count (for autoscaling), database tier, bandwidth, and additional service slots. For a single small web service plus a small database, Render is competitive with other PaaS options. For larger workloads with several services and a HA database, the bill can scale quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DeployHQ Managed VPS&lt;/strong&gt; charges a fixed monthly rate per server, shown on the provisioning form in your account's currency. Server sizes are currently 1 vCPU / 1 GB up to 2 vCPU / 4 GB in the beta. 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 Managed VPS at the smallest size at no charge.&lt;/p&gt;

&lt;p&gt;The honest framing: a single small Render web service plus a small managed DB can be cheaper than &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS plus a separately hosted Postgres if you only have one app. The math tilts toward &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; when you have multiple deployment targets (one project deploying to multiple servers) or you're already paying for &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; to deploy code elsewhere. &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; pricing for the full breakdown.&lt;/p&gt;

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

&lt;p&gt;Different architectural models means migration involves real work in either direction — not just DNS swaps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From Render to &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS&lt;/strong&gt;: provision the VPS in &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, install your app's runtime (Node, Ruby, Python, Go — whatever Render was running), set up your service supervisor (systemd, PM2, Passenger), migrate your managed Postgres data to either a self-hosted Postgres on the same VPS or a separate managed-DB provider, and configure DeployHQ's pipeline to handle build + deploy. If Render was handling autoscaling for you, you'll either manually size the VPS for peak load or move to a different scaling model (load balancer + multiple VPS instances). The &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; guide covers the broader migration 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; Managed VPS to Render&lt;/strong&gt;: re-architect around Render's runtime conventions. Define your &lt;code&gt;render.yaml&lt;/code&gt; service spec, set up managed-DB bindings, adapt any custom services to Render-supported types. If your app used SSH-only operations (cron jobs with unusual semantics, custom binary execution, monitoring agents), find PaaS-compatible alternatives. This is harder in practice than the other direction because you're moving toward a more constrained runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Or: combine them.&lt;/strong&gt; Run web tier on Render for autoscaling and managed DB, run worker processes or background jobs on a &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS where you can install whatever you need, and keep both in one &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project for unified deployment of the worker side. This combines the autoscaling story (Render) with the real-Linux story (DeployHQ Managed VPS) — both architectures get used for what they're good at.&lt;/p&gt;

&lt;p&gt;If you're stepping up from shared hosting and weighing modern deployment options generally, &lt;a href="https://dev.to/deployhq/5-signs-its-time-to-upgrade-from-shared-hosting-to-automated-deployments-2e8o"&gt;5 signs it's time to upgrade from shared hosting to automated deployments&lt;/a&gt; covers the inflection points.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;q&gt;real Linux vs PaaS&lt;/q&gt; trade-off
&lt;/h2&gt;

&lt;p&gt;Both products solve the deployment problem. They solve it for different application shapes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Render&lt;/strong&gt; is excellent for canonical PaaS workloads — stateless web services, standard runtimes, managed databases. It removes operational work in exchange for runtime constraints. Most CRUD apps fit this shape comfortably.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DeployHQ Managed VPS&lt;/strong&gt; is excellent when the PaaS shape doesn't fit — when you need root access, custom services, third-party agents, or any workload that needs to step outside a sandbox. It's a real Linux box with deployment automation wrapped around it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest answer for many teams is &lt;q&gt;both.&lt;/q&gt; Run the canonical web tier on Render for the autoscaling and managed-DB ergonomics. Run the worker/cron/agent side on a Managed VPS where you can do whatever Linux lets you do. Use &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; as the unifying deployment surface for whatever you put on the VPS side. The application splits along architectural lines instead of along vendor lines.&lt;/p&gt;

&lt;p&gt;For &lt;a href="https://www.deployhq.com/guides/express" rel="noopener noreferrer"&gt;Express / Node service deployments&lt;/a&gt; specifically, both platforms handle Node well — the choice comes down to whether you want the runtime managed for you or you want SSH into the box.&lt;/p&gt;

&lt;p&gt;For the wider catalog of where Managed VPS fits with other &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; hosting options, the &lt;a href="https://www.deployhq.com/hosting" rel="noopener noreferrer"&gt;hosting hub&lt;/a&gt; covers all five hosting types.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Does &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS allow SSH access like a regular VPS?&lt;/strong&gt;Yes. It's a real Linux VPS. SSH in with the key &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; generates (or your own key), install what you want, run any service. DeployHQ's deployment pipeline handles the build-and-deploy automation but doesn't restrict what runs on the box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Render allow SSH access?&lt;/strong&gt; No. Render is PaaS — you interact with the platform through its dashboard, CLI, and API. SSH into a running container isn't supported. This is by design — it's what makes the PaaS abstraction work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I run a &lt;a href="https://www.deployhq.com/guides/laravel" rel="noopener noreferrer"&gt;Laravel deployment&lt;/a&gt; on &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS?&lt;/strong&gt;Yes. PHP-FPM, Nginx, Composer, queue workers via Supervisor, Laravel scheduler via cron — all standard Linux setup. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; handles the build (Composer install, migrations, asset compilation), the VPS handles the runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about managed Postgres on &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS?&lt;/strong&gt;Not bundled. For small workloads, install Postgres on the VPS itself. For HA / point-in-time recovery / managed backups, point at a separate managed-DB service. DigitalOcean Managed Databases pairs well since the VPS is already on DO.&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; Managed VPS?&lt;/strong&gt;Trial accounts can provision one Managed VPS at the smallest size at no charge. Custom SSH keys aren't available on trial; &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; generates and uses a managed key automatically. Render has a free web service tier (with idle-sleep limitations) and free DB tier — both are useful for evaluation but typically not for production.&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 Managed VPS server to any project. The &lt;a href="https://www.deployhq.com/features/one-click-rollback" rel="noopener noreferrer"&gt;one-click rollback feature&lt;/a&gt; works on Managed VPS the same way it works on a BYO server.&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 Managed VPS lets you ship a real project end-to-end before committing. For the wider picture of how Managed VPS fits with the other hosting options &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;Render and &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS are both good products in their lanes. Render wins when your workload fits PaaS. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; Managed VPS wins when it doesn't — or when you want real Linux for any reason. The split is structural, not feature-level; pick the architecture, not the feature list.&lt;/p&gt;

&lt;p&gt;For more detail on the Managed VPS product, the &lt;a href="https://www.deployhq.com/support/servers/managed-vps-hosting" rel="noopener noreferrer"&gt;Managed VPS support library&lt;/a&gt; has the full provisioning, lifecycle, and management documentation.&lt;/p&gt;




&lt;p&gt;Questions or feedback on Managed VPS vs Render? 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>tutorials</category>
      <category>vps</category>
    </item>
    <item>
      <title>Using Claude Code with SQLite: The Safest Way to Prototype Queries Locally</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Mon, 13 Jul 2026 08:49:24 +0000</pubDate>
      <link>https://dev.to/deployhq/using-claude-code-with-sqlite-the-safest-way-to-prototype-queries-locally-5ap3</link>
      <guid>https://dev.to/deployhq/using-claude-code-with-sqlite-the-safest-way-to-prototype-queries-locally-5ap3</guid>
      <description>&lt;p&gt;Most guides about connecting an AI coding assistant to a database start with the hard case: a production Postgres cluster, credentials, network access, read-only roles. That is exactly backwards. If you are just getting comfortable with letting an assistant explore a schema and draft SQL for you, the smartest place to start is the database that lives entirely inside a single file on your laptop.&lt;/p&gt;

&lt;p&gt;SQLite is that database. There is no server to run, no port to open, no user to grant. The whole thing is one &lt;code&gt;.sqlite&lt;/code&gt; file you can open, copy, delete, and restore in seconds. That makes it the ideal sandbox for AI-assisted database work — a place where a bad query or a botched schema change costs you nothing, because rolling back is as simple as copying a file back over another one.&lt;/p&gt;

&lt;p&gt;This post walks through pointing &lt;a href="https://dev.to/deployhq/getting-started-with-claude-code-the-ai-coding-assistant-for-your-terminal-4cba"&gt;Claude Code&lt;/a&gt; at a local SQLite file, why a file-based database is the safest experimentation surface you can give an AI assistant, and how to prototype real queries before promoting anything to a server.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why SQLite is the right place to start
&lt;/h2&gt;

&lt;p&gt;Every other database engine assumes infrastructure. PostgreSQL and MySQL run as long-lived server processes, listen on a network port, and gate access behind users and roles. That is the correct model for production — but it is a lot of moving parts to stand up just to experiment with an AI assistant.&lt;/p&gt;

&lt;p&gt;SQLite removes all of it. A SQLite database is a single ordinary file. Your application (or your assistant's tooling) opens the file directly and reads and writes rows in place. There is no daemon, no &lt;code&gt;localhost:5432&lt;/code&gt;, no authentication handshake. If you have ever seen a &lt;code&gt;.db&lt;/code&gt; or &lt;code&gt;.sqlite&lt;/code&gt; file in a project, you have already been using it.&lt;/p&gt;

&lt;p&gt;For AI-assisted work, three properties matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is trivially reproducible.&lt;/strong&gt; Delete the file, run your seed script, and you are back to a known state. No &lt;code&gt;DROP DATABASE&lt;/code&gt;, no re-provisioning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is completely local.&lt;/strong&gt; Nothing leaves your machine. There is no risk of an assistant accidentally touching shared staging data because there is no shared anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is disposable by design.&lt;/strong&gt; A SQLite file is meant to be copied, moved, and thrown away. That disposability is what turns it into a safe playground.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want the deeper trade-offs between the engines, the &lt;a href="https://dev.to/deployhq/sqlite-vs-postgresql-vs-mysql-choosing-the-right-database-4i14"&gt;SQLite vs PostgreSQL vs MySQL comparison&lt;/a&gt; covers when each one earns its place. For learning the AI workflow, SQLite wins on setup cost alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pointing Claude Code at a &lt;code&gt;.sqlite&lt;/code&gt; file
&lt;/h2&gt;

&lt;p&gt;Claude Code talks to databases through an MCP server — a small bridge process that exposes your database to the assistant as a set of tools. If the term is new, the &lt;a href="https://www.deployhq.com/blog/build-your-first-mcp-server-model-context-protocol-guide" rel="noopener noreferrer"&gt;complete guide to MCP servers&lt;/a&gt; explains what they are and why they matter. The one we use here is DBHub, and the full multi-engine setup lives in the &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;step-by-step guide to querying databases with Claude Code and DBHub&lt;/a&gt; — this post won't re-run that whole setup, just the SQLite-specific piece.&lt;/p&gt;

&lt;p&gt;DBHub supports SQLite directly, and the SQLite DSN is refreshingly short. Where Postgres needs a host, port, user, password, and database name, SQLite needs a path to a file:&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="n"&gt;sqlite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;///&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;mydb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqlite&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;That is the entire connection string. The &lt;code&gt;sqlite://&lt;/code&gt; scheme followed by a path — three slashes for a relative path, four for an absolute one like &lt;code&gt;sqlite:////Users/you/data/mydb.sqlite&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In your Claude Code MCP configuration, DBHub takes that DSN as its &lt;code&gt;--dsn&lt;/code&gt; argument:&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;"mcpServers"&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;"dbhub"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"@bytebase/dbhub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--transport"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stdio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&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;"sqlite:///./mydb.sqlite"&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;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;Restart Claude Code, and it can now see your SQLite file's tables and columns. If you don't have a database to point at yet, DBHub also ships a demo mode that spins up an in-memory SQLite database, so you can kick the tires before pointing it at anything of your own. Keeping the exact DBHub flags handy is easier with the &lt;a href="https://www.deployhq.com/cheatsheets/claude-code" rel="noopener noreferrer"&gt;Claude Code cheatsheet&lt;/a&gt; open in another tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a file is the safest sandbox you can give an AI
&lt;/h2&gt;

&lt;p&gt;Here is the property that makes SQLite genuinely different for AI-assisted work, not just easier to set up: your entire database is one file, so a complete backup is one copy command.&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;cp &lt;/span&gt;mydb.sqlite mydb.backup.sqlite

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

&lt;/div&gt;



&lt;p&gt;That single line is your rollback plan. Before you let the assistant run anything destructive — an &lt;code&gt;ALTER TABLE&lt;/code&gt;, a bulk &lt;code&gt;UPDATE&lt;/code&gt;, a schema rewrite it suggested — you copy the file. If the change goes sideways, you restore it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp &lt;/span&gt;mydb.backup.sqlite mydb.sqlite

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

&lt;/div&gt;



&lt;p&gt;You are back exactly where you started, byte for byte. There is no migration to reverse, no &lt;code&gt;pg_restore&lt;/code&gt;, no &lt;q&gt;hope the transaction rolled back cleanly.&lt;/q&gt; The safety net is the filesystem itself.&lt;/p&gt;

&lt;p&gt;This changes how you can work with an assistant. On a shared server you approach every write cautiously, because a mistake affects other people and is annoying to undo. Against a copied SQLite file, you can be reckless on purpose: let the assistant try an aggressive denormalization, ask it to rewrite five tables, tell it to backfill a column three different ways and compare. Every experiment starts from a fresh copy and ends with a &lt;code&gt;cp&lt;/code&gt; if it didn't pan out.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prototype fearlessly, then promote deliberately.&lt;/strong&gt; The whole point of a disposable local database is that &lt;q&gt;undo&lt;/q&gt; is free — so use it to move fast locally, and save the careful, reviewed process for the moment your changes touch a real server. &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Start free with DeployHQ&lt;/a&gt; when you're ready to put a proper deployment pipeline behind those changes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A good habit is one backup per experiment: copy the file, name the copy after what you're about to try (&lt;code&gt;mydb.pre-index-test.sqlite&lt;/code&gt;), and keep the ones that produced results worth revisiting. Disk is cheap; a lost afternoon of exploration isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exploring schema and prototyping queries
&lt;/h2&gt;

&lt;p&gt;With the file connected and a backup in your pocket, the workflow becomes conversational. Instead of memorizing table names, you ask.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start by mapping the territory.&lt;/strong&gt; Ask the assistant to describe the schema — tables, columns, types, foreign keys. Because DBHub exposes the structure as tools, Claude Code reads it directly rather than guessing, so what it tells you reflects the actual file, not a hallucinated model of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then prototype against real shape.&lt;/strong&gt; Describe what you want in plain English — &lt;q&gt;show me the ten customers with the most orders in the last quarter&lt;/q&gt; — and let the assistant draft the SQL. Run it, read the result, and refine. Because you're working on a copy, you can run the query, realize the join is wrong, and iterate without any consequence beyond a few seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test schema changes the same way.&lt;/strong&gt; Ask for an index, a new column, or a normalized table split, apply it to your working copy, and check whether queries behave the way you expected. If the change made things worse, restore the backup and try a different shape. This tight loop — propose, apply, inspect, keep-or-revert — is the core value of a disposable database.&lt;/p&gt;

&lt;p&gt;A few practices keep the exploration honest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read the SQL before you run it.&lt;/strong&gt; The assistant is drafting, not deciding. Skim every statement, especially anything that writes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Back up before writes, not before reads.&lt;/strong&gt; Selects can't hurt the file; &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, and &lt;code&gt;ALTER&lt;/code&gt; can.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seed deterministically.&lt;/strong&gt; Keep a small script that rebuilds your test data so &lt;q&gt;start over&lt;/q&gt; is always available.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The moment you point an assistant at a networked, multi-user database instead of a local file, that &lt;code&gt;cp&lt;/code&gt; safety net disappears — which is exactly when you want to lock the connection down. Our guide to &lt;a href="https://dev.to/deployhq/how-to-let-ai-query-your-database-without-deleting-it-read-only-guardrails-for-claude-code-1jhi-temp-slug-5799689"&gt;read-only guardrails for AI database access&lt;/a&gt; covers the dedicated-role and MCP-flag recipe that keeps a shared database as safe as a throwaway file.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to graduate from SQLite
&lt;/h2&gt;

&lt;p&gt;SQLite is a superb sandbox and a perfectly good production database for plenty of workloads — but there is a point where you outgrow it. Move to PostgreSQL or MySQL when you need real concurrency (many clients writing at once), network access from multiple application servers, or engine-specific features SQLite doesn't offer. The comparison linked earlier lays out exactly where each of those lines sits.&lt;/p&gt;

&lt;p&gt;The reassuring part: the workflow you just learned transfers cleanly. Pointing Claude Code at Postgres or &lt;a href="https://www.deployhq.com/blog/connect-claude-code-to-mysql-auth-plugins-sockets-gotchas" rel="noopener noreferrer"&gt;MySQL through DBHub&lt;/a&gt; is the same pattern, only the DSN changes from a file path to a host, port, and credentials (and MySQL adds a few connection quirks of its own). What you can't carry over is the &lt;code&gt;cp&lt;/code&gt;-based safety net — which is precisely why doing your schema and query experimentation on SQLite first is worth it. You work out the shape where mistakes are free, and only take the validated result to an engine where they aren't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving validated queries into a real deployment
&lt;/h2&gt;

&lt;p&gt;Once a schema change or a set of queries has proven itself against your local file, it needs a disciplined path to production — because on a live database, the free-rollback luxury is gone. That is where the process flips from &lt;q&gt;experiment freely&lt;/q&gt; to &lt;q&gt;change carefully.&lt;/q&gt;&lt;/p&gt;

&lt;p&gt;The safe pattern is to capture the validated change as a versioned migration and apply it as part of your deployment, not by hand. Our guide on &lt;a href="https://www.deployhq.com/blog/how-do-you-handle-database-changes-during-a-deployment" rel="noopener noreferrer"&gt;how to handle database changes during a deployment&lt;/a&gt; covers version-controlling schema changes and running them automatically, and the deeper &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; walks through applying those changes without taking the application offline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; fits at exactly this handoff. A &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;build pipeline&lt;/a&gt; can run your migration step as part of every deploy, so the schema change you prototyped locally on SQLite ships the same way your code does — reviewed, versioned, and repeatable — instead of someone SSHing in to run SQL by hand. The local file gave you a place to be fearless; the pipeline gives you a place to be careful.&lt;/p&gt;

&lt;p&gt;That is the arc of AI-assisted database work done well: experiment where undo is free, promote where it isn't, and never confuse the two. SQLite is where you start precisely because it makes the first half of that sentence effortless.&lt;/p&gt;




&lt;p&gt;Ready to put a proper deployment pipeline behind your validated schema changes? Explore &lt;a href="https://www.deployhq.com/features" rel="noopener noreferrer"&gt;automated deployment for your stack&lt;/a&gt; and run your migrations automatically on every deploy.&lt;/p&gt;

&lt;p&gt;Questions or feedback? Reach us at &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or 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>tutorials</category>
    </item>
    <item>
      <title>Running AI Coding Agents in CI/CD: Claude Code, Codex, and Gemini CLI in Headless Mode</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Fri, 10 Jul 2026 14:47:36 +0000</pubDate>
      <link>https://dev.to/deployhq/running-ai-coding-agents-in-cicd-claude-code-codex-and-gemini-cli-in-headless-mode-558d</link>
      <guid>https://dev.to/deployhq/running-ai-coding-agents-in-cicd-claude-code-codex-and-gemini-cli-in-headless-mode-558d</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 10 of our series on AI coding assistants for developers. See also: &lt;a href="https://dev.to/deployhq/agentic-workflows-explained-how-ai-agents-are-changing-cicd-pipelines-nm0-temp-slug-2291085"&gt;Agentic Workflows Explained&lt;/a&gt; and &lt;a href="https://www.deployhq.com/blog/comparing-ai-cli-coding-assistants-claude-code-vs-codex-vs-gemini-cli" rel="noopener noreferrer"&gt;Comparing AI CLI Coding Assistants&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;GitHub's Copilot Coding Agent is convenient — assign an issue and it opens a PR. But it's a black box. You don't choose the model, you can't add custom tools, and you're locked into GitHub's pricing and rate limits.&lt;/p&gt;

&lt;p&gt;There's another way: run the same AI coding CLIs you use locally — &lt;a href="https://dev.to/deployhq/getting-started-with-claude-code-the-ai-coding-assistant-for-your-terminal-4cba"&gt;Claude Code&lt;/a&gt;, &lt;a href="https://dev.to/deployhq/getting-started-with-openai-codex-cli-ai-powered-code-generation-from-your-terminal-5hm8"&gt;Codex CLI&lt;/a&gt;, and &lt;a href="https://dev.to/deployhq/getting-started-with-google-gemini-cli-open-source-ai-agent-for-your-terminal-25e1"&gt;Gemini CLI&lt;/a&gt; — as headless steps in your CI/CD pipeline. You control the prompt, the model, the tools, and the cost. The agent runs non-interactively, produces output, and the pipeline decides what to do with it.&lt;/p&gt;

&lt;p&gt;This is the &lt;q&gt;bring your own agent&lt;/q&gt; approach. It works on any CI platform that can run a shell command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Headless Mode for Each CLI
&lt;/h2&gt;

&lt;p&gt;Each tool has a way to run without interactive input — no terminal UI, no approval prompts, just input and output.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Print mode — runs a single prompt, outputs result, exits&lt;/span&gt;
claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Analyze the git diff and generate a changelog entry"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allowedTools&lt;/span&gt; Edit,Write,Bash &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output-format&lt;/span&gt; json

&lt;span class="c"&gt;# With stdin piping&lt;/span&gt;
git diff HEAD~1 | claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Generate a changelog entry for these changes"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output-format&lt;/span&gt; text

&lt;span class="c"&gt;# With max tokens to control cost&lt;/span&gt;
claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Fix the failing test in tests/auth.test.ts"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allowedTools&lt;/span&gt; Edit,Read,Bash &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-turns&lt;/span&gt; 10

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

&lt;/div&gt;



&lt;p&gt;Key flags: &lt;code&gt;-p&lt;/code&gt; (print mode, non-interactive), &lt;code&gt;--allowedTools&lt;/code&gt; (restrict what the agent can do), &lt;code&gt;--output-format&lt;/code&gt; (json or text), &lt;code&gt;--max-turns&lt;/code&gt; (limit iterations).&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Full auto mode — no human approval needed&lt;/span&gt;
codex &lt;span class="nt"&gt;--approval-mode&lt;/span&gt; full-auto &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"Update the README with the latest API changes"&lt;/span&gt;

&lt;span class="c"&gt;# With file restrictions&lt;/span&gt;
codex &lt;span class="nt"&gt;--approval-mode&lt;/span&gt; full-auto &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--full-auto-error-mode&lt;/span&gt; ask-user &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"Add input validation to the user registration endpoint"&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Key flags: &lt;code&gt;--approval-mode full-auto&lt;/code&gt; (no interactive prompts), &lt;code&gt;--quiet&lt;/code&gt; (minimal output).&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemini CLI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Non-interactive with prompt&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Generate unit tests for src/utils/validator.ts"&lt;/span&gt; | gemini

&lt;span class="c"&gt;# With sandbox mode&lt;/span&gt;
gemini &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"Review the last commit for security issues"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sandbox&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Practical Use Cases with Full CI Config
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Auto-Generate Changelog from Git Diff
&lt;/h3&gt;

&lt;p&gt;The agent reads the diff since the last release tag and writes a human-readable changelog entry:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate Changelog&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v*'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;changelog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&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 Claude Code&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;npm install -g @anthropic-ai/claude-code&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;Generate changelog entry&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ANTHROPIC_API_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;PREV_TAG=$(git describe --tags --abbrev=0 HEAD~1 2&amp;gt;/dev/null || echo "")&lt;/span&gt;
          &lt;span class="s"&gt;DIFF=$(git log ${PREV_TAG}..HEAD --oneline --no-merges)&lt;/span&gt;

          &lt;span class="s"&gt;echo "$DIFF" | claude -p "Generate a changelog entry in Keep a Changelog format. Group changes into Added, Changed, Fixed, Removed. Be concise — one line per change. Output only the changelog markdown, nothing else." \&lt;/span&gt;
            &lt;span class="s"&gt;--output-format text &amp;gt; CHANGELOG_ENTRY.md&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;Create release with changelog&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;gh release create ${{ github.ref_name }} \&lt;/span&gt;
            &lt;span class="s"&gt;--title "${{ github.ref_name }}" \&lt;/span&gt;
            &lt;span class="s"&gt;--notes-file CHANGELOG_ENTRY.md&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. AI-Powered Code Review on Changed Files
&lt;/h3&gt;

&lt;p&gt;Run the agent as a review step on every PR — it reads the diff and leaves comments:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AI Code Review&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ai-review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&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 Claude Code&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;npm install -g @anthropic-ai/claude-code&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;Run AI review&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ANTHROPIC_API_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;DIFF=$(gh pr diff ${{ github.event.pull_request.number }})&lt;/span&gt;

          &lt;span class="s"&gt;REVIEW=$(echo "$DIFF" | claude -p "Review this diff for:&lt;/span&gt;
          &lt;span class="s"&gt;1. Security issues (SQL injection, XSS, hardcoded secrets)&lt;/span&gt;
          &lt;span class="s"&gt;2. Missing error handling&lt;/span&gt;
          &lt;span class="s"&gt;3. Performance concerns (N+1 queries, unnecessary loops)&lt;/span&gt;
          &lt;span class="s"&gt;4. Missing input validation&lt;/span&gt;

          &lt;span class="s"&gt;Format as a markdown list. If no issues found, say 'No issues found.' Be specific — reference file names and line numbers." \&lt;/span&gt;
            &lt;span class="s"&gt;--output-format text \&lt;/span&gt;
            &lt;span class="s"&gt;--max-turns 3)&lt;/span&gt;

          &lt;span class="s"&gt;gh pr comment ${{ github.event.pull_request.number }} \&lt;/span&gt;
            &lt;span class="s"&gt;--body "## AI Code Review&lt;/span&gt;

          &lt;span class="s"&gt;$REVIEW&lt;/span&gt;

          &lt;span class="s"&gt;---&lt;/span&gt;
          &lt;span class="s"&gt;*Automated review by Claude Code. This is advisory — human review is still required.*"&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Once an agent produces a change that passes review, the last mile is shipping it. Wire your CI into an &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;automated build pipeline&lt;/a&gt; and a green run deploys to your own server automatically — the agent writes, CI validates, and the release goes out with no manual step in between.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Auto-Fix CI Failures
&lt;/h3&gt;

&lt;p&gt;When tests fail, the agent reads the error output and proposes a fix:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Tests with Auto-Fix&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&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="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.head_ref }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;

      &lt;span class="pi"&gt;-&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;npm ci&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;Run tests&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tests&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;npm test 2&amp;gt;&amp;amp;1 | tee test-output.txt&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;AI fix attempt&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.tests.outcome == 'failure'&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ANTHROPIC_API_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;npm install -g @anthropic-ai/claude-code&lt;/span&gt;

          &lt;span class="s"&gt;claude -p "The tests failed. Here's the output:&lt;/span&gt;

          &lt;span class="s"&gt;$(cat test-output.txt)&lt;/span&gt;

          &lt;span class="s"&gt;Read the failing test files and the source code they test.&lt;/span&gt;
          &lt;span class="s"&gt;Fix the source code to make the tests pass.&lt;/span&gt;
          &lt;span class="s"&gt;Do NOT modify the test files — fix the implementation." \&lt;/span&gt;
            &lt;span class="s"&gt;--allowedTools Read,Edit,Bash \&lt;/span&gt;
            &lt;span class="s"&gt;--max-turns 15&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;Verify fix&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.tests.outcome == 'failure'&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;npm test&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;Commit fix&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.tests.outcome == 'failure'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git config user.name "AI Agent"&lt;/span&gt;
          &lt;span class="s"&gt;git config user.email "ai-agent@deployhq.com"&lt;/span&gt;
          &lt;span class="s"&gt;git add -A&lt;/span&gt;
          &lt;span class="s"&gt;git diff --staged --quiet || git commit -m "fix: auto-fix failing tests (AI-generated)"&lt;/span&gt;
          &lt;span class="s"&gt;git push&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Security Scanning with AI Triage
&lt;/h3&gt;

&lt;p&gt;Run a SAST tool, then have the AI prioritize and explain findings:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Security Scan + AI Triage&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&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;Run Semgrep&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;pip install semgrep&lt;/span&gt;
          &lt;span class="s"&gt;semgrep --config auto --json --output semgrep-results.json . || true&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;AI triage&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ANTHROPIC_API_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;npm install -g @anthropic-ai/claude-code&lt;/span&gt;

          &lt;span class="s"&gt;TRIAGE=$(claude -p "Here are Semgrep security scan results:&lt;/span&gt;

          &lt;span class="s"&gt;$(cat semgrep-results.json)&lt;/span&gt;

          &lt;span class="s"&gt;Triage these findings:&lt;/span&gt;
          &lt;span class="s"&gt;1. Remove false positives (explain why)&lt;/span&gt;
          &lt;span class="s"&gt;2. Rank remaining issues by severity (Critical, High, Medium, Low)&lt;/span&gt;
          &lt;span class="s"&gt;3. For each real issue, explain the attack vector in one sentence&lt;/span&gt;
          &lt;span class="s"&gt;4. Suggest a fix for Critical and High issues&lt;/span&gt;

          &lt;span class="s"&gt;Format as a markdown table." \&lt;/span&gt;
            &lt;span class="s"&gt;--output-format text \&lt;/span&gt;
            &lt;span class="s"&gt;--max-turns 3)&lt;/span&gt;

          &lt;span class="s"&gt;echo "$TRIAGE" &amp;gt;&amp;gt; $GITHUB_STEP_SUMMARY&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Generate Tests for Modified Code
&lt;/h3&gt;

&lt;p&gt;When a PR changes source files, the agent generates missing tests:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Auto-Generate Tests&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generate-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.head_ref }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;

      &lt;span class="pi"&gt;-&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;npm ci&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 Claude Code&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;npm install -g @anthropic-ai/claude-code&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;Generate tests for changed files&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ANTHROPIC_API_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;CHANGED=$(gh pr diff ${{ github.event.pull_request.number }} --name-only | grep -E '\.(ts|js)$' | grep -v test | grep -v spec)&lt;/span&gt;

          &lt;span class="s"&gt;for FILE in $CHANGED; do&lt;/span&gt;
            &lt;span class="s"&gt;TEST_FILE=$(echo "$FILE" | sed 's/\.ts$/.test.ts/' | sed 's|src/|tests/unit/|')&lt;/span&gt;

            &lt;span class="s"&gt;if [! -f "$TEST_FILE"]; then&lt;/span&gt;
              &lt;span class="s"&gt;claude -p "Read $FILE and generate a comprehensive unit test file.&lt;/span&gt;
              &lt;span class="s"&gt;Use Vitest. Follow the existing test patterns in the project.&lt;/span&gt;
              &lt;span class="s"&gt;Save the test file to $TEST_FILE.&lt;/span&gt;
              &lt;span class="s"&gt;Test all exported functions including edge cases." \&lt;/span&gt;
                &lt;span class="s"&gt;--allowedTools Read,Write \&lt;/span&gt;
                &lt;span class="s"&gt;--max-turns 10&lt;/span&gt;
            &lt;span class="s"&gt;fi&lt;/span&gt;
          &lt;span class="s"&gt;done&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;Verify tests pass&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;npm test&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;Commit generated tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git config user.name "AI Agent"&lt;/span&gt;
          &lt;span class="s"&gt;git config user.email "ai-agent@deployhq.com"&lt;/span&gt;
          &lt;span class="s"&gt;git add tests/&lt;/span&gt;
          &lt;span class="s"&gt;git diff --staged --quiet || git commit -m "test: add AI-generated tests for new code"&lt;/span&gt;
          &lt;span class="s"&gt;git push&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Other CI Platforms
&lt;/h2&gt;

&lt;p&gt;The same patterns work on any CI platform:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitLab CI:&lt;/strong&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;ai-review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;review&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;node:20&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm install -g @anthropic-ai/claude-code&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git diff origin/main...HEAD | claude -p "Review this diff for bugs and security issues" --output-format text &amp;gt; review.md&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cat review.md&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;reports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;dotenv&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;review.md&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$ANTHROPIC_API_KEY&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Bitbucket Pipelines:&lt;/strong&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;pipelines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;step&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;AI Code Review&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;node:20&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm install -g @anthropic-ai/claude-code&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git diff origin/main...HEAD | claude -p "Review for security issues" --output-format text&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Connecting to Deployment
&lt;/h2&gt;

&lt;p&gt;After the agent's changes pass CI, you can trigger a deployment automatically using the &lt;a href="https://www.deployhq.com/support/cli" rel="noopener noreferrer"&gt;DeployHQ CLI&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;- name: Deploy to Staging
  &lt;span class="k"&gt;if&lt;/span&gt;: success&lt;span class="o"&gt;()&lt;/span&gt;
  run: |
    curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://www.deployhq.com/install-cli.sh | bash
    dhq auth login &lt;span class="nt"&gt;--token&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="p"&gt;{ secrets.DEPLOYHQ_API_KEY &lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;
    dhq deploy &lt;span class="nt"&gt;--server&lt;/span&gt; staging &lt;span class="nt"&gt;--wait&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This completes the loop: agent writes code → CI validates → agent fixes failures → the deployment pipeline ships it. The full pattern is covered in our companion post on &lt;a href="https://www.deployhq.com/blog/ai-agents-cicd-pipelines-deployhq-cli" rel="noopener noreferrer"&gt;connecting agentic CI/CD to DeployHQ&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost Management
&lt;/h2&gt;

&lt;p&gt;Running AI agents in CI can get expensive fast. Here's how to keep costs under control:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;th&gt;Savings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Limit turns&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--max-turns 10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prevents infinite loops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Restrict tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--allowedTools Read,Edit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fewer API calls per turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Filter triggers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only run on &lt;code&gt;src/&lt;/code&gt; changes&lt;/td&gt;
&lt;td&gt;Skip docs-only PRs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Use cheaper models&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude Haiku for simple tasks&lt;/td&gt;
&lt;td&gt;10x cheaper than Opus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cache results&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Skip review if diff unchanged&lt;/td&gt;
&lt;td&gt;Eliminates duplicate runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Set timeouts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;timeout-minutes: 10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hard stop on runaway agents&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A typical AI review step costs $0.02-0.10 per PR with Claude Sonnet. Auto-fix attempts cost more ($0.20-1.00) because the agent reads files and makes multiple edits. Budget accordingly — for most teams, this is cheaper than the developer time it saves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Considerations
&lt;/h2&gt;

&lt;p&gt;Running an AI agent in CI means giving it access to your code and (potentially) your secrets. Lock it down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scope secrets tightly.&lt;/strong&gt; The agent needs an API key for its model provider. It does NOT need your database credentials, deployment tokens, or SSH keys. Use GitHub's environment-level secrets to control access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restrict file access.&lt;/strong&gt; Use &lt;code&gt;--allowedTools&lt;/code&gt; to prevent the agent from running arbitrary shell commands. &lt;code&gt;Read,Edit&lt;/code&gt; is sufficient for most review tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate output.&lt;/strong&gt; Don't blindly commit agent-generated changes. Run your full test suite after any agent modification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit agent commits.&lt;/strong&gt; Tag agent-generated commits with a machine author (&lt;code&gt;ai-agent@yourcompany.com&lt;/code&gt;) so they're easy to identify in git history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't run agents on forks.&lt;/strong&gt; Disable AI review steps on PRs from forked repositories — the fork author could craft a prompt injection in the diff.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When to Use What
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Trade-offs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Platform agent&lt;/strong&gt; (Copilot)&lt;/td&gt;
&lt;td&gt;Issue-to-PR automation, managed infrastructure&lt;/td&gt;
&lt;td&gt;Less control, GitHub-only, opaque&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Headless CLI&lt;/strong&gt; (Claude/Codex/Gemini)&lt;/td&gt;
&lt;td&gt;Custom review, fix, and generation workflows&lt;/td&gt;
&lt;td&gt;More setup, you manage costs, any CI platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Dedicated review tool&lt;/strong&gt; (&lt;a href="https://dev.to/deployhq/ai-code-review-tools-compared-coderabbit-vs-github-copilot-code-review-vs-sourcery-vs-ellipsis-29og-temp-slug-397619"&gt;CodeRabbit&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;PR review specifically&lt;/td&gt;
&lt;td&gt;Best review UX, less flexible for other tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple projects, strict security requirements&lt;/td&gt;
&lt;td&gt;Maximum control, minimum risk&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most teams end up combining approaches: a dedicated review tool (CodeRabbit or Copilot) for every PR, plus headless CLI agents for specific workflows like changelog generation or test scaffolding.&lt;/p&gt;




&lt;p&gt;Running your own AI agent in CI gives you control that platform-managed agents can't match. You choose the model, the prompt, the tools, and the budget. The trade-off is setup and maintenance — but once the workflow YAML is written, it runs on every PR without intervention.&lt;/p&gt;

&lt;p&gt;Start with one workflow — changelog generation or AI code review — and expand from there. Keep costs visible, scope permissions tightly, and always run tests after agent modifications.&lt;/p&gt;

&lt;p&gt;Ready to connect your AI-powered CI pipeline to deployment? &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; deploys to any server you own with &lt;a href="https://www.deployhq.com/features/zero-downtime-deployments" rel="noopener noreferrer"&gt;zero-downtime deployments&lt;/a&gt;, &lt;a href="https://www.deployhq.com/features/one-click-rollback" rel="noopener noreferrer"&gt;one-click rollback&lt;/a&gt;, and a CLI built for CI/CD automation. &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Get started for free&lt;/a&gt;.&lt;/p&gt;

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

</description>
      <category>ai</category>
      <category>devopsinfrastructure</category>
    </item>
    <item>
      <title>Managing Secrets: .env Files, .env.example, and Config in DeployHQ</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Wed, 08 Jul 2026 07:18:27 +0000</pubDate>
      <link>https://dev.to/deployhq/managing-secrets-env-files-envexample-and-config-in-deployhq-64i</link>
      <guid>https://dev.to/deployhq/managing-secrets-env-files-envexample-and-config-in-deployhq-64i</guid>
      <description>&lt;p&gt;Every application needs configuration that changes between environments — database URLs, API keys, feature flags. The &lt;code&gt;.env&lt;/code&gt; file is the standard way to manage these values locally without hardcoding them into your source code. This guide explains what &lt;code&gt;.env&lt;/code&gt; files are, how they work across languages, and how to handle them safely from development through &lt;a href="https://www.deployhq.com/features" rel="noopener noreferrer"&gt;deployment&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is a .env File?
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;.env&lt;/code&gt; file is a plain text file that sits in the root of your project and stores configuration as key-value pairs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;postgres://localhost:5432/myapp&lt;/span&gt;
&lt;span class="py"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sk-abc123def456&lt;/span&gt;
&lt;span class="py"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3000&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Your application reads this file at startup and loads the values into the process environment. This keeps sensitive credentials and environment-specific settings out of your codebase.&lt;/p&gt;

&lt;p&gt;The name &lt;code&gt;.env&lt;/code&gt; follows the Unix convention where dot-prefixed files are hidden by default — present but unobtrusive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The KEY=VALUE Format
&lt;/h2&gt;

&lt;p&gt;The syntax is simple, but there are rules worth knowing:&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="c"&gt;# This is a comment
&lt;/span&gt;&lt;span class="py"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;postgres://localhost:5432/myapp&lt;/span&gt;

&lt;span class="c"&gt;# Quoted values preserve whitespace
&lt;/span&gt;&lt;span class="py"&gt;APP_NAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"My Application"&lt;/span&gt;

&lt;span class="c"&gt;# Single quotes prevent variable expansion
&lt;/span&gt;&lt;span class="py"&gt;GREETING&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'Hello $USER'&lt;/span&gt;

&lt;span class="c"&gt;# No spaces around the equals sign
&lt;/span&gt;&lt;span class="py"&gt;CORRECT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;
&lt;span class="c"&gt;# WRONG = value &amp;lt;-- this breaks most parsers
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No spaces around &lt;code&gt;=&lt;/code&gt;&lt;/strong&gt;. &lt;code&gt;KEY=value&lt;/code&gt; is correct; &lt;code&gt;KEY = value&lt;/code&gt; breaks most parsers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comments&lt;/strong&gt; start with &lt;code&gt;#&lt;/code&gt; at the beginning of a line&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty lines&lt;/strong&gt; are ignored&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quotes&lt;/strong&gt; are optional for simple values. Use double quotes for values with spaces, single quotes to prevent variable interpolation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;export&lt;/code&gt; keyword&lt;/strong&gt;. Unlike shell scripts, &lt;code&gt;.env&lt;/code&gt; files omit &lt;code&gt;export&lt;/code&gt;. Some parsers accept it, but it's not standard&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Different Frameworks Load .env Files
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file doesn't load itself. Your language or framework needs a library to read it and inject values into the process environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Node.js
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/motdotla/dotenv" rel="noopener noreferrer"&gt;dotenv&lt;/a&gt; package is the standard:&lt;br&gt;
&lt;/p&gt;

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


&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dbUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Node.js 20.6+ has a built-in &lt;code&gt;--env-file&lt;/code&gt; flag that eliminates the package entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--env-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env app.js

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Python
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dotenv&lt;/span&gt;


&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;db_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEBUG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Django projects often use &lt;a href="https://django-environ.readthedocs.io/" rel="noopener noreferrer"&gt;django-environ&lt;/a&gt; for typed access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;environ&lt;/span&gt;
&lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_env&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;DEBUG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEBUG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Ruby
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Gemfile&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'dotenv'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;groups: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:development&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Load early in boot&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'dotenv/load'&lt;/span&gt;

&lt;span class="n"&gt;db_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'DATABASE_URL'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Rails projects use &lt;code&gt;dotenv-rails&lt;/code&gt;, which loads automatically before the app initializes.&lt;/p&gt;

&lt;h3&gt;
  
  
  PHP
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;composer&lt;/span&gt; &lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="n"&gt;vlucas&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;phpdotenv&lt;/span&gt;


&lt;span class="nv"&gt;$dotenv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Dotenv\Dotenv&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;createImmutable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$dotenv&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$dbUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'DATABASE_URL'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Require specific variables (throws exception if missing)&lt;/span&gt;
&lt;span class="nv"&gt;$dotenv&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'DATABASE_URL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'API_KEY'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Laravel loads &lt;code&gt;.env&lt;/code&gt; automatically. Access values with the &lt;code&gt;env()&lt;/code&gt; helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$debug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'APP_DEBUG'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;If you're shipping a Laravel app to production, our &lt;a href="https://dev.to/theqadiariesforyou/how-to-deploy-laravel-zero-downtime-build-pipelines-and-best-practices-1jcj-temp-slug-5099175"&gt;zero-downtime Laravel deployment guide&lt;/a&gt; walks through build pipelines and environment configuration end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Best Practices
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file often contains the most sensitive data in your project. Mishandling it is one of the most common security mistakes in web development.&lt;/p&gt;

&lt;h3&gt;
  
  
  Never Commit .env to Version Control
&lt;/h3&gt;

&lt;p&gt;Add &lt;code&gt;.env&lt;/code&gt; to your &lt;a href="https://www.deployhq.com/blog/understanding-keep-and-gitkeep-files-a-guide" rel="noopener noreferrer"&gt;.gitignore&lt;/a&gt; immediately:&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="err"&gt;.env&lt;/span&gt;
&lt;span class="err"&gt;.env.local&lt;/span&gt;
&lt;span class="err"&gt;.env.*.local&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;If you've already committed a &lt;code&gt;.env&lt;/code&gt; file, removing it from &lt;code&gt;.gitignore&lt;/code&gt; isn't enough — the values are still in your Git history. Rotate every exposed secret, then use &lt;a href="https://rtyley.github.io/bfg-repo-cleaner/" rel="noopener noreferrer"&gt;BFG Repo-Cleaner&lt;/a&gt; to scrub the history. Our guide to &lt;a href="https://www.deployhq.com/blog/protecting-your-api-keys-a-quick-guide" rel="noopener noreferrer"&gt;protecting your API keys&lt;/a&gt; covers rotation and leak response in more depth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use .env.example as a Template
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;.env.example&lt;/code&gt; that documents every required variable with placeholder values:&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="c"&gt;# .env.example - Copy to .env and fill in real values
&lt;/span&gt;&lt;span class="py"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;postgres://user:password@localhost:5432/dbname&lt;/span&gt;
&lt;span class="py"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your-api-key-here&lt;/span&gt;
&lt;span class="py"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3000&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Commit this file. New developers copy it, fill in their values, and they're ready to go.&lt;/p&gt;

&lt;h3&gt;
  
  
  Restrict File Permissions
&lt;/h3&gt;

&lt;p&gt;On Unix systems:&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;chmod &lt;/span&gt;600 .env

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

&lt;/div&gt;



&lt;p&gt;This ensures only the file owner can read or write it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment-Specific Files
&lt;/h2&gt;

&lt;p&gt;Most frameworks support a hierarchy of &lt;code&gt;.env&lt;/code&gt; files that override each other:&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="err"&gt;.env&lt;/span&gt; &lt;span class="c"&gt;# Shared defaults (committed)
&lt;/span&gt;&lt;span class="err"&gt;.env.local&lt;/span&gt; &lt;span class="c"&gt;# Local overrides (not committed)
&lt;/span&gt;&lt;span class="err"&gt;.env.development&lt;/span&gt; &lt;span class="c"&gt;# Development-specific
&lt;/span&gt;&lt;span class="err"&gt;.env.test&lt;/span&gt; &lt;span class="c"&gt;# Test-specific
&lt;/span&gt;&lt;span class="err"&gt;.env.production&lt;/span&gt; &lt;span class="c"&gt;# Production-specific
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The typical loading order (varies by framework):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;.env&lt;/code&gt; — base defaults&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.env.local&lt;/code&gt; — local overrides (skipped in test)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.env.{environment}&lt;/code&gt; — environment-specific values&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.env.{environment}.local&lt;/code&gt; — local environment overrides&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Vite&lt;/strong&gt; only exposes variables prefixed with &lt;code&gt;VITE_&lt;/code&gt; to client-side code. &lt;strong&gt;Next.js&lt;/strong&gt; uses &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt;. &lt;strong&gt;Create React App&lt;/strong&gt; uses &lt;code&gt;REACT_APP_&lt;/code&gt;. These prefixes prevent accidental exposure of server-side secrets to the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Patterns and Gotchas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Variable Expansion
&lt;/h3&gt;

&lt;p&gt;Some parsers support referencing other variables:&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;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com&lt;/span&gt;
&lt;span class="py"&gt;USERS_API&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;${BASE_URL}/users&lt;/span&gt;
&lt;span class="py"&gt;POSTS_API&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;${BASE_URL}/posts&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;dotenv-expand&lt;/code&gt; package enables this in Node.js. Python's &lt;code&gt;python-dotenv&lt;/code&gt; supports it natively. Not all parsers handle expansion, so test before relying on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiline Values
&lt;/h3&gt;

&lt;p&gt;Multiline values require double quotes:&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;PRIVATE_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF...
-----END RSA PRIVATE KEY-----"&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;If your parser struggles with this, base64-encode the value instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;PRIVATE_KEY_BASE64&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;TUlJRXBBSUJBQUtDQVFF&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;


&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PRIVATE_KEY_BASE64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Boolean Values
&lt;/h3&gt;

&lt;p&gt;There's no standard for booleans. These are all common:&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;DEBUG&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Pick one convention and be consistent. Always parse explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEBUG&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuring Secrets in DeployHQ: UI, CLI, and API
&lt;/h2&gt;

&lt;p&gt;During local development, &lt;code&gt;.env&lt;/code&gt; files are convenient. In production, you should not deploy &lt;code&gt;.env&lt;/code&gt; files to your servers. Once your secrets are out of your repository, you still need a way to store them and inject them at deploy time. &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; gives you three ways to do that — the web UI, the &lt;code&gt;dhq&lt;/code&gt; command-line tool, and the API — so you can pick whichever fits a one-off change or an automated workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Config Files vs Environment Variables
&lt;/h3&gt;

&lt;p&gt;Before configuring anything, it helps to know which of the two mechanisms you actually need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Config files&lt;/strong&gt; are whole files uploaded at deploy time and &lt;em&gt;not&lt;/em&gt; stored in your repository — think a production &lt;code&gt;.env&lt;/code&gt;, a &lt;code&gt;credentials.json&lt;/code&gt;, or a &lt;code&gt;settings.yml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables&lt;/strong&gt; are individual values injected into the build and referenced from config files or SSH commands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule of thumb is simple: if you're managing a single value, use an environment variable; if you're managing an entire file, use a config file.&lt;/p&gt;

&lt;h3&gt;
  
  
  The UI Method
&lt;/h3&gt;

&lt;p&gt;For a one-off change, the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; web UI is the fastest path.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com/support/config-files" rel="noopener noreferrer"&gt;&lt;strong&gt;Config files&lt;/strong&gt;&lt;/a&gt; live under &lt;strong&gt;Settings → Config Files&lt;/strong&gt;. They're uploaded during deployment rather than stored in your repository, which makes them ideal for sensitive data like database configuration. When you create one, you specify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The full path as the filename (this is where the file lands on the server)&lt;/li&gt;
&lt;li&gt;The file content&lt;/li&gt;
&lt;li&gt;An optional description&lt;/li&gt;
&lt;li&gt;A language for syntax highlighting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two toggles shape how the file is used. &lt;strong&gt;Build Pipeline Integration&lt;/strong&gt; makes the config file available during your build commands. &lt;strong&gt;Server Distribution&lt;/strong&gt; lets you upload it to all current and future servers and server groups, or target specific ones. You can also set up multiple config files per environment type — development, staging, and production — and only deploy them where they belong. Config files can go out with a standard deployment, or via a dedicated &lt;strong&gt;Config file deployment&lt;/strong&gt; that ships only config files with no other changes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com/support/projects/environment-variables" rel="noopener noreferrer"&gt;&lt;strong&gt;Environment variables&lt;/strong&gt;&lt;/a&gt; live in the project sidebar under &lt;strong&gt;Environment Variables&lt;/strong&gt;. These are dynamic values usable in config files, SSH commands, and notifications. To create one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The name must start with a letter and contain only uppercase letters, numbers, and underscores&lt;/li&gt;
&lt;li&gt;Enter a value&lt;/li&gt;
&lt;li&gt;Toggle &lt;strong&gt;Use this variable with the Build Pipeline&lt;/strong&gt; if the build needs it&lt;/li&gt;
&lt;li&gt;Choose which server or server group can access it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the build pipeline, these are exported as real environment variables, so tools like Vite, Webpack, Next.js, and Create React App read them via &lt;code&gt;process.env.VARIABLE_NAME&lt;/code&gt; with no &lt;code&gt;.env&lt;/code&gt; file setup at all. In deployments, config files, and SSH commands, you reference them with the &lt;code&gt;%VARIABLE_NAME%&lt;/code&gt; syntax.&lt;/p&gt;

&lt;p&gt;On security: all variable values are &lt;strong&gt;encrypted at rest&lt;/strong&gt; and never visible in logs or the UI. Values can also be &lt;strong&gt;locked&lt;/strong&gt; , which prevents viewing or changing them while they keep working in deployments. When the same variable is defined at multiple levels, targeting precedence runs highest to lowest: server-specific overrides a server group, which overrides all-servers. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; also ships roughly 20 built-in variables — such as &lt;code&gt;%environment%&lt;/code&gt;, &lt;code&gt;%branch%&lt;/code&gt;, &lt;code&gt;%deployer%&lt;/code&gt;, and &lt;code&gt;%status%&lt;/code&gt; — that you can reference without defining anything.&lt;/p&gt;

&lt;p&gt;If you're new to how the build stage fits into a deployment, &lt;a href="https://www.deployhq.com/blog/the-deployhq-build-pipeline" rel="noopener noreferrer"&gt;the&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; build pipeline explains where these variables and files are injected.&lt;/p&gt;

&lt;h3&gt;
  
  
  The CLI Method
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://www.deployhq.com/support/cli" rel="noopener noreferrer"&gt;&lt;code&gt;dhq&lt;/code&gt; command-line tool&lt;/a&gt; is a single binary that brings the &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; workflow to your terminal — deploy, monitor, manage servers, and automate, all without leaving the shell.&lt;/p&gt;

&lt;p&gt;Install it with Homebrew, the install script, or a prebuilt binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;deployhq/tap/dhq
&lt;span class="c"&gt;# or&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/deployhq/deployhq-cli/main/install.sh | sh

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

&lt;/div&gt;



&lt;p&gt;You can also grab a binary directly from GitHub Releases.&lt;/p&gt;

&lt;p&gt;Authenticate once with &lt;code&gt;dhq auth login&lt;/code&gt;, which stores your credentials in the OS keyring. &lt;code&gt;dhq hello&lt;/code&gt; gives you a guided login/signup flow plus project selection, and for CI/CD you authenticate via environment variables instead of the interactive login.&lt;/p&gt;

&lt;p&gt;The commands relevant to secrets are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dhq env-vars&lt;/code&gt; — manage a project's environment variables&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dhq global-env-vars&lt;/code&gt; — manage account-wide environment variables&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dhq config-files&lt;/code&gt; — manage configuration files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dhq api&lt;/code&gt; — reach the full set of 144+ API endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CLI also offers JSON output for scripting and shell completions for faster typing.&lt;/p&gt;

&lt;p&gt;The exact create, set, and update sub-command syntax isn't fixed here — run &lt;code&gt;dhq &amp;lt;command&amp;gt; --help&lt;/code&gt; (for example, &lt;code&gt;dhq env-vars --help&lt;/code&gt;) to see the current usage for your version rather than guessing at flags.&lt;/p&gt;

&lt;h3&gt;
  
  
  The API Method
&lt;/h3&gt;

&lt;p&gt;For fully programmatic management, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; exposes &lt;strong&gt;144+ API endpoints&lt;/strong&gt; , reachable either through &lt;code&gt;dhq api&lt;/code&gt; or directly. This is the right layer when you want to script secret provisioning — for instance, auto-creating a standard set of environment variables and config files every time you onboard a new project or client.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which One Should You Use?
&lt;/h3&gt;

&lt;p&gt;The CLI and API make your secrets reproducible and automatable, which is exactly what you want when they need to live in CI or in an onboarding script that spins up a new project the same way every time. The UI, by contrast, is the quickest route for a one-off change. Most teams end up using all three: the UI to eyeball and tweak, the CLI and API to codify.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  A[Code in Git] --&amp;gt; B[DeployHQ]
  B --&amp;gt; C[Build with env vars]
  C --&amp;gt; D[Deploy to server]
  E[Secure variable store] --&amp;gt; B

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

&lt;/div&gt;



&lt;p&gt;The end-to-end workflow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Development&lt;/strong&gt; : Use &lt;code&gt;.env&lt;/code&gt; files locally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/Build&lt;/strong&gt; : Environment variables and config files set in DeployHQ's project configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment&lt;/strong&gt; : Variables available during build commands and injected into config files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime&lt;/strong&gt; : Application reads from the server's environment&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you deploy from &lt;a href="https://www.deployhq.com/deploy-from-github" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; or &lt;a href="https://www.deployhq.com/deploy-from-gitlab" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; connects to your repository and handles the rest. For &lt;a href="https://www.deployhq.com/for-agencies" rel="noopener noreferrer"&gt;agencies&lt;/a&gt; managing multiple client projects, each project gets an isolated variable store so credentials never leak between clients. This same config-in-the-environment discipline is the backbone of &lt;a href="https://dev.to/deployhq/implementing-12-factor-app-methodology-with-deployhq-38lh"&gt;the 12-Factor App methodology&lt;/a&gt;, and it applies just as much to a &lt;a href="https://dev.to/deployhq/deployhq-static-hosting-vs-cloudflare-pages-same-edge-different-workflows-47jg-temp-slug-7657484"&gt;static site on Cloudflare's edge&lt;/a&gt; as it does to a traditional server.&lt;/p&gt;

&lt;p&gt;Ready to get your secrets out of Git for good? &lt;strong&gt;&lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Start a&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; project&lt;/strong&gt; and add your first encrypted environment variable in a couple of minutes.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Should I commit .env.example to Git?&lt;/strong&gt; Yes. It documents which variables your app requires with placeholder values. Never put real credentials in it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if a variable is defined in both .env and the system environment?&lt;/strong&gt; In most libraries, system environment variables take precedence. This lets your deployment platform override defaults without modifying files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use .env files in Docker containers?&lt;/strong&gt; Yes. Docker supports &lt;code&gt;--env-file&lt;/code&gt; natively, and Docker Compose reads &lt;code&gt;.env&lt;/code&gt; in the project root automatically for variable substitution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I handle .env files across a team?&lt;/strong&gt; Use &lt;code&gt;.env.example&lt;/code&gt; as the shared template. Each developer maintains their own &lt;code&gt;.env&lt;/code&gt; with local values. For shared secrets, use a password vault rather than distributing files via chat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are .env files secure enough for production?&lt;/strong&gt; For local development, yes. For production, use your platform's built-in secrets management — like DeployHQ's secure environment variables, which are encrypted at rest and injected at deploy time.&lt;/p&gt;




&lt;p&gt;Ready to stop worrying about environment configuration? &lt;strong&gt;&lt;a href="https://www.deployhq.com/features/automatic-deployments" rel="noopener noreferrer"&gt;Automate deployments with DeployHQ's build pipeline&lt;/a&gt;&lt;/strong&gt; and manage your environment variables securely from day one. See &lt;a href="https://www.deployhq.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt; for team plans.&lt;/p&gt;




&lt;p&gt;Questions? Reach out 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;.&lt;/p&gt;

</description>
      <category>devopsinfrastructure</category>
      <category>tipstricks</category>
    </item>
    <item>
      <title>Project Folders Are Here: Group Deployments by Client, Team, or Environment</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Tue, 07 Jul 2026 07:59:05 +0000</pubDate>
      <link>https://dev.to/deployhq/project-folders-are-here-group-deployments-by-client-team-or-environment-58mb</link>
      <guid>https://dev.to/deployhq/project-folders-are-here-group-deployments-by-client-team-or-environment-58mb</guid>
      <description>&lt;p&gt;If your &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; projects list has grown into an endless scroll, you already know the friction: hunting for the right staging project among two dozen others, second-guessing which &lt;q&gt;acme-prod&lt;/q&gt; belongs to which client, and losing a few seconds every single deploy. We built &lt;strong&gt;project folders&lt;/strong&gt; to fix exactly that — a simple, new way to group related projects into collections so your dashboard maps to how you actually work.&lt;/p&gt;

&lt;p&gt;Folders are especially handy if you run deployments for more than one client, team, or product line. Put everything for a client in a single folder, and their staging site, production site, and supporting microservices sit together instead of scattered across an alphabetical wall of projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  What project folders actually do
&lt;/h2&gt;

&lt;p&gt;A folder is a collection of projects. &lt;strong&gt;Each project belongs to a single folder&lt;/strong&gt; — it's a flat, one-folder-per-project structure, so there's no nesting to reason about and no ambiguity about where a project lives. You group, you filter, you find what you need. That's the whole idea.&lt;/p&gt;

&lt;p&gt;On the Projects page, a sidebar gives you the controls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;All Projects&lt;/strong&gt; — the full view. Any projects you've marked as favourites float to the top here, regardless of which folder they're in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Favourites&lt;/strong&gt; — a quick lane for the projects you touch most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your folders&lt;/strong&gt; — listed alphabetically, each one filtering the view to just its projects when selected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ungrouped&lt;/strong&gt; — everything you haven't filed into a folder yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Selecting any sidebar entry filters the main view to match. And crucially, &lt;strong&gt;search still spans every project across every folder&lt;/strong&gt; — so organising into folders never hides anything from you. You get structure when you're browsing and a flat search when you're hunting.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters for agencies and freelancers
&lt;/h2&gt;

&lt;p&gt;If you manage client work, a crowded projects dashboard is a daily tax. This is where folders earn their keep, and it's the reason we think &lt;strong&gt;&lt;a href="https://www.deployhq.com/for-agencies" rel="noopener noreferrer"&gt;agencies running deployments across many clients&lt;/a&gt;&lt;/strong&gt; will feel the difference fastest.&lt;/p&gt;

&lt;p&gt;The pattern we'd recommend starting with:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One folder per client.&lt;/strong&gt; Client A's staging environment, production environment, and any microservices all live in the same folder. When Client A calls, you click their folder and everything relevant is right there — no scanning, no guessing. This builds directly on the workflow we covered in &lt;a href="https://www.deployhq.com/blog/deployhq-a-game-changer-for-web-agencies" rel="noopener noreferrer"&gt;how&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; helps web agencies manage multiple projects, giving that multi-client setup a visual home.&lt;/p&gt;

&lt;p&gt;Freelancers benefit from the same move at a smaller scale. If you've already &lt;a href="https://dev.to/deployhq/freelance-freedom-ditch-ssh-ftp-for-effortless-deployments-with-deployhq-5898"&gt;ditched SSH and FTP for automated deployments&lt;/a&gt;, folders are the next tidy-up: one folder per client keeps a growing roster of side projects and retainers from turning into clutter.&lt;/p&gt;

&lt;p&gt;You don't have to organise by client, either. Folders work just as well grouped by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Team&lt;/strong&gt; — front-end, platform, data, each with their own collection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment group&lt;/strong&gt; — separating a batch of internal tooling from client-facing production work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product line&lt;/strong&gt; — one folder per product when you're running several under one roof.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Setting up folders in a couple of clicks
&lt;/h2&gt;

&lt;p&gt;Everything here lives on the Projects page and its sidebar. The full step-by-step lives in the &lt;strong&gt;&lt;a href="https://www.deployhq.com/support/projects/organising-projects-into-folders" rel="noopener noreferrer"&gt;support guide on organising projects into folders&lt;/a&gt;&lt;/strong&gt;, but here's the shape of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create a folder.&lt;/strong&gt; Any user who can create projects can create a folder, straight from the sidebar. An empty folder doesn't touch your existing projects — nothing moves until you decide it should — so there's no risk in setting up your structure first and filing projects in afterward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assign a project to a folder.&lt;/strong&gt; Two ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the project, go to its &lt;strong&gt;Settings&lt;/strong&gt; page, pick a folder from the &lt;strong&gt;Folder&lt;/strong&gt; dropdown (or choose &lt;q&gt;No folder&lt;/q&gt; to pull it back out), and &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Or just &lt;strong&gt;drag the project onto a folder&lt;/strong&gt; in the sidebar to move it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That drag-and-drop is the fast path when you're doing an initial cleanup and want to file a dozen projects in one sitting.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ungrouped as an intake lane
&lt;/h2&gt;

&lt;p&gt;Here's a small workflow trick worth adopting: treat &lt;strong&gt;Ungrouped&lt;/strong&gt; as a triage lane rather than a junk drawer. Every new project starts life ungrouped, so that section naturally becomes your &lt;q&gt;needs filing&lt;/q&gt; queue. Onboard a new client, spin up their first project, and it waits in Ungrouped until you drop it into the right folder. Glance at Ungrouped periodically and it doubles as a reminder of anything you've set up but not yet organised.&lt;/p&gt;

&lt;p&gt;Pair that with &lt;strong&gt;Favourites&lt;/strong&gt; for whatever you're actively shipping this week, and you've got a two-speed system: Favourites for the hot work, folders for the durable structure, and Ungrouped as the inbox in between.&lt;/p&gt;

&lt;p&gt;Want to keep your dashboard clean &lt;em&gt;and&lt;/em&gt; your pipeline fast? &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 set your folders up on day one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Permissions: let a lead own the structure
&lt;/h2&gt;

&lt;p&gt;Folder management respects your existing access model, which matters the moment more than one person shares an account. &lt;strong&gt;Account administrators and users granted access to all projects can rename and delete folders.&lt;/strong&gt; Users with limited project access can't — they work within the structure but can't reshape it.&lt;/p&gt;

&lt;p&gt;For an agency, that maps cleanly onto real roles. A lead administrator owns the folder structure — deciding that every client gets a folder and keeping the naming consistent — while contractors and junior team members operate inside their scoped access without accidentally renaming or removing a client's folder. If you're formalising who-can-do-what across your pipeline, it's the same access-control thinking auditors expect when they review &lt;a href="https://dev.to/deployhq/soc-2-compliance-for-deployment-workflows-what-auditors-look-for-g1e"&gt;deployment workflows for SOC 2 compliance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One reassuring detail: &lt;strong&gt;deleting a folder never deletes the projects inside it.&lt;/strong&gt; Those projects simply become ungrouped, ready to be reassigned. Restructuring is always safe — the worst case is a few projects landing back in the Ungrouped lane.&lt;/p&gt;




&lt;h2&gt;
  
  
  Take it further: team management for scoped access
&lt;/h2&gt;

&lt;p&gt;Folders organise &lt;em&gt;what you see&lt;/em&gt;. To control &lt;em&gt;who can see and touch what&lt;/em&gt;, pair them with team management — together they're the complete control story for a shared agency account. Team management is available on DeployHQ's &lt;strong&gt;Business and Enterprise plans&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A team is a group of users who share the same permissions and project access. Add someone to a team and they inherit everything that team is granted, so you configure access once rather than fiddling with it per person. Each team sets two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Account-level permissions&lt;/strong&gt; — whether members can create projects, manage users, manage billing, manage the deployment agents, or act as full account administrators.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project access&lt;/strong&gt; — either &lt;strong&gt;all projects&lt;/strong&gt; (with the option to exclude sensitive ones) or a hand-picked set of &lt;strong&gt;specific projects&lt;/strong&gt; , with granular control over who can deploy to which servers, update project configuration, and manage config files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That maps directly onto a folder-per-client layout. Give a contractor team access to only the specific projects for the client they're working on, while your lead administrators keep all-projects access and own the folder structure. Because a user can belong to several teams and their effective access is the combination — &lt;strong&gt;the most permissive rule wins&lt;/strong&gt; — you can layer a &lt;q&gt;can deploy production&lt;/q&gt; team on top of a &lt;q&gt;client X&lt;/q&gt; team and get exactly the access you'd expect. Teams are created and managed under &lt;strong&gt;Settings → Team Management&lt;/strong&gt; ; the &lt;a href="https://www.deployhq.com/support/users/team-management" rel="noopener noreferrer"&gt;team management guide&lt;/a&gt; covers every permission in detail.&lt;/p&gt;




&lt;h2&gt;
  
  
  Automating folders through the API
&lt;/h2&gt;

&lt;p&gt;Folders are available through the &lt;strong&gt;DeployHQ API&lt;/strong&gt; too, using folder identifiers. The API supports listing, creating, renaming, and deleting folders, along with assigning projects to them.&lt;/p&gt;

&lt;p&gt;That opens up the best part for teams that onboard clients regularly: &lt;strong&gt;auto-provisioning&lt;/strong&gt;. Wire folder creation into your onboarding flow and a new client's folder can be created automatically the moment their account is set up, with their first project assigned to it — no manual filing at all. If you've already explored &lt;a href="https://dev.to/deployhq/using-deployhqs-api-automating-your-deployment-workflows-with-scripts-and-webhooks-2ldo"&gt;automating your deployment workflows with the&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; API, folder operations slot straight into the same scripts and webhooks you're already running.&lt;/p&gt;




&lt;h2&gt;
  
  
  A small feature that scales with you
&lt;/h2&gt;

&lt;p&gt;Project folders won't change how a single deploy runs — your &lt;a href="https://dev.to/deployhq/case-study-digital-agency-achieves-zero-downtime-deployments-on-wp-engine-with-deployhq-2h8c"&gt;build pipelines and zero-downtime releases&lt;/a&gt; work exactly as before. What they change is everything &lt;em&gt;around&lt;/em&gt; the deploy: how quickly you find the right project, how sensibly a shared account stays organised, and how little friction there is when your project count keeps climbing. If you're still weighing platforms and how they handle scale, our rundown of the &lt;a href="https://dev.to/deployhq/best-cicd-software-top-10-tools-to-know-in-2025-2a7b"&gt;best CI/CD tools compared on pricing and features&lt;/a&gt; is a good next read.&lt;/p&gt;

&lt;p&gt;Set up a folder per client, mark your active work as favourites, and let Ungrouped keep you honest about what still needs filing. It's a few minutes of setup that pays off on every deploy after.&lt;/p&gt;

&lt;p&gt;Ready to organise your deployments the way you actually work? &lt;a href="https://www.deployhq.com/pricing" rel="noopener noreferrer"&gt;Compare&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; plans and get started — folders are part of the projects experience, waiting in the sidebar.&lt;/p&gt;




&lt;p&gt;Questions or feedback on project folders? Email us at &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or reach out on X at &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;@deployhq&lt;/a&gt; — we'd love to hear how you're organising your projects.&lt;/p&gt;

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