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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

&lt;p&gt;In your local repo, create &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_DB}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

&lt;p&gt;On the VPS, install Nginx and Certbot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nginx certbot python3-certbot-nginx

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

&lt;/div&gt;



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

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

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

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

    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Enable it and run Certbot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx
&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot &lt;span class="nt"&gt;--nginx&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; workflows.yourdomain.com

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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




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

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

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

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

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

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

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




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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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




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

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

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

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

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

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




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

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

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

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

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

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

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




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

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

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

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

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

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




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

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

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




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

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

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

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

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

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




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

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

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

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

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

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

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




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

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

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

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




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

</description>
      <category>prs</category>
      <category>github</category>
      <category>gitlab</category>
      <category>bitbutcket</category>
    </item>
    <item>
      <title>SQLite vs PostgreSQL vs MySQL: Choosing the Right Database</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Thu, 02 Apr 2026 06:31:57 +0000</pubDate>
      <link>https://dev.to/deployhq/sqlite-vs-postgresql-vs-mysql-choosing-the-right-database-4i14</link>
      <guid>https://dev.to/deployhq/sqlite-vs-postgresql-vs-mysql-choosing-the-right-database-4i14</guid>
      <description>&lt;p&gt;Every application stores data somewhere, and for most web applications, that means a relational database. But which one? SQLite, PostgreSQL, and MySQL serve very different use cases despite sharing SQL as their query language.&lt;/p&gt;

&lt;p&gt;This guide compares all three with honest benchmarks, real configuration differences, and practical advice for choosing the right database for your project and &lt;a href="https://deployhq.com/features" rel="noopener noreferrer"&gt;deployment workflow&lt;/a&gt;.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;SQLite&lt;/th&gt;
&lt;th&gt;PostgreSQL&lt;/th&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Embedded (serverless)&lt;/td&gt;
&lt;td&gt;Client-server&lt;/td&gt;
&lt;td&gt;Client-server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Single file&lt;/td&gt;
&lt;td&gt;Server with data directory&lt;/td&gt;
&lt;td&gt;Server with data directory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Zero configuration&lt;/td&gt;
&lt;td&gt;Install + configure&lt;/td&gt;
&lt;td&gt;Install + configure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrent writes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Limited (file-level locking)&lt;/td&gt;
&lt;td&gt;Excellent (MVCC)&lt;/td&gt;
&lt;td&gt;Good (row-level locking with InnoDB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Max database size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~281 TB (theoretical)&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JSON support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Basic (json functions)&lt;/td&gt;
&lt;td&gt;Advanced (JSONB with indexing)&lt;/td&gt;
&lt;td&gt;JSON type with functions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Full-text search&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;FTS5 extension&lt;/td&gt;
&lt;td&gt;Built-in (ts_vector)&lt;/td&gt;
&lt;td&gt;Built-in (InnoDB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Replication&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Streaming + logical&lt;/td&gt;
&lt;td&gt;Primary-replica, Group Replication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Small apps, development, embedded&lt;/td&gt;
&lt;td&gt;Complex apps, data integrity&lt;/td&gt;
&lt;td&gt;Web apps, read-heavy workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  SQLite: The Embedded Database
&lt;/h2&gt;

&lt;p&gt;SQLite isn't a server — it's a library that reads and writes directly to a single file on disk. There's no installation, no configuration, no separate process. Your application links against the SQLite library and talks directly to the database file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strengths
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero configuration&lt;/strong&gt; : No server to install, configure, or maintain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single file&lt;/strong&gt; : The entire database is one &lt;code&gt;.sqlite&lt;/code&gt; or &lt;code&gt;.db&lt;/code&gt; file — easy to copy, backup, and move&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-contained&lt;/strong&gt; : No external dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliable&lt;/strong&gt; : Used in every smartphone (iOS and Android), every web browser, and most operating systems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast for reads&lt;/strong&gt; : No network overhead since it's in-process&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Concurrent writes&lt;/strong&gt; : Only one writer at a time (readers can run concurrently). WAL mode improves this but doesn't match PostgreSQL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No user management&lt;/strong&gt; : No built-in authentication or access control&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limited ALTER TABLE&lt;/strong&gt; : Can't drop or rename columns in older versions (improved in 3.35.0+)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No network access&lt;/strong&gt; : The database file must be on the same machine as the application&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When to Use SQLite
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Development and prototyping&lt;/strong&gt; : Get started without setting up a database server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small-to-medium applications&lt;/strong&gt; : Blogs, internal tools, single-server apps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile and desktop apps&lt;/strong&gt; : The database ships with the application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedded systems&lt;/strong&gt; : IoT devices, configuration stores&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing&lt;/strong&gt; : In-memory SQLite databases run tests fast with no cleanup&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration Example
&lt;/h3&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;sqlite3&lt;/span&gt;

&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app.db&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="s"&gt;
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        email TEXT UNIQUE NOT NULL,
        name TEXT NOT NULL,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP
    )
&lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  PostgreSQL: The Enterprise Database
&lt;/h2&gt;

&lt;p&gt;PostgreSQL is the most feature-rich open-source database. It emphasizes data integrity, SQL standards compliance, and extensibility. It handles complex queries, large datasets, and high-concurrency workloads.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strengths
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data integrity&lt;/strong&gt; : Strict type checking, ACID compliance, foreign key enforcement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advanced features&lt;/strong&gt; : JSONB, full-text search, window functions, CTEs, materialized views&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensibility&lt;/strong&gt; : Custom types, functions, operators, and extensions (PostGIS, pgvector, TimescaleDB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency&lt;/strong&gt; : MVCC (Multi-Version Concurrency Control) handles many concurrent writers efficiently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL standards&lt;/strong&gt; : Most standards-compliant open-source database&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Setup complexity&lt;/strong&gt; : Requires installation, configuration, and ongoing maintenance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource usage&lt;/strong&gt; : Higher memory and CPU usage than SQLite or MySQL for simple workloads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared hosting&lt;/strong&gt; : Less commonly available on basic hosting plans&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning curve&lt;/strong&gt; : Advanced features require deeper PostgreSQL knowledge&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When to Use PostgreSQL
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complex applications&lt;/strong&gt; : Apps with complex queries, relationships, and data integrity requirements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics and reporting&lt;/strong&gt; : Window functions, CTEs, and materialized views&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geospatial data&lt;/strong&gt; : PostGIS extension is the gold standard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON-heavy workloads&lt;/strong&gt; : JSONB with GIN indexes is powerful&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-concurrency writes&lt;/strong&gt; : MVCC handles many concurrent writers well&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration Example
&lt;/h3&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;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- GIN index on JSONB for fast queries&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_users_metadata&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Full-text search&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;search_vector&lt;/span&gt; &lt;span class="n"&gt;tsvector&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;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_users_search&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search_vector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  MySQL: The Web Database
&lt;/h2&gt;

&lt;p&gt;MySQL (and its fork MariaDB) has been the default database for web applications since the early 2000s. It's the &lt;q&gt;M&lt;/q&gt; in LAMP (Linux, Apache, MySQL, PHP) and remains the most widely deployed database in web hosting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strengths
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ubiquity&lt;/strong&gt; : Available on virtually every hosting platform, including shared hosting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read performance&lt;/strong&gt; : Optimized for read-heavy web workloads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replication&lt;/strong&gt; : Mature primary-replica replication for scaling reads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ecosystem&lt;/strong&gt; : Massive community, extensive tooling, widespread ORM support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WordPress&lt;/strong&gt; : Powers 40%+ of the web through WordPress&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Less strict by default&lt;/strong&gt; : Older versions silently truncate data or accept invalid dates — modern versions (8.0+) improved this significantly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fewer advanced features&lt;/strong&gt; : No native JSONB indexing (until recently), limited window function support in older versions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage engine complexity&lt;/strong&gt; : InnoDB vs MyISAM distinction can confuse beginners&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When to Use MySQL
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web applications&lt;/strong&gt; : Traditional LAMP/LEMP stack applications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WordPress sites&lt;/strong&gt; : WordPress requires MySQL (or MariaDB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-heavy workloads&lt;/strong&gt; : MySQL excels at simple SELECT queries at scale&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared hosting&lt;/strong&gt; : When it's the only database available&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legacy systems&lt;/strong&gt; : Migrating away from MySQL is often more work than staying&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration Example
&lt;/h3&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;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="n"&gt;AUTO_INCREMENT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;InnoDB&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;CHARSET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;utf8mb4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  SQL Syntax Differences
&lt;/h2&gt;

&lt;p&gt;Despite all three using SQL, there are syntax differences that matter when writing queries or switching databases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;SQLite&lt;/th&gt;
&lt;th&gt;PostgreSQL&lt;/th&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auto-increment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AUTOINCREMENT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SERIAL&lt;/code&gt; or &lt;code&gt;GENERATED ALWAYS&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AUTO_INCREMENT&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Boolean type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Integer (0/1)&lt;/td&gt;
&lt;td&gt;Native &lt;code&gt;BOOLEAN&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TINYINT(1)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;String concat&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;`\&lt;/td&gt;
&lt;td&gt;\&lt;/td&gt;
&lt;td&gt;`&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Current time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CURRENT_TIMESTAMP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NOW()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NOW()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UPSERT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INSERT OR REPLACE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ON CONFLICT DO UPDATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ON DUPLICATE KEY UPDATE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LIMIT with offset&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LIMIT n OFFSET m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LIMIT n OFFSET m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LIMIT m, n&lt;/code&gt; or &lt;code&gt;LIMIT n OFFSET m&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;ORMs like Prisma, SQLAlchemy, Django ORM, and ActiveRecord abstract these differences, letting you switch databases without rewriting queries (in theory — in practice, database-specific features often creep in).&lt;/p&gt;

&lt;h2&gt;
  
  
  How Database Choice Affects Deployment
&lt;/h2&gt;

&lt;p&gt;Your database choice has direct implications for how you deploy and manage your application:&lt;/p&gt;

&lt;h3&gt;
  
  
  SQLite Deployments
&lt;/h3&gt;

&lt;p&gt;SQLite's database file lives alongside your application code. This creates a unique deployment challenge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your-app/
├── app.js
├── database.sqlite ← This is your database
├── package.json
└── ...

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

&lt;/div&gt;



&lt;p&gt;When deploying with &lt;a href="https://deployhq.com/features" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, you need to be careful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't overwrite the database on every deploy&lt;/strong&gt;. Exclude &lt;code&gt;*.sqlite&lt;/code&gt; and &lt;code&gt;*.db&lt;/code&gt; from your deployment or use DeployHQ's excluded files feature&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backups are simple&lt;/strong&gt; : Just copy the file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No connection string needed&lt;/strong&gt; : The file path is the connection&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  PostgreSQL and MySQL Deployments
&lt;/h3&gt;

&lt;p&gt;These databases run as separate servers. Your application connects via a connection string:&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="c"&gt;# PostgreSQL
&lt;/span&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;=&lt;span class="n"&gt;postgres&lt;/span&gt;://&lt;span class="n"&gt;user&lt;/span&gt;:&lt;span class="n"&gt;password&lt;/span&gt;@&lt;span class="n"&gt;db&lt;/span&gt;-&lt;span class="n"&gt;server&lt;/span&gt;:&lt;span class="m"&gt;5432&lt;/span&gt;/&lt;span class="n"&gt;myapp&lt;/span&gt;

&lt;span class="c"&gt;# MySQL
&lt;/span&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;=&lt;span class="n"&gt;mysql&lt;/span&gt;://&lt;span class="n"&gt;user&lt;/span&gt;:&lt;span class="n"&gt;password&lt;/span&gt;@&lt;span class="n"&gt;db&lt;/span&gt;-&lt;span class="n"&gt;server&lt;/span&gt;:&lt;span class="m"&gt;3306&lt;/span&gt;/&lt;span class="n"&gt;myapp&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Store these connection strings as environment variables — never hardcode them. DeployHQ's &lt;a href="https://deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;build pipelines&lt;/a&gt; can inject these securely during deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migrations
&lt;/h3&gt;

&lt;p&gt;All three databases need schema migrations as your application evolves. Run migrations as part of your deployment:&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;# In your DeployHQ build command&lt;/span&gt;
npm ci
npm run build
npm run db:migrate

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

&lt;/div&gt;



&lt;p&gt;Whether you push from &lt;a href="https://deployhq.com/deploy-from-github" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; or &lt;a href="https://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; runs your build commands (including migrations) before transferring files to the server.&lt;/p&gt;

&lt;p&gt;For &lt;a href="https://deployhq.com/for-agencies" rel="noopener noreferrer"&gt;agencies&lt;/a&gt; managing client databases, PostgreSQL and MySQL offer user-level access control — you can give each client's app its own database user with restricted permissions.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Can I start with SQLite and migrate to PostgreSQL later?&lt;/strong&gt; Yes, if you use an ORM. The ORM abstracts SQL differences, making the switch mostly painless. Test thoroughly — there are always edge cases with date handling, string comparison, and JSON support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which database is fastest?&lt;/strong&gt; It depends on the workload. SQLite is fastest for single-user read-heavy applications (no network overhead). PostgreSQL handles concurrent writes best. MySQL is optimized for simple read queries at scale. Benchmark your actual workload rather than relying on generic benchmarks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need PostgreSQL for a simple blog?&lt;/strong&gt; No. SQLite or MySQL are fine for simple applications. Choose PostgreSQL when you need advanced features like JSONB, full-text search, or complex analytical queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is MariaDB the same as MySQL?&lt;/strong&gt; MariaDB forked from MySQL in 2009 and is largely compatible. Most MySQL applications work with MariaDB without changes. MariaDB has diverged more in recent versions, adding unique features, but the core SQL and wire protocol remain compatible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which database does &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; recommend?&lt;/strong&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; works with any database — it deploys your application code, and your application connects to whatever database you've set up. For new projects, PostgreSQL is the most versatile choice.&lt;/p&gt;




&lt;p&gt;There's no universally &lt;q&gt;best&lt;/q&gt; database — only the right one for your project. SQLite for simplicity, PostgreSQL for power, MySQL for ubiquity. Pick the one that matches your needs and deploy it confidently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://deployhq.com/signup" rel="noopener noreferrer"&gt;Try&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; free&lt;/strong&gt; — deploy your application with any database and manage connection strings securely. See &lt;a href="https://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>sqlite</category>
      <category>psql</category>
      <category>mysql</category>
    </item>
    <item>
      <title>Nginx vs Apache vs Caddy: Choosing the Right Web Server</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Wed, 01 Apr 2026 13:45:21 +0000</pubDate>
      <link>https://dev.to/deployhq/nginx-vs-apache-vs-caddy-choosing-the-right-web-server-19en</link>
      <guid>https://dev.to/deployhq/nginx-vs-apache-vs-caddy-choosing-the-right-web-server-19en</guid>
      <description>&lt;p&gt;Every web application needs a web server to handle HTTP requests. Whether you're serving a static site, reverse-proxying to a Node.js backend, or hosting a &lt;a href="https://deployhq.com/blog/deploy-wordpress-with-deployhq" rel="noopener noreferrer"&gt;WordPress&lt;/a&gt; installation, your choice of web server affects performance, security, and how much time you spend on configuration.&lt;/p&gt;

&lt;p&gt;This guide compares the three most popular options — Nginx, Apache, and Caddy — with real configuration examples and practical advice for choosing the right one.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Nginx&lt;/th&gt;
&lt;th&gt;Apache&lt;/th&gt;
&lt;th&gt;Caddy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Architecture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Event-driven, async&lt;/td&gt;
&lt;td&gt;Process/thread per connection&lt;/td&gt;
&lt;td&gt;Event-driven, Go goroutines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Excellent for static files and high concurrency&lt;/td&gt;
&lt;td&gt;Good, but higher memory under load&lt;/td&gt;
&lt;td&gt;Very good, slightly behind Nginx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Configuration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;nginx.conf&lt;/code&gt; (custom syntax)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;httpd.conf&lt;/code&gt; + &lt;code&gt;.htaccess&lt;/code&gt; (XML-like)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Caddyfile&lt;/code&gt; (minimal, human-readable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual (Let's Encrypt + certbot)&lt;/td&gt;
&lt;td&gt;Manual (Let's Encrypt + certbot)&lt;/td&gt;
&lt;td&gt;Automatic (built-in ACME)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Per-directory config&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No (centralized only)&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;.htaccess&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No (centralized only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Module system&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Compiled modules&lt;/td&gt;
&lt;td&gt;Dynamic modules (loaded at runtime)&lt;/td&gt;
&lt;td&gt;Plugins (Go modules)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Market share&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~34% (most popular)&lt;/td&gt;
&lt;td&gt;~29% (declining)&lt;/td&gt;
&lt;td&gt;~1% (growing fast)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High-traffic sites, reverse proxy&lt;/td&gt;
&lt;td&gt;Shared hosting, PHP apps&lt;/td&gt;
&lt;td&gt;Small-to-medium sites, automatic HTTPS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Nginx
&lt;/h2&gt;

&lt;p&gt;Nginx (pronounced &lt;q&gt;engine-x&lt;/q&gt;) was created in 2004 to solve the C10K problem — handling 10,000 concurrent connections. Its event-driven architecture uses a small number of worker processes, each handling thousands of connections asynchronously.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strengths
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt; : Serves static files extremely fast with minimal memory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse proxy&lt;/strong&gt; : The de facto standard for proxying to application servers (Node.js, Python, Ruby, Go)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load balancing&lt;/strong&gt; : Built-in upstream balancing with multiple algorithms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low memory footprint&lt;/strong&gt; : Handles thousands of connections with minimal RAM&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration Example
&lt;/h3&gt;

&lt;p&gt;Serving a static site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/var/www/example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;index&lt;/span&gt; &lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Cache static assets&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.(css|js|png|jpg|gif|ico)&lt;/span&gt;$ &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;expires&lt;/span&gt; &lt;span class="s"&gt;30d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;"public,&lt;/span&gt; &lt;span class="s"&gt;immutable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Reverse proxy to a Node.js app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;api.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  When to Use Nginx
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;High-traffic websites needing maximum performance&lt;/li&gt;
&lt;li&gt;Reverse proxying to application servers&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://deployhq.com/blog/how-to-deploy-to-a-vps-with-deployhq" rel="noopener noreferrer"&gt;VPS deployments&lt;/a&gt; where you control the server&lt;/li&gt;
&lt;li&gt;Load balancing across multiple backends&lt;/li&gt;
&lt;li&gt;Serving static assets alongside dynamic applications&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Apache
&lt;/h2&gt;

&lt;p&gt;Apache HTTP Server has been running the web since 1995. It introduced the concept of dynamically loadable modules and per-directory configuration via &lt;code&gt;.htaccess&lt;/code&gt; files — features that made shared hosting possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strengths
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.htaccess&lt;/code&gt; support&lt;/strong&gt; : Per-directory configuration without server restart&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Module ecosystem&lt;/strong&gt; : Hundreds of modules (mod_php, mod_rewrite, mod_security)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared hosting&lt;/strong&gt; : Still the default on most shared hosting providers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation&lt;/strong&gt; : Decades of community knowledge and tutorials&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration Example
&lt;/h3&gt;

&lt;p&gt;Serving a static site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;VirtualHost&lt;/span&gt;&lt;span class="sr"&gt; *:80&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="nc"&gt;ServerName&lt;/span&gt; example.com
    &lt;span class="nc"&gt;DocumentRoot&lt;/span&gt; /var/www/example.com

    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;Directory&lt;/span&gt;&lt;span class="sr"&gt; /var/www/example.com&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="nc"&gt;Options&lt;/span&gt; -Indexes +FollowSymLinks
        &lt;span class="nc"&gt;AllowOverride&lt;/span&gt; &lt;span class="ss"&gt;All&lt;/span&gt;
        &lt;span class="nc"&gt;Require&lt;/span&gt; &lt;span class="ss"&gt;all&lt;/span&gt; granted
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;Directory&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;
    &lt;span class="c"&gt;# Cache static assets&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;FilesMatch&lt;/span&gt;&lt;span class="sr"&gt; "\.(css|js|png|jpg|gif|ico)$"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="nc"&gt;ExpiresActive&lt;/span&gt; &lt;span class="ss"&gt;On&lt;/span&gt;
        &lt;span class="nc"&gt;ExpiresDefault&lt;/span&gt; "access plus 30 days"
        &lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Cache-Control "public, immutable"
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;FilesMatch&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;VirtualHost&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reverse proxy to a Node.js app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;VirtualHost&lt;/span&gt;&lt;span class="sr"&gt; *:80&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="nc"&gt;ServerName&lt;/span&gt; api.example.com

    &lt;span class="nc"&gt;ProxyPreserveHost&lt;/span&gt; &lt;span class="ss"&gt;On&lt;/span&gt;
    &lt;span class="nc"&gt;ProxyPass&lt;/span&gt; / http://127.0.0.1:3000/
    &lt;span class="nc"&gt;ProxyPassReverse&lt;/span&gt; / http://127.0.0.1:3000/

    &lt;span class="nc"&gt;RequestHeader&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; X-Forwarded-Proto "http"
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;VirtualHost&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When to Use Apache
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Shared hosting environments where Nginx isn't available&lt;/li&gt;
&lt;li&gt;PHP applications that benefit from mod_php&lt;/li&gt;
&lt;li&gt;Projects that need &lt;code&gt;.htaccess&lt;/code&gt; for per-directory configuration&lt;/li&gt;
&lt;li&gt;Legacy applications with existing Apache configurations&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Caddy
&lt;/h2&gt;

&lt;p&gt;Caddy (v2, released 2020) is the newest of the three. Its headline feature is automatic HTTPS — it obtains and renews TLS certificates from Let's Encrypt without any configuration. The Caddyfile syntax is designed to be as simple as possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strengths
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automatic HTTPS&lt;/strong&gt; : Certificates obtained and renewed automatically via ACME&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple configuration&lt;/strong&gt; : A basic site takes 2-3 lines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modern defaults&lt;/strong&gt; : HTTP/2, HTTP/3, TLS 1.3 out of the box&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single binary&lt;/strong&gt; : No dependencies, easy to install and update&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration Example
&lt;/h3&gt;

&lt;p&gt;Serving a static site (with automatic HTTPS):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;example&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;www&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;
    &lt;span class="nx"&gt;file_server&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;That's it. Caddy automatically obtains a TLS certificate, redirects HTTP to HTTPS, and serves the files.&lt;/p&gt;

&lt;p&gt;Reverse proxy to a Node.js app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;reverse_proxy&lt;/span&gt; &lt;span class="mf"&gt;127.0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="err"&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;h3&gt;
  
  
  When to Use Caddy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Small-to-medium sites where simplicity matters&lt;/li&gt;
&lt;li&gt;Projects where automatic HTTPS saves significant ops time&lt;/li&gt;
&lt;li&gt;Development environments needing local HTTPS&lt;/li&gt;
&lt;li&gt;APIs and microservices with straightforward routing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance Comparison
&lt;/h2&gt;

&lt;p&gt;For static file serving and high concurrency, Nginx leads. Apache's process-per-connection model uses more memory under load. Caddy performs well but sits between the two.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Nginx&lt;/th&gt;
&lt;th&gt;Apache&lt;/th&gt;
&lt;th&gt;Caddy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Static file throughput&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Highest&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory per 10K connections&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~2.5 MB&lt;/td&gt;
&lt;td&gt;~10+ MB (prefork)&lt;/td&gt;
&lt;td&gt;~5 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrent connection handling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Good (with worker MPM)&lt;/td&gt;
&lt;td&gt;Very good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TLS handshake overhead&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low (with tuning)&lt;/td&gt;
&lt;td&gt;Low (with tuning)&lt;/td&gt;
&lt;td&gt;Low (default)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In practice, the web server is rarely the bottleneck. Your application code, database queries, and network latency have far more impact on user-perceived performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Web Server Choice Affects Deployment
&lt;/h2&gt;

&lt;p&gt;When you deploy with &lt;a href="https://deployhq.com/features" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, your code is transferred to the server — the web server configuration ships alongside it. Understanding the relationship between your application code and web server is important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Configuration files deploy with your code&lt;/strong&gt;. Your &lt;code&gt;nginx.conf&lt;/code&gt;, &lt;code&gt;.htaccess&lt;/code&gt;, or &lt;code&gt;Caddyfile&lt;/code&gt; lives in your Git repository and deploys automatically when you push from &lt;a href="https://deployhq.com/deploy-from-github" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; or &lt;a href="https://deployhq.com/deploy-from-gitlab" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server restart may be needed&lt;/strong&gt;. After deploying Nginx or Apache config changes, you often need a reload command. DeployHQ's &lt;a href="https://deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;build pipelines&lt;/a&gt; can run post-deploy commands to handle this&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The web server is already running&lt;/strong&gt;. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; deploys your application code. The web server itself is installed and managed separately on your server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For teams managing multiple client sites, having a consistent web server across projects simplifies operations. &lt;a href="https://deployhq.com/for-agencies" rel="noopener noreferrer"&gt;Agencies&lt;/a&gt; often standardize on Nginx for its performance and flexibility.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Can I run Nginx and Apache together?&lt;/strong&gt; Yes. A common pattern is Nginx as a reverse proxy in front of Apache. Nginx handles static files and SSL termination; Apache handles PHP via mod_php. This gives you the best of both worlds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Caddy production-ready?&lt;/strong&gt; Yes. Caddy v2 is stable and used in production by many companies. Its automatic certificate management actually reduces ops risk compared to manual cert renewal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which is best for WordPress?&lt;/strong&gt; Apache is the traditional choice because WordPress relies on &lt;code&gt;.htaccess&lt;/code&gt; for URL rewriting. However, Nginx works well with WordPress using a &lt;code&gt;try_files&lt;/code&gt; directive, and many high-traffic WordPress sites run on Nginx.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a web server if I'm using Node.js/Express?&lt;/strong&gt; Node.js can serve HTTP directly, but a reverse proxy (typically Nginx or Caddy) in front of it provides SSL termination, static file serving, load balancing, and protection against slow clients. For production, always use a reverse proxy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which web server is easiest to learn?&lt;/strong&gt; Caddy, by a wide margin. A functional Caddyfile can be 3 lines. Nginx has a moderate learning curve. Apache's configuration is the most verbose but extremely well-documented.&lt;/p&gt;




&lt;p&gt;Your web server is the front door to your application. Whether you choose Nginx for performance, Apache for compatibility, or Caddy for simplicity, the important thing is that your deployment pipeline handles the configuration reliably.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://deployhq.com/signup" rel="noopener noreferrer"&gt;Try&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; free&lt;/strong&gt; — deploy your web server configuration alongside your application code on every push. See &lt;a href="https://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>nginx</category>
      <category>apache</category>
      <category>webserver</category>
    </item>
    <item>
      <title>Understanding CORS: The Developer's Guide</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Mon, 30 Mar 2026 06:22:41 +0000</pubDate>
      <link>https://dev.to/deployhq/understanding-cors-the-developers-guide-5gha</link>
      <guid>https://dev.to/deployhq/understanding-cors-the-developers-guide-5gha</guid>
      <description>&lt;p&gt;If you've ever opened your browser console and seen a red error message about &lt;q&gt;CORS policy,&lt;/q&gt; you're not alone. CORS errors are one of the most common and most frustrating issues in web development — and they almost always surface after deployment, not during local development.&lt;/p&gt;

&lt;p&gt;This guide explains what CORS is, why browsers enforce it, and how to fix it in every major framework.&lt;/p&gt;

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

&lt;p&gt;CORS stands for Cross-Origin Resource Sharing. It's a security mechanism built into web browsers that controls which websites can make requests to your server.&lt;/p&gt;

&lt;p&gt;When your frontend at &lt;code&gt;https://app.example.com&lt;/code&gt; tries to fetch data from &lt;code&gt;https://api.example.com&lt;/code&gt;, the browser checks whether the API server explicitly allows requests from &lt;code&gt;app.example.com&lt;/code&gt;. If the server doesn't say yes, the browser blocks the response.&lt;/p&gt;

&lt;p&gt;This isn't your server rejecting the request — the server processes it normally. The &lt;em&gt;browser&lt;/em&gt; blocks the response from reaching your JavaScript code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Same-Origin Policy
&lt;/h2&gt;

&lt;p&gt;CORS is built on top of the Same-Origin Policy, which defines what counts as the &lt;q&gt;same origin&lt;/q&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://app.example.com:443/page
  ↑ ↑ ↑
protocol domain port

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

&lt;/div&gt;



&lt;p&gt;Two URLs have the same origin only if all three parts match:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;URL A&lt;/th&gt;
&lt;th&gt;URL B&lt;/th&gt;
&lt;th&gt;Same origin?&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://app.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://app.example.com/api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Same protocol, domain, port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://app.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://app.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Different protocol&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://app.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://api.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Different subdomain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://app.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://app.example.com:8080&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Different port&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When origins don't match, the browser requires CORS headers from the server before it will allow the response through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simple Requests vs Preflight Requests
&lt;/h2&gt;

&lt;p&gt;Not all cross-origin requests are handled the same way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simple Requests
&lt;/h3&gt;

&lt;p&gt;The browser sends the request directly if it meets all of these conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Method is &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;HEAD&lt;/code&gt;, or &lt;code&gt;POST&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Only standard headers (&lt;code&gt;Accept&lt;/code&gt;, &lt;code&gt;Content-Type&lt;/code&gt;, &lt;code&gt;Content-Language&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Content-Type&lt;/code&gt; is &lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt;, &lt;code&gt;multipart/form-data&lt;/code&gt;, or &lt;code&gt;text/plain&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For simple requests, the browser sends the request and checks the CORS headers in the response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preflight Requests
&lt;/h3&gt;

&lt;p&gt;For anything else — &lt;code&gt;PUT&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;, custom headers, &lt;code&gt;Content-Type: application/json&lt;/code&gt; — the browser sends a preflight &lt;code&gt;OPTIONS&lt;/code&gt; request first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser → OPTIONS /api/users (preflight)
Server → 200 OK + CORS headers
Browser → PUT /api/users (actual request)
Server → 200 OK + data


sequenceDiagram
    Browser-&amp;gt;&amp;gt;Server: OPTIONS /api/users (preflight)
    Server--&amp;gt;&amp;gt;Browser: 200 OK + CORS headers
    Browser-&amp;gt;&amp;gt;Server: PUT /api/users (actual request)
    Server--&amp;gt;&amp;gt;Browser: 200 OK + response data

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

&lt;/div&gt;



&lt;p&gt;The preflight asks: &lt;q&gt;Am I allowed to make this type of request?&lt;/q&gt; If the server responds with the right headers, the browser proceeds with the actual request.&lt;/p&gt;

&lt;h2&gt;
  
  
  CORS Headers Explained
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Access-Control-Allow-Origin
&lt;/h3&gt;

&lt;p&gt;The most important header. It tells the browser which origins can access the response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Access-Control-Allow-Origin: https://app.example.com

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

&lt;/div&gt;



&lt;p&gt;Or allow any origin (use with caution):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Access-Control-Allow-Origin: *

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt; : You can only specify one origin or &lt;code&gt;*&lt;/code&gt;. You cannot list multiple origins. To support multiple origins, your server must check the request's &lt;code&gt;Origin&lt;/code&gt; header and echo it back if it's in your allowed list.&lt;/p&gt;

&lt;h3&gt;
  
  
  Access-Control-Allow-Methods
&lt;/h3&gt;

&lt;p&gt;Which HTTP methods are permitted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Access-Control-Allow-Headers
&lt;/h3&gt;

&lt;p&gt;Which request headers are permitted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Access-Control-Allow-Credentials
&lt;/h3&gt;

&lt;p&gt;Whether the browser should send cookies and authentication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Access-Control-Allow-Credentials: true

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Critical&lt;/strong&gt; : When this is &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; cannot be &lt;code&gt;*&lt;/code&gt;. You must specify the exact origin.&lt;/p&gt;

&lt;h3&gt;
  
  
  Access-Control-Max-Age
&lt;/h3&gt;

&lt;p&gt;How long (in seconds) the browser caches the preflight response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Access-Control-Max-Age: 86400

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

&lt;/div&gt;



&lt;p&gt;This avoids sending a preflight &lt;code&gt;OPTIONS&lt;/code&gt; request before every actual request. Set it to 24 hours (86400) for production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Access-Control-Expose-Headers
&lt;/h3&gt;

&lt;p&gt;Which response headers JavaScript can read (beyond the basic set):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Access-Control-Expose-Headers: X-Total-Count, X-Request-Id

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to Enable CORS
&lt;/h2&gt;

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

&lt;p&gt;The &lt;code&gt;cors&lt;/code&gt; middleware is the standard approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;npm install cors


const express = require('express');
const cors = require('cors');
const app = express();

// Allow specific origin
app.use(cors({
  origin: 'https://app.example.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400
}));

// Or allow multiple origins
app.use(cors({
  origin: ['https://app.example.com', 'https://staging.example.com'],
  credentials: true
}));

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

&lt;/div&gt;



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

&lt;p&gt;Install &lt;a href="https://github.com/adamchainz/django-cors-headers" rel="noopener noreferrer"&gt;django-cors-headers&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;pip &lt;span class="nb"&gt;install &lt;/span&gt;django-cors-headers


&lt;span class="c"&gt;# settings.py&lt;/span&gt;
INSTALLED_APPS &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'corsheaders'&lt;/span&gt;,
    &lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;span class="o"&gt;]&lt;/span&gt;

MIDDLEWARE &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'corsheaders.middleware.CorsMiddleware'&lt;/span&gt;, &lt;span class="c"&gt;# Must be before CommonMiddleware&lt;/span&gt;
    &lt;span class="s1"&gt;'django.middleware.common.CommonMiddleware'&lt;/span&gt;,
    &lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;span class="o"&gt;]&lt;/span&gt;

CORS_ALLOWED_ORIGINS &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'https://app.example.com'&lt;/span&gt;,
    &lt;span class="s1"&gt;'https://staging.example.com'&lt;/span&gt;,
&lt;span class="o"&gt;]&lt;/span&gt;

CORS_ALLOW_CREDENTIALS &lt;span class="o"&gt;=&lt;/span&gt; True

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

&lt;/div&gt;



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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pip&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt; &lt;span class="nx"&gt;flask&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;cors&lt;/span&gt;


&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nx"&gt;flask&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Flask&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nx"&gt;flask_cors&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CORS&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;__name__&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;CORS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;origins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://app.example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;supports_credentials&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Ruby on Rails
&lt;/h3&gt;

&lt;p&gt;Add &lt;code&gt;rack-cors&lt;/code&gt; to your Gemfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem &lt;span class="s1"&gt;'rack-cors'&lt;/span&gt;


&lt;span class="c"&gt;# config/initializers/cors.rb&lt;/span&gt;
Rails.application.config.middleware.insert_before 0, Rack::Cors &lt;span class="k"&gt;do
  &lt;/span&gt;allow &lt;span class="k"&gt;do
    &lt;/span&gt;origins &lt;span class="s1"&gt;'https://app.example.com'&lt;/span&gt;
    resource &lt;span class="s1"&gt;'*'&lt;/span&gt;,
      headers: :any,
      methods: &lt;span class="o"&gt;[&lt;/span&gt;:get, :post, :put, :patch, :delete, :options],
      credentials: &lt;span class="nb"&gt;true&lt;/span&gt;,
      max_age: 86400
  end
end

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  PHP (Manual Headers)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;
&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;allowed_origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://app.example.com&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;HTTP_ORIGIN&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;HTTP_ORIGIN&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;allowed_origin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Origin: $allowed_origin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Methods: GET, POST, PUT, DELETE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Headers: Content-Type, Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Credentials: true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Max-Age: 86400&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;Handle&lt;/span&gt; &lt;span class="n"&gt;preflight&lt;/span&gt;
&lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;REQUEST_METHOD&lt;/span&gt;&lt;span class="sh"&gt;'&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;OPTIONS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Nginx (Server-Level)
&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;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;server_name&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;# Handle preflight
&lt;/span&gt;        &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;request_method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OPTIONS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;add_header&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://app.example.com&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;add_header&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Methods&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GET, POST, PUT, DELETE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;add_header&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Headers&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-Type, Authorization&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;add_header&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Max-Age&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;add_header&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-Length&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;add_header&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://app.example.com&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;add_header&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Credentials&lt;/span&gt;&lt;span class="sh"&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;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;proxy_pass&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="mf"&gt;127.0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common CORS Errors and Fixes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;q&gt;No 'Access-Control-Allow-Origin' header is present&lt;/q&gt;
&lt;/h3&gt;

&lt;p&gt;The server isn't sending CORS headers. Add the appropriate middleware or headers for your framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;q&gt;The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*' when the request's credentials mode is 'include'&lt;/q&gt;
&lt;/h3&gt;

&lt;p&gt;You're using &lt;code&gt;credentials: true&lt;/code&gt; (or &lt;code&gt;withCredentials: true&lt;/code&gt; in fetch/axios) but the server is responding with &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt;. You must specify the exact origin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;// Wrong
res.header&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access-Control-Allow-Origin'&lt;/span&gt;, &lt;span class="s1"&gt;'*'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

// Correct
res.header&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access-Control-Allow-Origin'&lt;/span&gt;, &lt;span class="s1"&gt;'https://app.example.com'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Preflight Failures (OPTIONS Returns 405 or 404)
&lt;/h3&gt;

&lt;p&gt;Your server doesn't handle &lt;code&gt;OPTIONS&lt;/code&gt; requests. Some frameworks need explicit configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="sr"&gt;//&lt;/span&gt; &lt;span class="no"&gt;Express&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cors&lt;/span&gt; &lt;span class="n"&gt;middleware&lt;/span&gt; &lt;span class="n"&gt;handles&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt; &lt;span class="n"&gt;automatically&lt;/span&gt;
&lt;span class="sr"&gt;//&lt;/span&gt; &lt;span class="no"&gt;Without&lt;/span&gt; &lt;span class="n"&gt;cors&lt;/span&gt; &lt;span class="n"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;add:
&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'*'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access-Control-Allow-Origin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'https://app.example.com'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access-Control-Allow-Methods'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'GET, POST, PUT, DELETE'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access-Control-Allow-Headers'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type, Authorization'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  CORS Works Locally but Fails in Production
&lt;/h3&gt;

&lt;p&gt;This is the most common deployment issue. Locally, your frontend and API often run on &lt;code&gt;localhost&lt;/code&gt; (same origin or with a dev proxy). In production, they're on different subdomains.&lt;/p&gt;

&lt;p&gt;Check that your production server's CORS configuration includes your production frontend URL, not just &lt;code&gt;localhost:3000&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  CORS in Development vs Production
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Development
&lt;/h3&gt;

&lt;p&gt;During development, CORS issues are often avoided with a dev proxy. Most frontend tools support this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vite:&lt;/strong&gt;&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="c1"&gt;// vite.config.js&lt;/span&gt;
&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s1"&gt;'/api'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'http://localhost:3000'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The proxy makes API requests appear same-origin during development, bypassing CORS entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Production
&lt;/h3&gt;

&lt;p&gt;In production, you have several options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Same origin&lt;/strong&gt; : Serve frontend and API from the same domain (no CORS needed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CORS headers&lt;/strong&gt; : Configure your API to accept requests from your frontend's origin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse proxy&lt;/strong&gt; : Use Nginx to serve both frontend and API from the same domain&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Option 3 is often the simplest. If your frontend and API are both deployed via &lt;a href="https://deployhq.com/features" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, an Nginx reverse proxy can route &lt;code&gt;/api/*&lt;/code&gt; to your backend and everything else to your frontend — making them the same origin.&lt;/p&gt;

&lt;h2&gt;
  
  
  How This Relates to Deployment
&lt;/h2&gt;

&lt;p&gt;CORS issues are deployment issues. They rarely appear during development (because of dev proxies or same-origin localhost) and almost always appear when code hits staging or production.&lt;/p&gt;

&lt;p&gt;When you deploy with &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; from &lt;a href="https://deployhq.com/deploy-from-github" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; or &lt;a href="https://deployhq.com/deploy-from-gitlab" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your CORS configuration deploys with your code&lt;/strong&gt; — it's part of your application's middleware or server config&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment-specific origins&lt;/strong&gt; can be managed via DeployHQ's &lt;a href="https://deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;build pipelines&lt;/a&gt; — set &lt;code&gt;CORS_ORIGIN&lt;/code&gt; as a build variable that differs between staging and production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nginx config deploys alongside your app&lt;/strong&gt; — your reverse proxy rules are version-controlled and auto-deployed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For &lt;a href="https://deployhq.com/for-agencies" rel="noopener noreferrer"&gt;agencies&lt;/a&gt; deploying frontend and backend separately for clients, getting CORS right from the start saves hours of debugging post-deploy.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never use &lt;code&gt;*&lt;/code&gt; in production&lt;/strong&gt; for APIs that handle authentication. It allows any website to make requests to your API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate origins&lt;/strong&gt; : Don't blindly echo the &lt;code&gt;Origin&lt;/code&gt; header. Maintain a whitelist of allowed origins&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't confuse CORS with authentication&lt;/strong&gt;. CORS controls which &lt;em&gt;websites&lt;/em&gt; can talk to your API. It doesn't replace API authentication (tokens, sessions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credentials require specific origins&lt;/strong&gt;. If you use cookies or auth headers, you must specify exact origins — &lt;code&gt;*&lt;/code&gt; is not allowed&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Does CORS apply to server-to-server requests?&lt;/strong&gt; No. CORS is a browser-only mechanism. Server-to-server HTTP requests (curl, fetch from Node.js, Python requests) are not subject to CORS restrictions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I disable CORS in the browser?&lt;/strong&gt; Yes, for testing — but never in production. Chrome can be launched with &lt;code&gt;--disable-web-security&lt;/code&gt;, but this is dangerous and only for debugging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does CORS protect my API from abuse?&lt;/strong&gt; No. CORS is enforced by browsers, not servers. Anyone can use curl or Postman to make requests directly. Use authentication and rate limiting to protect your API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does my API work in Postman but not in the browser?&lt;/strong&gt; Postman doesn't enforce CORS — it's not a browser. The same request that fails in the browser due to CORS will succeed in Postman because there's no same-origin policy to enforce.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I handle CORS in my application or in Nginx?&lt;/strong&gt; Either works. Application-level CORS (Express cors middleware, Django cors-headers) is more flexible and portable. Nginx-level CORS is useful when you can't modify the application. Don't configure CORS in both places — the headers can conflict.&lt;/p&gt;




&lt;p&gt;CORS is confusing at first, but it's simple once you understand the flow: the browser asks the server for permission, the server responds with headers, and the browser enforces the result. Get your headers right, test on staging before production, and you'll never see that red console error again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://deployhq.com/signup" rel="noopener noreferrer"&gt;Try&lt;/a&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; free&lt;/strong&gt; — deploy your frontend and backend together with consistent CORS configuration. See &lt;a href="https://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>frontend</category>
      <category>tutorials</category>
      <category>cors</category>
      <category>developers</category>
    </item>
    <item>
      <title>How To Deploy from Windows Using WSL2, Docker, and DeployHQ</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Wed, 25 Mar 2026 11:51:46 +0000</pubDate>
      <link>https://dev.to/deployhq/how-to-deploy-from-windows-using-wsl2-docker-and-deployhq-419i</link>
      <guid>https://dev.to/deployhq/how-to-deploy-from-windows-using-wsl2-docker-and-deployhq-419i</guid>
      <description>&lt;p&gt;Most deployment tutorials assume you are already on Linux or macOS. But if your daily machine runs Windows, you have likely hit the gap between &lt;q&gt;it works on my laptop&lt;/q&gt; and &lt;q&gt;it works on the server.&lt;/q&gt; WSL2 closes that gap — it gives you a real Linux kernel inside Windows, so your local Docker containers behave exactly like they will in production.&lt;/p&gt;

&lt;p&gt;This guide covers the full workflow: setting up WSL2 and Docker on Windows, structuring a project for deployment, and using &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; to ship changes from your Git repository to a remote Linux server — automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    Dev["Windows + WSL2"] --&amp;gt;|git push| Git["GitHub / GitLab"]
    Git --&amp;gt;|webhook| DHQ["DeployHQ"]
    DHQ --&amp;gt;|SSH + files| Prod["Linux VPS"]
    DHQ --&amp;gt;|SSH commands| Prod

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

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why WSL2 for deployment workflows
&lt;/h2&gt;

&lt;p&gt;If you develop on Windows and deploy to Linux servers, you have probably run into at least one of these problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Path differences&lt;/strong&gt; : Windows uses backslashes and drive letters (&lt;code&gt;C:\Users\&lt;/code&gt;), Linux uses forward slashes (&lt;code&gt;/home/&lt;/code&gt;). Build scripts and Docker volumes break when they cross this boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shell incompatibilities&lt;/strong&gt; : Bash scripts that run perfectly on the server fail in PowerShell or CMD.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File permission mismatches&lt;/strong&gt; : Linux file permissions (chmod, chown) do not exist on NTFS, causing issues with Docker volume mounts and deployment scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Line ending conflicts&lt;/strong&gt; : Windows uses &lt;code&gt;\r\n&lt;/code&gt;, Linux uses &lt;code&gt;\n&lt;/code&gt;. A single wrong line ending can break a shell script or a Dockerfile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WSL2 solves all of these by running a real Linux distribution (not an emulation layer) inside a lightweight VM managed by Windows. Docker Desktop integrates with WSL2 directly, so containers run on a real Linux kernel — identical to your production server.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1 — Install WSL2 and Ubuntu
&lt;/h2&gt;

&lt;p&gt;Open PowerShell as Administrator and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wsl &lt;span class="nt"&gt;--install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; Ubuntu-24.04

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

&lt;/div&gt;



&lt;p&gt;This installs WSL2 (the default since Windows 10 version 2004) and Ubuntu 24.04 LTS. Restart your machine when prompted.&lt;/p&gt;

&lt;p&gt;After reboot, Ubuntu will launch and ask you to create a Linux username and password. These are separate from your Windows credentials.&lt;/p&gt;

&lt;p&gt;Verify the installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wsl &lt;span class="nt"&gt;--list&lt;/span&gt; &lt;span class="nt"&gt;--verbose&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;You should see Ubuntu-24.04 running on WSL version 2.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Already have WSL1?&lt;/strong&gt; Upgrade with &lt;code&gt;wsl --set-version Ubuntu-24.04 2&lt;/code&gt;. WSL1 does not support Docker.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 2 — Install Docker Desktop with the WSL2 backend
&lt;/h2&gt;

&lt;p&gt;Download and install &lt;a href="https://www.docker.com/products/docker-desktop/" rel="noopener noreferrer"&gt;Docker Desktop for Windows&lt;/a&gt;. During setup, ensure &lt;strong&gt;Use WSL 2 based engine&lt;/strong&gt; is checked (it is the default).&lt;/p&gt;

&lt;p&gt;After installation, open Docker Desktop and go to &lt;strong&gt;Settings &amp;gt; Resources &amp;gt; WSL Integration&lt;/strong&gt;. Enable integration with your Ubuntu-24.04 distribution.&lt;/p&gt;

&lt;p&gt;Verify from your WSL2 terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nt"&gt;--version&lt;/span&gt;
docker compose version

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

&lt;/div&gt;



&lt;p&gt;Both commands should work without &lt;code&gt;sudo&lt;/code&gt;. Docker Desktop handles the daemon — you do not need to install Docker Engine inside WSL2 separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance tip
&lt;/h3&gt;

&lt;p&gt;Store your project files inside the WSL2 filesystem (&lt;code&gt;~/projects/&lt;/code&gt;), not on the Windows mount (&lt;code&gt;/mnt/c/&lt;/code&gt;). File operations on &lt;code&gt;/mnt/c/&lt;/code&gt; are &lt;a href="https://docs.docker.com/desktop/features/wsl/best-practices/" rel="noopener noreferrer"&gt;significantly slower&lt;/a&gt; due to the cross-filesystem translation layer.&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;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/projects
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/projects

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

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3 — Set up Git and SSH keys
&lt;/h2&gt;

&lt;p&gt;Your WSL2 environment has its own Git configuration and SSH keys, separate from Windows. Configure them inside WSL2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; user.name &lt;span class="s2"&gt;"Your Name"&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; user.email &lt;span class="s2"&gt;"your@email.com"&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Generate an SSH key for connecting to both your Git provider and your deployment server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"your@email.com"&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.ssh/id_ed25519.pub

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

&lt;/div&gt;



&lt;p&gt;Add this public key to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your &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; account&lt;/li&gt;
&lt;li&gt;Your remote server's &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; file&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 4 — Create a sample Docker Compose project
&lt;/h2&gt;

&lt;p&gt;Let's create a minimal project to demonstrate the full workflow. This example uses Nginx serving a static site, but the pattern works for any Docker-based application.&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; ~/projects
&lt;span class="nb"&gt;mkdir &lt;/span&gt;my-app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;my-app
git init

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

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&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;nginx:alpine&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;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;./public:/usr/share/nginx/html:ro&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Create a simple page:&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;mkdir &lt;/span&gt;public
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;h1&amp;gt;Deployed with DeployHQ from WSL2&amp;lt;/h1&amp;gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; public/index.html

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

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;.gitignore&lt;/code&gt;:&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
curl http://localhost:8080

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

&lt;/div&gt;



&lt;p&gt;You should see your HTML. Stop it with &lt;code&gt;docker compose down&lt;/code&gt;.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Initial project setup"&lt;/span&gt;
git remote add origin git@github.com:your-username/my-app.git
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main

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

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 5 — Set up DeployHQ
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; connects your Git repository to your server and deploys changes automatically — or on your approval. Here is how to wire it up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a new project&lt;/strong&gt; in &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; and connect your Git repository. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; supports &lt;a href="https://www.deployhq.com/deploy-from-github" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, &lt;a href="https://www.deployhq.com/deploy-from-gitlab" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;, Bitbucket, and any self-hosted Git server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add your server&lt;/strong&gt; as a deployment target. Choose SSH/SFTP and enter your server's IP, SSH user, and deployment path (e.g., &lt;code&gt;/home/deploy/my-app&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Configure &lt;a href="https://www.deployhq.com/support/ssh-commands" rel="noopener noreferrer"&gt;SSH commands&lt;/a&gt;&lt;/strong&gt; to manage Docker containers on each deployment:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Timing&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Before upload&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cd /home/deploy/my-app &amp;amp;&amp;amp; docker compose down&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stop running containers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;After upload&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cd /home/deploy/my-app &amp;amp;&amp;amp; docker compose up -d --remove-orphans&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start updated containers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; runs SSH commands from the user's &lt;code&gt;$HOME&lt;/code&gt; by default, so the &lt;code&gt;cd&lt;/code&gt; prefix is required. See the &lt;a href="https://www.deployhq.com/support/deployments/deployment-commands-examples" rel="noopener noreferrer"&gt;deployment command examples&lt;/a&gt; for more patterns.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enable automatic deployments&lt;/strong&gt; (optional): Under project settings, enable auto-deploy so every push to &lt;code&gt;main&lt;/code&gt; triggers a deployment without manual approval.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 6 — Deploy and verify
&lt;/h2&gt;

&lt;p&gt;From your WSL2 terminal, make a change and push:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;h1&amp;gt;Updated from WSL2!&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;Deployed automatically.&amp;lt;/p&amp;gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; public/index.html
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Update landing page"&lt;/span&gt;
git push

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

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detect the new commit via webhook&lt;/li&gt;
&lt;li&gt;Upload changed files to your server&lt;/li&gt;
&lt;li&gt;Run the SSH commands to restart Docker containers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Check the deployment log in your &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ dashboard&lt;/a&gt; to confirm success.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7 — Edit with VS Code from Windows
&lt;/h2&gt;

&lt;p&gt;One of the best parts of WSL2: you can edit files inside the Linux filesystem using VS Code on Windows. Install the &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl" rel="noopener noreferrer"&gt;WSL extension&lt;/a&gt;, then from your WSL2 terminal:&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; ~/projects/my-app
code &lt;span class="nb"&gt;.&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;VS Code opens on Windows but operates inside WSL2 — file access, terminal, Git, and Docker commands all run natively in Linux. Your workflow becomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Edit&lt;/strong&gt; in VS Code on Windows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test&lt;/strong&gt; with &lt;code&gt;docker compose up&lt;/code&gt; in the integrated WSL2 terminal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit and push&lt;/strong&gt; from the same terminal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DeployHQ deploys&lt;/strong&gt; to your Linux server automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No context switching. No path translation issues. No &lt;q&gt;works on my machine&lt;/q&gt; surprises.&lt;/p&gt;




&lt;h2&gt;
  
  
  Going further: real-world project patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Environment-specific configuration
&lt;/h3&gt;

&lt;p&gt;Use separate Compose files for local development and production:&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;# Local development (with hot reload, debug ports)&lt;/span&gt;
docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.yml &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.dev.yml up

&lt;span class="c"&gt;# Production uses the base file only&lt;/span&gt;
&lt;span class="c"&gt;# DeployHQ deploys docker-compose.yml to the server&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using DeployHQ build pipelines
&lt;/h3&gt;

&lt;p&gt;If your project needs a build step (compiling assets, running tests), &lt;a href="https://www.deployhq.com/blog/build-pipelines-in-deployhq-streamline-your-deployment-workflow" rel="noopener noreferrer"&gt;DeployHQ build pipelines&lt;/a&gt; can run commands before deploying. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build

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

&lt;/div&gt;



&lt;p&gt;This runs on DeployHQ's build servers, so your production server only receives the compiled output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker image builds with DeployHQ
&lt;/h3&gt;

&lt;p&gt;For projects that build custom Docker images, DeployHQ's &lt;a href="https://changelog.deployhq.com/p/docker-support-build-and-container-image-repository" rel="noopener noreferrer"&gt;Docker build feature&lt;/a&gt; can build and push images to Docker Hub, GitHub Container Registry, or other registries — then deploy a &lt;code&gt;docker compose pull &amp;amp;&amp;amp; docker compose up -d&lt;/code&gt; on your server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rolling back
&lt;/h3&gt;

&lt;p&gt;If a deployment breaks something, open DeployHQ's deployment history and click &lt;strong&gt;Redeploy this version&lt;/strong&gt; on the last working deployment. The SSH commands will restart the containers with the previous configuration.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Docker commands not available in WSL2&lt;/strong&gt; Open Docker Desktop &amp;gt; Settings &amp;gt; Resources &amp;gt; WSL Integration and ensure your Ubuntu distro is enabled. Then restart your WSL2 terminal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slow file access in Docker volumes&lt;/strong&gt; You are likely mounting from &lt;code&gt;/mnt/c/&lt;/code&gt;. Move your project to &lt;code&gt;~/projects/&lt;/code&gt; inside WSL2. The performance difference is &lt;a href="https://docs.docker.com/desktop/features/wsl/best-practices/" rel="noopener noreferrer"&gt;substantial&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSH key not working with &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; or your server&lt;/strong&gt;Make sure you generated the key inside WSL2 (not Windows). Check permissions: &lt;code&gt;chmod 600 ~/.ssh/id_ed25519&lt;/code&gt;. Test with &lt;code&gt;ssh -T git@github.com&lt;/code&gt; or &lt;code&gt;ssh deploy@your-server-ip&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Line ending issues in Git&lt;/strong&gt; Configure Git inside WSL2 to keep Linux line endings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; core.autocrlf input

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

&lt;/div&gt;



&lt;p&gt;This ensures files stay with &lt;code&gt;\n&lt;/code&gt; endings even if you occasionally edit them from Windows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DeployHQ deployment times out&lt;/strong&gt; Docker image pulls can be slow on the first deployment. Increase the SSH command timeout in DeployHQ's server settings, or pre-pull images on the server with &lt;code&gt;docker compose pull&lt;/code&gt; before the first deploy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I use WSL2 without Docker Desktop?
&lt;/h3&gt;

&lt;p&gt;Yes. You can install Docker Engine directly inside your WSL2 Ubuntu distribution using the &lt;a href="https://docs.docker.com/engine/install/ubuntu/" rel="noopener noreferrer"&gt;official Linux install steps&lt;/a&gt;. This avoids Docker Desktop entirely and is free for all team sizes. The trade-off is that you lose the GUI, automatic updates, and the seamless cross-distro integration that Docker Desktop provides. The deployment workflow with &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; works exactly the same either way — it only cares about what you push to Git.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does DeployHQ run on Windows or inside WSL2?
&lt;/h3&gt;

&lt;p&gt;Neither. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; is a hosted service — it runs in the cloud. You interact with it through your browser or via Git webhooks. Your local environment (Windows, WSL2, macOS, Linux) only matters for writing code and pushing commits. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; connects to your Git provider and your remote server independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is WSL2 fast enough for Docker development?
&lt;/h3&gt;

&lt;p&gt;For containers and builds, WSL2 performs at near-native Linux speed. The one bottleneck is cross-filesystem access — reading files from &lt;code&gt;/mnt/c/&lt;/code&gt; (the Windows drive) inside a container is significantly slower than reading from the native Linux filesystem (&lt;code&gt;~/&lt;/code&gt;). Keep your projects inside WSL2's home directory and performance will not be an issue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I deploy to multiple servers from the same project?
&lt;/h3&gt;

&lt;p&gt;Yes. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; supports &lt;a href="https://www.deployhq.com/support/servers/advanced-server-configuration" rel="noopener noreferrer"&gt;multiple servers per project&lt;/a&gt;. You can deploy the same Git repository to a staging server and a production server with different SSH commands for each. For example, staging could auto-deploy on every push, while production requires manual approval.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if my team uses a mix of Windows, macOS, and Linux?
&lt;/h3&gt;

&lt;p&gt;That is one of the main advantages of this workflow. Docker Compose files are cross-platform, Git is cross-platform, and &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; does not care what OS pushed the commit. Team members on macOS or Linux skip the WSL2 step and use Docker natively — the rest of the workflow (Git push → &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; → server) is identical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need WSL2 if I only deploy static sites or PHP apps (no Docker)?
&lt;/h3&gt;

&lt;p&gt;No. WSL2 and Docker are optional. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; can deploy any Git repository to any server over SSH/SFTP — no containers needed. WSL2 becomes valuable when your production stack uses Docker, because it lets you run the same containers locally on Windows that you will run on the server. For static sites or traditional PHP deployments, &lt;a href="https://www.deployhq.com/support" rel="noopener noreferrer"&gt;DeployHQ works directly&lt;/a&gt; without any local Docker setup.&lt;/p&gt;




&lt;p&gt;Ready to bridge your Windows development environment with production Linux servers? &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Sign up for DeployHQ&lt;/a&gt; and connect your first project in minutes.&lt;/p&gt;

&lt;p&gt;For questions or help, reach out to &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or find us on &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;X (@deployhq)&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>windows</category>
      <category>wsl2</category>
    </item>
    <item>
      <title>Heroku Enters Sustaining Engineering Mode: What It Means and When to Consider Alternatives</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Mon, 23 Mar 2026 05:24:10 +0000</pubDate>
      <link>https://dev.to/deployhq/heroku-enters-sustaining-engineering-mode-what-it-means-and-when-to-consider-alternatives-2kb5</link>
      <guid>https://dev.to/deployhq/heroku-enters-sustaining-engineering-mode-what-it-means-and-when-to-consider-alternatives-2kb5</guid>
      <description>&lt;p&gt;In February 2026, Salesforce &lt;a href="https://www.heroku.com/blog/an-update-on-heroku/" rel="noopener noreferrer"&gt;announced&lt;/a&gt; that Heroku is transitioning to a sustaining engineering model. No new features, no new Enterprise contracts for new customers, and a strategic pivot toward AI-driven products instead of platform development.&lt;/p&gt;

&lt;p&gt;For the thousands of teams that built their deployment workflows around Heroku, this raises a practical question: what now?&lt;/p&gt;

&lt;p&gt;This article breaks down what the announcement actually means, who should consider migrating, and how &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; fits as a deployment layer — especially if you deploy to your own servers or cloud infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Heroku's &lt;q&gt;Sustaining Engineering&lt;/q&gt; Actually Means
&lt;/h2&gt;

&lt;p&gt;Heroku's &lt;a href="https://www.heroku.com/blog/an-update-on-heroku/" rel="noopener noreferrer"&gt;official update&lt;/a&gt; and &lt;a href="https://www.heroku.com/blog/march-2026-update/" rel="noopener noreferrer"&gt;March 2026 follow-up&lt;/a&gt; confirm the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No new feature development.&lt;/strong&gt; The roadmap is defensive — security patches, stability fixes, and infrastructure maintenance only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise contracts are no longer offered to new customers.&lt;/strong&gt; Existing Enterprise customers can renew, but Salesforce is not onboarding new ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credit card customers are unaffected for now.&lt;/strong&gt; If you pay via the Heroku dashboard, pricing and billing remain the same.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No sunset date announced.&lt;/strong&gt; Heroku could run in this mode for years, but long-term certainty decreases over time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://www.heroku.com/blog/march-2026-update/" rel="noopener noreferrer"&gt;March 2026 update&lt;/a&gt; framed this as a mission to &lt;q&gt;provide the most stable, secure, and reliable environment for your apps and data&lt;/q&gt; — but conspicuously dropped any mention of innovation or feature roadmap.&lt;/p&gt;

&lt;p&gt;In software terms, &lt;q&gt;sustaining engineering&lt;/q&gt; is the phase before end-of-life. It does not mean Heroku is shutting down tomorrow. It means the platform has stopped evolving, and teams planning for the next 2-5 years should factor that into their infrastructure decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Should Consider Moving — And Who Shouldn't
&lt;/h2&gt;

&lt;p&gt;Not every Heroku customer needs to migrate immediately. Here is a practical framework:&lt;/p&gt;

&lt;h3&gt;
  
  
  Stay on Heroku (for now) if:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Your app is stable, low-traffic, and doesn't require new platform features&lt;/li&gt;
&lt;li&gt;You are on a credit card plan with no Enterprise dependencies&lt;/li&gt;
&lt;li&gt;You have no compliance requirements that demand an active development roadmap from your platform vendor&lt;/li&gt;
&lt;li&gt;Migration cost exceeds the risk of remaining on a sustaining platform&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Start planning a migration if:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You need features Heroku will never ship (private networking, advanced build controls, regional deployment)&lt;/li&gt;
&lt;li&gt;You are an Enterprise customer whose contract renewal terms may change&lt;/li&gt;
&lt;li&gt;Your compliance or security team requires vendors with active development commitments&lt;/li&gt;
&lt;li&gt;You are scaling and hitting Heroku's known limitations (30-second request timeout, daily dyno restarts, limited build customisation)&lt;/li&gt;
&lt;li&gt;You deploy to your own servers, VPS, or cloud infrastructure — Heroku was always an awkward fit for this&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why DeployHQ Is Worth Considering
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; is not a Heroku replacement in the traditional PaaS sense. It does not host your application or manage your infrastructure. Instead, it handles one thing exceptionally well: &lt;strong&gt;getting your code from a Git repository to your servers, reliably and automatically&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This distinction matters. Many Heroku users — particularly those deploying PHP, Ruby, Python, or Node.js applications — do not actually need a full PaaS. They need a deployment pipeline that connects to their existing servers.&lt;/p&gt;

&lt;p&gt;Here is where &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; fits:&lt;/p&gt;

&lt;h3&gt;
  
  
  Git-Native Deployment
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; connects directly to &lt;a href="https://www.deployhq.com/deploy-from-github" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, &lt;a href="https://www.deployhq.com/deploy-from-gitlab" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;, and &lt;a href="https://www.deployhq.com/deploy-from-bitbucket" rel="noopener noreferrer"&gt;Bitbucket&lt;/a&gt;. Push to a branch and your code is deployed — similar to Heroku's &lt;code&gt;git push heroku main&lt;/code&gt; workflow, but to any server you control.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zero Downtime Deployments
&lt;/h3&gt;

&lt;p&gt;Heroku handles this behind the scenes with its dyno management. With &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, &lt;a href="https://www.deployhq.com/features/zero-downtime-deployments" rel="noopener noreferrer"&gt;zero downtime deployments&lt;/a&gt; work by maintaining atomic release directories on your server. New releases are prepared in isolation, symlinked on success, and rolled back instantly on failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  One-Click Rollback
&lt;/h3&gt;

&lt;p&gt;One of Heroku's best features was easy rollbacks. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; provides &lt;a href="https://www.deployhq.com/features/one-click-rollback" rel="noopener noreferrer"&gt;one-click rollback&lt;/a&gt; to any previous deployment — no CLI commands, no release slugs to manage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build Pipelines
&lt;/h3&gt;

&lt;p&gt;If your application needs a build step (compiling assets, installing dependencies, running tests), DeployHQ's &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;build pipelines&lt;/a&gt; run these steps before deployment. Similar to Heroku's buildpacks, but configurable to match your exact workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy Behind Firewalls
&lt;/h3&gt;

&lt;p&gt;Unlike Heroku, which is limited to its own infrastructure, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; can &lt;a href="https://www.deployhq.com/features/deploy-behind-firewalls" rel="noopener noreferrer"&gt;deploy behind firewalls&lt;/a&gt; using network agents — reaching servers in private networks, corporate environments, or air-gapped infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Builds
&lt;/h3&gt;

&lt;p&gt;For teams moving toward containers, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; supports &lt;a href="https://www.deployhq.com/features/docker-builds" rel="noopener noreferrer"&gt;Docker builds&lt;/a&gt; as part of the deployment pipeline — build, test, and deploy container images to any registry or server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Checklist: Heroku to DeployHQ
&lt;/h2&gt;

&lt;p&gt;If you decide to move, here is a practical migration path:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Audit Your Heroku Setup
&lt;/h3&gt;

&lt;p&gt;Before changing anything, document what you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Application type&lt;/strong&gt; : Web app, API, worker process, or scheduled job?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language/framework&lt;/strong&gt; : PHP, Ruby, Python, Node.js, Go?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add-ons&lt;/strong&gt; : Which Heroku add-ons do you depend on? (Postgres, Redis, logging, monitoring)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables&lt;/strong&gt; : Export your config vars (&lt;code&gt;heroku config -s &amp;gt; .env&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build process&lt;/strong&gt; : What does your Procfile and buildpack do?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Provision Your Server
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; deploys to servers you control. You will need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A VPS or cloud server (DigitalOcean, AWS EC2, Linode, Hetzner, or your own hardware)&lt;/li&gt;
&lt;li&gt;SSH access configured&lt;/li&gt;
&lt;li&gt;Your runtime environment installed (PHP, Node.js, Ruby, Python)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For teams deploying WordPress, Laravel, Rails, Django, or Express apps to a VPS, this is a natural fit. DeployHQ's &lt;a href="https://www.deployhq.com/features/deployment-targets" rel="noopener noreferrer"&gt;deployment targets&lt;/a&gt; support SSH/SFTP, Amazon S3, Rackspace Cloud Files, and more.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Set Up DeployHQ
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Sign up for DeployHQ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Create a project and connect your Git repository&lt;/li&gt;
&lt;li&gt;Configure your server as a deployment target&lt;/li&gt;
&lt;li&gt;Set up environment-specific &lt;a href="https://www.deployhq.com/support/config-files" rel="noopener noreferrer"&gt;config files&lt;/a&gt; to replace Heroku's config vars&lt;/li&gt;
&lt;li&gt;Add &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;build pipeline&lt;/a&gt; steps if needed (e.g., &lt;code&gt;npm install &amp;amp;&amp;amp; npm run build&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Configure &lt;a href="https://www.deployhq.com/features/automatic-deployments" rel="noopener noreferrer"&gt;automatic deployments&lt;/a&gt; to trigger on push&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  4. Migrate Your Data
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; : Export with &lt;code&gt;pg_dump&lt;/code&gt;, import to your new database server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt; : Export with &lt;code&gt;redis-cli --rdb&lt;/code&gt; or use &lt;code&gt;BGSAVE&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File storage&lt;/strong&gt; : If you used ephemeral storage on Heroku, move to S3 or equivalent&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Update DNS and Go Live
&lt;/h3&gt;

&lt;p&gt;Point your domain to your new server, run smoke tests, and monitor with your preferred observability stack. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; integrates with &lt;a href="https://www.deployhq.com/features/integrations" rel="noopener noreferrer"&gt;Slack, Discord, and other notification services&lt;/a&gt; to keep your team informed during the transition.&lt;/p&gt;

&lt;h2&gt;
  
  
  DeployHQ vs Heroku: A Practical 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;Heroku&lt;/th&gt;
&lt;th&gt;DeployHQ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What it does&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hosts and runs your app (PaaS)&lt;/td&gt;
&lt;td&gt;Deploys code to your servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Server management&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fully managed&lt;/td&gt;
&lt;td&gt;You manage your servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Git push deploys&lt;/strong&gt;&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;&lt;strong&gt;Rollback&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Via CLI (&lt;code&gt;heroku rollback&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;One-click in dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build steps&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Buildpacks (limited customisation)&lt;/td&gt;
&lt;td&gt;Fully configurable build pipelines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Zero downtime&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Preboot (paid plans)&lt;/td&gt;
&lt;td&gt;Included on all plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deploy to own servers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (SSH, SFTP, S3, and more)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Private network support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (via network agents)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Container registry&lt;/td&gt;
&lt;td&gt;Docker build pipelines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Integrations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Heroku add-ons marketplace&lt;/td&gt;
&lt;td&gt;GitHub, GitLab, Bitbucket, Slack, Sentry, and more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pricing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Starts at $5/mo per dyno&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.deployhq.com/pricing" rel="noopener noreferrer"&gt;Starts at $4/mo&lt;/a&gt; for 10 projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Active development&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sustaining engineering only&lt;/td&gt;
&lt;td&gt;Active feature development&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What DeployHQ Does Not Replace
&lt;/h2&gt;

&lt;p&gt;To be transparent: &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; is not a drop-in Heroku replacement for every use case.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App hosting&lt;/strong&gt; : &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; does not run your application. You need your own server or hosting provider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed databases&lt;/strong&gt; : Heroku Postgres was convenient. You will need to self-manage or use a managed database service (AWS RDS, DigitalOcean Managed Databases, PlanetScale, Neon).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add-on marketplace&lt;/strong&gt; : Heroku's add-on ecosystem has no direct equivalent. You will integrate services directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaling&lt;/strong&gt; : Heroku's dyno scaling is automatic. With your own servers, you handle scaling yourself (or use auto-scaling from your cloud provider).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For teams that want the full PaaS experience, platforms like Render, Railway, or Fly.io are closer Heroku replacements. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; targets a different — and arguably larger — audience: &lt;strong&gt;developers who already have servers and need a reliable way to deploy to them&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;Heroku's move to sustaining engineering is part of a broader industry trend. Salesforce acquired Heroku in 2010 for $212 million, and the platform defined a generation of developer experience. But priorities shift, and Salesforce's investment is now directed toward enterprise AI.&lt;/p&gt;

&lt;p&gt;For developers, the lesson is practical: avoid single points of failure in your deployment infrastructure. Whether you stay on Heroku for now or migrate to a combination of your own servers and a &lt;a href="https://dev.to/deployhq/best-software-deployment-tools-in-2026-3aaa-temp-slug-4453201"&gt;deployment tool like DeployHQ&lt;/a&gt;, the goal is the same — control over how and where your code runs.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Ready to take control of your deployments?&lt;/strong&gt; &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Start a free trial of DeployHQ&lt;/a&gt; and deploy your first project in under 5 minutes. If you have questions about migrating from Heroku, reach out to us at &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or on Twitter at &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;@deployhq&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>news</category>
      <category>heroku</category>
    </item>
    <item>
      <title>Introducing the DeployHQ Chrome Extension: Deploy Without Leaving Your Browser</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Wed, 18 Mar 2026 07:43:07 +0000</pubDate>
      <link>https://dev.to/deployhq/introducing-the-deployhq-chrome-extension-deploy-without-leaving-your-browser-2abp</link>
      <guid>https://dev.to/deployhq/introducing-the-deployhq-chrome-extension-deploy-without-leaving-your-browser-2abp</guid>
      <description>&lt;p&gt;If you deploy web applications, you know the routine: switch to your deployment platform, find the right project, pick the branch, and hit deploy. It works, but it pulls you out of your flow every single time.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.deployhq.com/support/chrome-extension" rel="noopener noreferrer"&gt;DeployHQ Chrome Extension&lt;/a&gt; eliminates that context switch entirely. It brings deployment controls directly into GitHub, GitLab, and Bitbucket — the platforms where you already review and merge code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Does the Chrome Extension Do?
&lt;/h2&gt;

&lt;p&gt;The extension adds a &lt;strong&gt;&lt;q&gt;Deploy with DeployHQ&lt;/q&gt;&lt;/strong&gt; button to your repository pages on GitHub, GitLab, and Bitbucket. Instead of navigating to a separate deployment dashboard, you trigger deployments from the same page where you just merged a pull request or pushed a commit.&lt;/p&gt;

&lt;p&gt;Here's what it covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One-click deploys&lt;/strong&gt; from repository root pages, branch pages, and pull/merge request pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project dashboard&lt;/strong&gt; with a searchable list of your &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; projects and their latest deployment timestamps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server and branch selection&lt;/strong&gt; so you can target specific environments before deploying&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time notifications&lt;/strong&gt; via desktop alerts when deployments start, succeed, or fail&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Badge indicators&lt;/strong&gt; on the extension icon — blue for active deployments, red for failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a repository isn't connected to &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; yet, the extension shows a &lt;strong&gt;&lt;q&gt;Connect to DeployHQ&lt;/q&gt;&lt;/strong&gt; link instead, making it easy to set up new projects without leaving your browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters for Your Workflow
&lt;/h2&gt;

&lt;p&gt;Most deployment friction isn't about the deploy itself — it's about the interruption. You finish a code review, approve the PR, merge it, and then... open a new tab, log in to your deployment tool, navigate to the right project, and trigger the deploy manually.&lt;/p&gt;

&lt;p&gt;With the Chrome Extension, the deploy button is right there on the pull request page. Merge and deploy in one smooth motion.&lt;/p&gt;

&lt;p&gt;This is especially useful if you practice &lt;a href="https://dev.to/deployhq/continuous-delivery-vs-continuous-deployment-whats-the-difference-ig-temp-slug-3043658"&gt;continuous delivery&lt;/a&gt; — where every merged change should reach staging or production quickly. The fewer steps between merge and deploy, the faster your feedback loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Visit the &lt;a href="https://chromewebstore.google.com/detail/deployhq/oigfcfnijljhhenakancnahkjhfhgdjn" rel="noopener noreferrer"&gt;Chrome Web Store listing&lt;/a&gt; and click &lt;strong&gt;Add to Chrome&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Confirm the installation when prompted&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The extension needs permissions for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt; — to save your credentials locally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notifications&lt;/strong&gt; — to alert you about deployment status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Site access&lt;/strong&gt; — to deployhq.com, github.com, gitlab.com, and bitbucket.org&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration
&lt;/h3&gt;

&lt;p&gt;Click the extension icon in your browser toolbar and enter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; subdomain&lt;/strong&gt; (e.g., &lt;code&gt;mycompany&lt;/code&gt; if your account is at &lt;code&gt;mycompany.deployhq.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your email address&lt;/strong&gt; associated with the account&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your API key&lt;/strong&gt; — find this in your &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; account under &lt;a href="https://www.deployhq.com/support/api" rel="noopener noreferrer"&gt;Settings &amp;gt; Security&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your credentials are stored locally in your browser and are never sent to any third party.&lt;/p&gt;

&lt;h3&gt;
  
  
  Customisation
&lt;/h3&gt;

&lt;p&gt;The extension offers a few configurable options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Polling interval&lt;/strong&gt; — how often it checks for deployment updates (default: 60 seconds)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notifications&lt;/strong&gt; — toggle desktop alerts on or off&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform integrations&lt;/strong&gt; — enable or disable the deploy button on GitHub, GitLab, or Bitbucket individually&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How It Fits with the Rest of DeployHQ
&lt;/h2&gt;

&lt;p&gt;The Chrome Extension is one of several ways to interact with &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; beyond the web dashboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.deployhq.com/support/api" rel="noopener noreferrer"&gt;API&lt;/a&gt;&lt;/strong&gt; — full programmatic access for custom integrations and automation scripts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.deployhq.com/support/cli" rel="noopener noreferrer"&gt;CLI Tool&lt;/a&gt;&lt;/strong&gt; — trigger and manage deployments from your terminal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.deployhq.com/support/mcp-server" rel="noopener noreferrer"&gt;MCP Server&lt;/a&gt;&lt;/strong&gt; — integrate &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; with AI coding assistants like Claude Code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.deployhq.com/features/integrations" rel="noopener noreferrer"&gt;Integrations&lt;/a&gt;&lt;/strong&gt; — connect with Slack, Discord, Sentry, and other tools in your stack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you prefer a browser-based workflow, a terminal-first approach, or AI-assisted automation, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; meets you where you work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open Source
&lt;/h2&gt;

&lt;p&gt;The Chrome Extension is fully open source. You can browse the code, report issues, or contribute improvements on the &lt;a href="https://github.com/deployhq/deployhq-chrome-extension" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you spot a bug or have a feature request, opening an issue there is the fastest way to get it addressed.&lt;/p&gt;

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

&lt;p&gt;If you're already using &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, install the extension and shave a few seconds off every deployment. Those seconds add up — especially on teams that deploy multiple times a day.&lt;/p&gt;

&lt;p&gt;If you haven't tried &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; yet, &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;sign up for a free account&lt;/a&gt; and experience how simple deployment can be — from &lt;a href="https://www.deployhq.com/deploy-from-github" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, &lt;a href="https://www.deployhq.com/deploy-from-gitlab" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;, or any Git repository.&lt;/p&gt;




&lt;p&gt;Have questions or feedback? Reach out to &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/Twitter&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>launches</category>
      <category>newfeatures</category>
      <category>chromeextension</category>
      <category>deployhq</category>
    </item>
    <item>
      <title>Continuous Delivery vs Continuous Deployment: What's the Difference?</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Fri, 13 Mar 2026 11:39:07 +0000</pubDate>
      <link>https://dev.to/deployhq/continuous-delivery-vs-continuous-deployment-whats-the-difference-3p2b</link>
      <guid>https://dev.to/deployhq/continuous-delivery-vs-continuous-deployment-whats-the-difference-3p2b</guid>
      <description>&lt;p&gt;Continuous delivery and continuous deployment sound almost identical, and most teams use the terms interchangeably. That confusion is costly. The difference between them determines who (or what) decides when your code reaches production, and getting that decision wrong can mean either unnecessary bottlenecks or uncontrolled releases hitting your users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick definitions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Continuous delivery&lt;/strong&gt; means every code change is built, tested, and packaged into a release artifact that &lt;em&gt;could&lt;/em&gt; go to production at any moment. The pipeline does everything except the final step: a human decides when to press the deploy button. Your code is always in a deployable state, but deployment remains a deliberate business decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Continuous deployment&lt;/strong&gt; takes that one step further. Every change that passes the full automated test suite is deployed to production automatically, with no human gate. There is no deploy button. If the tests pass, your users see the change within minutes.&lt;/p&gt;

&lt;p&gt;The distinction comes down to a single question: &lt;strong&gt;is there a human approval step before production, or not?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[Code Commit] --&amp;gt; B[Build &amp;amp; Unit Tests]
    B --&amp;gt; C[Integration Tests]
    C --&amp;gt; D[Staging Deploy]
    D --&amp;gt; E{Human Approval?}
    E -- "Yes: Continuous Delivery" --&amp;gt; F[Manual Production Deploy]
    E -- "No: Continuous Deployment" --&amp;gt; G[Automatic Production Deploy]

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

&lt;/div&gt;






&lt;h2&gt;
  
  
  How they relate to continuous integration
&lt;/h2&gt;

&lt;p&gt;Neither continuous delivery nor continuous deployment works without continuous integration (CI) as the foundation. CI is the practice of merging code changes frequently and running automated builds and tests on every merge. It catches integration issues early and keeps the main branch in a working state.&lt;/p&gt;

&lt;p&gt;Think of it as a spectrum:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Continuous integration&lt;/strong&gt; --- automated build and test on every commit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuous delivery&lt;/strong&gt; --- CI + automated release pipeline + manual deploy gate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuous deployment&lt;/strong&gt; --- CI + automated release pipeline + automatic deploy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each level builds on the previous one. You cannot skip straight to continuous deployment without first having solid CI and a reliable delivery pipeline. For a deeper look at how these pieces fit together, see &lt;a href="https://www.deployhq.com/blog/what-is-ci-cd" rel="noopener noreferrer"&gt;What is CI/CD?&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    CI["Continuous Integration&amp;lt;br/&amp;gt;Build + Test on every commit"] --&amp;gt; CD_DELIVERY["Continuous Delivery&amp;lt;br/&amp;gt;+ Release pipeline&amp;lt;br/&amp;gt;+ Manual deploy gate"]
    CD_DELIVERY --&amp;gt; CD_DEPLOY["Continuous Deployment&amp;lt;br/&amp;gt;+ Automatic production deploy"]

    style CI fill:#e8f4f8,stroke:#2196F3
    style CD_DELIVERY fill:#fff3e0,stroke:#FF9800
    style CD_DEPLOY fill:#e8f5e9,stroke:#4CAF50

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

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key differences at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Continuous Delivery&lt;/th&gt;
&lt;th&gt;Continuous Deployment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment trigger&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual approval&lt;/td&gt;
&lt;td&gt;Automatic on passing tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Risk tolerance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lower --- human checkpoint catches edge cases&lt;/td&gt;
&lt;td&gt;Higher --- trusts the automated test suite completely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Release cadence&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;On-demand, when the business decides&lt;/td&gt;
&lt;td&gt;Every passing change, potentially dozens per day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Test requirements&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Very high --- tests are the &lt;em&gt;only&lt;/em&gt; gate to production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rollback strategy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pre-deployment review catches most issues&lt;/td&gt;
&lt;td&gt;Must have automated rollback and monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Regulated industries, complex coordinated releases&lt;/td&gt;
&lt;td&gt;SaaS products, web apps, fast-iteration teams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Team maturity needed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Moderate --- need solid CI and release pipeline&lt;/td&gt;
&lt;td&gt;High --- need comprehensive tests, monitoring, and incident response&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The practical effect: with continuous delivery, a deploy to production is a one-click action that happens when someone decides the time is right. With continuous deployment, there is no &lt;q&gt;deploy action&lt;/q&gt; at all --- production updates are a side effect of merging code.&lt;/p&gt;




&lt;h2&gt;
  
  
  When continuous delivery is the right choice
&lt;/h2&gt;

&lt;p&gt;Continuous delivery is not the &lt;q&gt;lesser&lt;/q&gt; option. For many teams, it is the correct and deliberate choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regulated industries demand it.&lt;/strong&gt; If you are shipping software for healthcare, financial services, or government systems, compliance frameworks often require documented human approval before production changes. A payment processor cannot auto-deploy changes to transaction-handling code without a sign-off. Continuous delivery gives you the speed of an automated pipeline while preserving the audit trail regulators expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coordinated releases need it.&lt;/strong&gt; When a release involves database migrations, API version bumps, third-party integrations, or marketing launches, you need to control timing. Continuous delivery lets the pipeline prepare everything, then a team lead triggers the deploy when all the pieces are aligned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incomplete test coverage warrants it.&lt;/strong&gt; If your automated test suite does not cover critical edge cases --- and honestly, most test suites have gaps --- a human review step before production is a reasonable safety net. The goal is to close those gaps over time, but in the meantime, delivery gives you a responsible middle ground.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Business timing matters.&lt;/strong&gt; Sometimes you do not want changes going live on a Friday afternoon, during a peak traffic window, or before a coordinated announcement. Continuous delivery decouples &lt;q&gt;ready to ship&lt;/q&gt; from &lt;q&gt;shipped.&lt;/q&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: fintech startup
&lt;/h3&gt;

&lt;p&gt;A payments team has 40 microservices. Their core transaction services use continuous delivery: every merge to main triggers the full pipeline and deploys to staging automatically, but production deploys require a senior engineer to approve in the deployment dashboard. Their internal tooling services, which are lower risk, use continuous deployment. This mixed approach balances speed with the compliance requirements their banking partners enforce.&lt;/p&gt;




&lt;h2&gt;
  
  
  When continuous deployment makes sense
&lt;/h2&gt;

&lt;p&gt;Continuous deployment is not reckless --- it is a sign that your engineering practices have matured to the point where you trust your automated systems more than manual review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SaaS products thrive on it.&lt;/strong&gt; When your product is a web application with thousands of users, shipping small changes frequently reduces the blast radius of any single deploy. A five-line CSS fix and a critical security patch go through the same pipeline. Both are live within minutes of merging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comprehensive test suites enable it.&lt;/strong&gt; If your team has invested in unit tests, integration tests, end-to-end tests, contract tests, and performance benchmarks that collectively cover the critical paths, those tests are a more reliable gate than a tired engineer reviewing a pull request at 4pm. Continuous deployment works when you trust the tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feature flags provide a safety net.&lt;/strong&gt; Continuous deployment does not mean every user sees every change immediately. Teams using &lt;a href="https://www.deployhq.com/blog/what-are-feature-flags" rel="noopener noreferrer"&gt;feature flags&lt;/a&gt; can deploy code to production without activating it, then gradually roll out to a percentage of users. If something breaks, disable the flag --- no rollback needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Microservices architectures benefit.&lt;/strong&gt; When services are independently deployable, continuous deployment per service is natural. Each team owns their deploy pipeline and cadence. A change to the notification service does not need to coordinate with the billing service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: SaaS startup
&lt;/h3&gt;

&lt;p&gt;A 15-person engineering team runs a project management SaaS. They merge to main 20-30 times per day. Every merge triggers the pipeline: build, 2,000+ automated tests, deploy to canary, monitor error rates for 5 minutes, then promote to full production. Average time from merge to live: 12 minutes. They find and fix issues faster than teams that batch releases weekly because each deploy is small and easy to reason about.&lt;/p&gt;




&lt;h2&gt;
  
  
  The hybrid approach most teams actually use
&lt;/h2&gt;

&lt;p&gt;In practice, very few organisations pick one approach exclusively. The most effective teams use different strategies for different contexts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Continuous deployment to staging, continuous delivery to production.&lt;/strong&gt; Every merge auto-deploys to a staging environment where QA, product managers, and stakeholders can review. Production deploys happen when the team is ready. This is the most common pattern for teams transitioning from manual deployments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Different pipelines for different risk levels.&lt;/strong&gt; Frontend changes to marketing pages might auto-deploy. Backend changes to authentication or billing go through a manual approval gate. The risk profile of the change determines the process, not a blanket policy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feature flags bridge the gap.&lt;/strong&gt; With feature flags, you can have continuous deployment (code goes to production automatically) while maintaining continuous delivery semantics (the feature is not &lt;q&gt;released&lt;/q&gt; until someone enables the flag). This gives you the deployment speed of CD with the release control of delivery.&lt;/p&gt;

&lt;p&gt;Understanding &lt;a href="https://dev.to/deployhq/what-is-software-deployment-a-complete-guide-23n2-temp-slug-2309058"&gt;what software deployment actually involves&lt;/a&gt; helps you design these pipelines with the right level of automation for each stage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common misconceptions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;q&gt;Continuous deployment means no testing.&lt;/q&gt;&lt;/strong&gt; This is backwards. Continuous deployment requires &lt;em&gt;more&lt;/em&gt; testing than continuous delivery, not less. When tests are the only gate to production, they must be comprehensive, fast, and reliable. Teams doing continuous deployment typically invest heavily in test infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;q&gt;Continuous delivery is just manual deployment.&lt;/q&gt;&lt;/strong&gt; No. With continuous delivery, the entire pipeline is automated --- build, test, package, deploy to staging, run acceptance tests. The &lt;em&gt;only&lt;/em&gt; manual step is the final approval to promote to production. That approval might be a single click in a dashboard, not a manual deployment process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;q&gt;You need continuous deployment to move fast.&lt;/q&gt;&lt;/strong&gt; Not necessarily. A team doing continuous delivery with a one-click deploy can ship a hotfix to production in under 5 minutes. The difference between &lt;q&gt;auto-deploy on merge&lt;/q&gt; and &lt;q&gt;click a button after merge&lt;/q&gt; is seconds, not hours. The real speed gains come from CI and automated pipelines, which both approaches share.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;q&gt;Continuous deployment is always the goal.&lt;/q&gt;&lt;/strong&gt; It depends entirely on your context. A hospital's medical device software should not auto-deploy to production. A personal blog's deployment pipeline probably should. The &lt;q&gt;best&lt;/q&gt; approach is the one that matches your risk tolerance, regulatory requirements, and team capabilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;q&gt;You have to choose one.&lt;/q&gt;&lt;/strong&gt; As covered in the hybrid section, most teams use both. The question is not &lt;q&gt;which one?&lt;/q&gt; but &lt;q&gt;which one for this particular service or change?&lt;/q&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How \DeployHQ supports both approaches
&lt;/h2&gt;

&lt;p&gt;Whether your team practises continuous delivery, continuous deployment, or a hybrid of both, DeployHQ handles the deployment step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For continuous delivery workflows&lt;/strong&gt; , DeployHQ deploys automatically when you push to a branch, but you control which branch triggers a deploy and when you merge to it. Your deployment dashboard shows pending deployments, lets you review what will change, and gives you a one-click deploy when you are ready. You stay in control of timing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For continuous deployment workflows&lt;/strong&gt; , configure DeployHQ to deploy on every push to your main branch. Pair this with your CI server --- when your tests pass and code merges, DeployHQ picks up the change and deploys it automatically. Your CI pipeline is the gate; DeployHQ is the delivery mechanism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.deployhq.com/features/zero-downtime-deployments" rel="noopener noreferrer"&gt;Zero-downtime deployments&lt;/a&gt;&lt;/strong&gt; are essential for continuous deployment. DeployHQ supports atomic deployments that swap the live version only after the new version is fully uploaded, so your users never see a half-deployed state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;Build pipelines&lt;/a&gt;&lt;/strong&gt; let you run build commands (npm install, webpack, composer install) on \DeployHQ's servers before deploying. This means your CI server handles testing while DeployHQ handles building and deploying --- a clean separation of concerns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One-click rollback&lt;/strong&gt; provides a safety net for both approaches. If a deploy introduces a problem, roll back to the previous version in seconds from the DeployHQ dashboard.&lt;/p&gt;

&lt;p&gt;For teams &lt;a href="https://www.deployhq.com/deploy-from-github" rel="noopener noreferrer"&gt;deploying from GitHub&lt;/a&gt;, setting up &lt;a href="https://www.deployhq.com/features/automatic-deployments" rel="noopener noreferrer"&gt;automatic deployments&lt;/a&gt; takes minutes. Push to main, DeployHQ deploys. Push to staging, DeployHQ deploys to your staging server. Different branches, different servers, different strategies.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting started: a practical path
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you are not doing either yet&lt;/strong&gt; , start with continuous integration. Get automated builds and tests running on every commit. Once your team trusts the CI pipeline, add automated staging deploys --- that is continuous delivery. For a step-by-step guide, see &lt;a href="https://dev.to/deployhq/cicd-pipelines-the-complete-guide-9p5-temp-slug-1919683"&gt;CI/CD Pipelines: The Complete Guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you are doing continuous delivery&lt;/strong&gt; , evaluate whether your test suite is comprehensive enough to remove the manual gate. Track how often the human approval step actually catches issues that tests missed. If the answer is &lt;q&gt;rarely,&lt;/q&gt; you might be ready for continuous deployment --- at least for lower-risk services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you are doing continuous deployment&lt;/strong&gt; , make sure your safety nets are solid. Automated rollback should be fast (under 60 seconds). Monitoring should alert on error rate spikes within minutes. Incident response should be practised, not theoretical. And review whether &lt;a href="https://www.deployhq.com/blog/what-is-continuous-deployment" rel="noopener noreferrer"&gt;continuous deployment as a practice&lt;/a&gt; is working for every service, or if some should move back to delivery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The maturity path looks like this:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A["Manual Deployments&amp;lt;br/&amp;gt;FTP uploads, SSH scripts"] --&amp;gt; B["Continuous Integration&amp;lt;br/&amp;gt;Automated build + test"]
    B --&amp;gt; C["Continuous Delivery&amp;lt;br/&amp;gt;Automated pipeline + manual gate"]
    C --&amp;gt; D["Continuous Deployment&amp;lt;br/&amp;gt;Fully automated to production"]
    C --&amp;gt; E["Hybrid Approach&amp;lt;br/&amp;gt;CD for low-risk, Delivery for high-risk"]
    D --&amp;gt; E

    style A fill:#ffebee,stroke:#f44336
    style B fill:#e8f4f8,stroke:#2196F3
    style C fill:#fff3e0,stroke:#FF9800
    style D fill:#e8f5e9,stroke:#4CAF50
    style E fill:#f3e5f5,stroke:#9C27B0

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

&lt;/div&gt;



&lt;p&gt;Most teams land on the hybrid approach, and that is perfectly fine. The goal is not to reach the &lt;q&gt;end&lt;/q&gt; of the spectrum --- it is to find the deployment strategy that lets your team ship confidently and frequently.&lt;/p&gt;




&lt;p&gt;Ready to automate your deployments? &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Sign up for \DeployHQ&lt;/a&gt; and set up your first deployment pipeline in minutes, whether you are practising continuous delivery, continuous deployment, or both.&lt;/p&gt;

&lt;p&gt;If you have questions about setting up your deployment workflow, reach out at &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or find us on &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;Twitter/X @deployhq&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devopsinfrastructure</category>
      <category>whatis</category>
      <category>continuousdelivery</category>
      <category>continuousdeployment</category>
    </item>
    <item>
      <title>CI/CD Pipelines: The Complete Guide</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Fri, 13 Mar 2026 11:39:02 +0000</pubDate>
      <link>https://dev.to/deployhq/cicd-pipelines-the-complete-guide-1n3k</link>
      <guid>https://dev.to/deployhq/cicd-pipelines-the-complete-guide-1n3k</guid>
      <description>&lt;p&gt;If you already know &lt;a href="https://www.deployhq.com/blog/what-is-ci-cd" rel="noopener noreferrer"&gt;what CI/CD is&lt;/a&gt;, the next question is practical: how do you actually build a pipeline that works for your team? This guide walks through designing, configuring, securing, and optimizing a CI/CD pipeline from scratch. No enterprise-scale assumptions — just actionable steps you can implement today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anatomy of a CI/CD Pipeline
&lt;/h2&gt;

&lt;p&gt;Every CI/CD pipeline follows the same fundamental flow, regardless of the tools you use. Code moves through a series of automated stages, each acting as a quality gate before the next.&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[Source] --&amp;gt; B[Build]
    B --&amp;gt; C[Test]
    C --&amp;gt; D[Deploy]
    D --&amp;gt; E[Monitor]
    E --&amp;gt;|Rollback| D

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Source&lt;/strong&gt; is where everything starts. A commit or pull request triggers the pipeline. Your CI system detects the change, checks out the code, and begins processing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build&lt;/strong&gt; compiles your application, installs dependencies, and produces deployable artifacts. For interpreted languages like Python or PHP, this might just be dependency installation and asset compilation. For compiled languages like Go or Java, it includes the actual compilation step. For a deeper look at &lt;a href="https://www.deployhq.com/blog/what-is-a-build-pipeline" rel="noopener noreferrer"&gt;what a build pipeline is&lt;/a&gt; and how to structure one, start there. The &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;build pipelines feature&lt;/a&gt; in &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; lets you run build commands as part of the deployment process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test&lt;/strong&gt; runs your automated test suite against the built artifacts. This is the most critical gate — if tests fail, the pipeline stops and nothing reaches production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy&lt;/strong&gt; pushes the tested artifacts to your target environment. This could be a staging server for manual review or production for fully automated continuous deployment. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; handles this stage with support for &lt;a href="https://www.deployhq.com/features/automatic-deployments" rel="noopener noreferrer"&gt;automatic deployments&lt;/a&gt; triggered by webhooks or API calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitor&lt;/strong&gt; watches the deployed application for errors, performance degradation, or unexpected behavior. Good monitoring closes the feedback loop — if something breaks, you catch it before your users do.&lt;/p&gt;




&lt;h2&gt;
  
  
  Designing Your Pipeline
&lt;/h2&gt;

&lt;p&gt;Before writing any configuration, make three decisions that shape everything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  Branch Strategy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Trunk-based development&lt;/strong&gt; works best for small-to-medium teams. Everyone commits to &lt;code&gt;main&lt;/code&gt; (or short-lived feature branches that merge within a day or two). The pipeline runs on every push to &lt;code&gt;main&lt;/code&gt;, and deployments happen frequently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitFlow&lt;/strong&gt; uses long-lived &lt;code&gt;develop&lt;/code&gt;, &lt;code&gt;release&lt;/code&gt;, and &lt;code&gt;hotfix&lt;/code&gt; branches. It adds ceremony but gives you more control over what reaches production and when. If your release cycle is weekly or longer, GitFlow might make sense.&lt;/p&gt;

&lt;p&gt;For most teams, trunk-based development with feature flags is the simpler, faster option. You deploy more often, which means smaller changes, which means fewer things break.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment Strategy
&lt;/h3&gt;

&lt;p&gt;A typical setup uses three environments:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Deploys from&lt;/th&gt;
&lt;th&gt;Audience&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Development&lt;/td&gt;
&lt;td&gt;Integration testing&lt;/td&gt;
&lt;td&gt;Feature branches&lt;/td&gt;
&lt;td&gt;Developers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staging&lt;/td&gt;
&lt;td&gt;Pre-production validation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;main&lt;/code&gt; branch&lt;/td&gt;
&lt;td&gt;QA, stakeholders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;td&gt;Live application&lt;/td&gt;
&lt;td&gt;Tagged releases or &lt;code&gt;main&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Users&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can start with just staging and production. Add environments only when you have a concrete reason — each one adds maintenance overhead and slows your feedback loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Delivery vs. Deployment
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Continuous delivery&lt;/strong&gt; means every commit that passes the pipeline is &lt;em&gt;ready&lt;/em&gt; to deploy, but a human clicks the button. &lt;strong&gt;Continuous deployment&lt;/strong&gt; means every passing commit goes to production automatically, with no manual gate. The &lt;a href="https://dev.to/alex_morgan_2e6f2f637b128/continuous-delivery-vs-continuous-deployment-whats-the-difference-c6f-temp-slug-5636075"&gt;comparison between these approaches&lt;/a&gt; is worth reading if you are deciding which to adopt.&lt;/p&gt;

&lt;p&gt;Start with continuous delivery. Move to continuous deployment once your test suite is comprehensive enough that you trust it to catch regressions without human review.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pipeline Configuration Example
&lt;/h2&gt;

&lt;p&gt;Here is a real GitHub Actions workflow that builds a Node.js application, runs tests in parallel, and triggers a &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; deployment on success.&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;CI/CD Pipeline&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;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&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;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&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;NODE_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&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;build&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;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="s"&gt;${{ env.NODE_VERSION }}&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&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/upload-artifact@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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build-output&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist/&lt;/span&gt;
          &lt;span class="na"&gt;retention-days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;

  &lt;span class="na"&gt;test-unit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&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;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="s"&gt;${{ env.NODE_VERSION }}&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run test:unit -- --shard=${{ matrix.shard }}&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;shard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;1/3&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;2/3&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;3/3&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;test-integration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&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;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&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="s"&gt;5432:5432&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;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="s"&gt;${{ env.NODE_VERSION }}&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run test:integration&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;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://postgres:test@localhost:5432/test&lt;/span&gt;

  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;test-unit&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;test-integration&lt;/span&gt;&lt;span class="pi"&gt;]&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;github.ref == 'refs/heads/main' &amp;amp;&amp;amp; github.event_name == 'push'&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Trigger DeployHQ deployment&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;curl -s -X POST \&lt;/span&gt;
            &lt;span class="s"&gt;"${{ secrets.DEPLOYHQ_WEBHOOK_URL }}" \&lt;/span&gt;
            &lt;span class="s"&gt;-H "Content-Type: application/json" \&lt;/span&gt;
            &lt;span class="s"&gt;-d '{"branch": "main"}'&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;A few things to notice in this configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dependency caching&lt;/strong&gt; (&lt;code&gt;cache: 'npm'&lt;/code&gt;) avoids re-downloading packages on every run. This alone can cut 30-60 seconds off each build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test sharding&lt;/strong&gt; splits unit tests across three parallel runners. A test suite that takes 6 minutes sequentially finishes in roughly 2 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DeployHQ as the CD step&lt;/strong&gt; keeps your deployment logic out of CI. The webhook triggers DeployHQ, which handles the actual file transfer, build commands, and server configuration. This separation means you can change your CI provider without touching your deployment setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are deploying &lt;a href="https://www.deployhq.com/deploy-from-github" rel="noopener noreferrer"&gt;from GitHub&lt;/a&gt;, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; can also detect pushes directly without the webhook — but using a webhook gives you the option to deploy only after CI passes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing in Your Pipeline
&lt;/h2&gt;

&lt;p&gt;Not all tests belong in every pipeline run. The testing pyramid helps you decide what to run and when.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TB
    subgraph Pyramid["Testing Pyramid"]
        direction TB
        E2E["E2E Tests\n(few, slow, high confidence)"]
        INT["Integration Tests\n(moderate count, moderate speed)"]
        UNIT["Unit Tests\n(many, fast, focused)"]
    end
    E2E --- INT
    INT --- UNIT

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Unit tests&lt;/strong&gt; run on every commit. They are fast (seconds to low minutes), test individual functions in isolation, and catch logic errors early. If your unit tests take longer than 3 minutes, split them into parallel shards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integration tests&lt;/strong&gt; run on every push to &lt;code&gt;main&lt;/code&gt; and on pull requests. They verify that components work together — database queries return expected results, API endpoints respond correctly, services communicate as expected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;End-to-end tests&lt;/strong&gt; run before production deployments, ideally on a staging environment. They simulate real user workflows through a browser. E2E tests are slow and brittle, so keep the count low — cover critical paths (signup, checkout, core features) and nothing more.&lt;/p&gt;

&lt;p&gt;The key principle: &lt;strong&gt;fast feedback first&lt;/strong&gt;. A developer should know within 2-3 minutes whether their change broke something. Push expensive tests later in the pipeline where they do not block the inner development loop.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deployment Strategies
&lt;/h2&gt;

&lt;p&gt;How code reaches production matters as much as how it is tested. The right strategy depends on your risk tolerance, infrastructure, and team size. For a broader look at &lt;a href="https://dev.to/deployhq/what-is-software-deployment-a-complete-guide-23n2-temp-slug-2309058"&gt;what software deployment involves&lt;/a&gt;, start there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Direct Deployment
&lt;/h3&gt;

&lt;p&gt;The simplest approach: upload new files and replace the old ones. This works for static sites and small applications where a few seconds of downtime during the swap is acceptable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zero-Downtime Deployment
&lt;/h3&gt;

&lt;p&gt;Uses symlink switching to swap between releases atomically. The new version is uploaded to a fresh directory, and once ready, the web server's document root symlink is flipped to point at it. There is no moment where the application is unavailable. DeployHQ supports this natively — see &lt;a href="https://www.deployhq.com/features/zero-downtime-deployments" rel="noopener noreferrer"&gt;zero-downtime deployments with DeployHQ&lt;/a&gt; for the setup walkthrough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blue-Green Deployment
&lt;/h3&gt;

&lt;p&gt;Maintains two identical production environments. Traffic routes to &lt;q&gt;blue&lt;/q&gt; while &lt;q&gt;green&lt;/q&gt; receives the new deployment. After validation, traffic switches from blue to green.&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
    LB[Load Balancer]
    LB --&amp;gt;|Active| Blue[Blue Environment\nv1.2.3]
    LB -.-&amp;gt;|Standby| Green[Green Environment\nv1.2.4]
    Green --&amp;gt;|Validated| Switch{Switch Traffic}
    Switch --&amp;gt;|Cutover| LB

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

&lt;/div&gt;



&lt;p&gt;The advantage is instant rollback — just switch traffic back to blue. The downside is cost: you are running two full environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Canary Deployment
&lt;/h3&gt;

&lt;p&gt;Routes a small percentage of traffic (typically 5-10%) to the new version while the rest continues hitting the current version. If error rates stay flat, you gradually increase the percentage until 100% of traffic reaches the new version.&lt;/p&gt;

&lt;p&gt;Canary deployments catch issues that only surface under real traffic patterns — race conditions, performance regressions at scale, edge cases your test suite missed. They require a load balancer that supports weighted routing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rolling Deployment
&lt;/h3&gt;

&lt;p&gt;Updates servers one at a time (or in small batches) behind a load balancer. At any point during the rollout, some servers run the old version and some run the new. This works well for stateless applications but can cause issues if the old and new versions are not compatible with each other (different database schemas, changed API contracts).&lt;/p&gt;

&lt;p&gt;For most small-to-medium teams, &lt;strong&gt;zero-downtime deployment via symlink switching&lt;/strong&gt; hits the sweet spot — no downtime, simple rollback, and no extra infrastructure cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security in CI/CD Pipelines
&lt;/h2&gt;

&lt;p&gt;Your pipeline has access to production credentials, source code, and deployment infrastructure. Treat it as a high-value attack surface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secrets Management
&lt;/h3&gt;

&lt;p&gt;Never hardcode secrets in pipeline configuration files or repository code. Use your CI provider's encrypted secrets store (GitHub Actions Secrets, GitLab CI Variables, etc.) and inject them as environment variables at runtime.&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="c1"&gt;# Good: secrets injected at runtime&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;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DATABASE_URL }}&lt;/span&gt;
  &lt;span class="na"&gt;API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.API_KEY }}&lt;/span&gt;

&lt;span class="c1"&gt;# Bad: secrets in the repository (never do this)&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;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;postgres://admin:password123@db.example.com/prod"&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Rotate secrets on a schedule. If a secret is ever exposed in logs or a commit, rotate it immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependency Scanning
&lt;/h3&gt;

&lt;p&gt;Automated tools like &lt;code&gt;npm audit&lt;/code&gt;, Dependabot, or Snyk scan your dependency tree for known vulnerabilities. Run these on every pull request — they add seconds to the pipeline and catch issues before they merge.&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="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;Audit dependencies&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 audit --audit-level=high&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Static Analysis (SAST)
&lt;/h3&gt;

&lt;p&gt;Static Application Security Testing scans your source code for common vulnerabilities: SQL injection, XSS, insecure deserialization, hardcoded credentials. Tools like Semgrep, SonarQube, or CodeQL integrate directly into CI workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supply Chain Security
&lt;/h3&gt;

&lt;p&gt;Lock files (&lt;code&gt;package-lock.json&lt;/code&gt;, &lt;code&gt;Gemfile.lock&lt;/code&gt;, &lt;code&gt;poetry.lock&lt;/code&gt;) pin exact dependency versions. Always commit them and use deterministic install commands (&lt;code&gt;npm ci&lt;/code&gt; instead of &lt;code&gt;npm install&lt;/code&gt;) to prevent supply chain attacks where a compromised package publishes a malicious minor version.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment Permissions
&lt;/h3&gt;

&lt;p&gt;Use role-based access control (RBAC) for deployment. Not everyone who can merge code should be able to deploy to production. DeployHQ supports &lt;a href="https://www.deployhq.com/pricing" rel="noopener noreferrer"&gt;team permissions&lt;/a&gt; that let you restrict who can trigger deployments to specific environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Monitoring and Rollback
&lt;/h2&gt;

&lt;p&gt;Deploying is only half the job. You need to know whether the deployment actually worked.&lt;/p&gt;

&lt;h3&gt;
  
  
  Post-Deployment Health Checks
&lt;/h3&gt;

&lt;p&gt;After every deployment, run automated checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTP health endpoint&lt;/strong&gt; returns 200 with expected response body&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database connectivity&lt;/strong&gt; confirmed via a lightweight query&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External service dependencies&lt;/strong&gt; are reachable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key business metrics&lt;/strong&gt; (error rate, response time) remain within normal bounds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any check fails within the first 5 minutes, trigger a rollback automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automated Rollback
&lt;/h3&gt;

&lt;p&gt;Define clear rollback triggers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Threshold&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 5xx error rate&lt;/td&gt;
&lt;td&gt;&amp;gt;5% for 2 minutes&lt;/td&gt;
&lt;td&gt;Auto-rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Response time p95&lt;/td&gt;
&lt;td&gt;&amp;gt;2x baseline for 5 minutes&lt;/td&gt;
&lt;td&gt;Alert + manual rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health check failure&lt;/td&gt;
&lt;td&gt;Any check fails&lt;/td&gt;
&lt;td&gt;Auto-rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Critical exception spike&lt;/td&gt;
&lt;td&gt;&amp;gt;10x normal rate&lt;/td&gt;
&lt;td&gt;Auto-rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;DeployHQ provides one-click rollback to any previous deployment, which makes recovery a matter of seconds rather than minutes. For a deeper look at how &lt;a href="https://www.deployhq.com/blog/deployment-automation-a-quick-overview" rel="noopener noreferrer"&gt;deployment automation&lt;/a&gt; reduces recovery time, that guide covers the specifics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error Monitoring
&lt;/h3&gt;

&lt;p&gt;Integrate error tracking (Sentry, Bugsnag, Honeybadger) into your application. These tools catch unhandled exceptions, group them by root cause, and alert your team. The deployment marker feature in most error trackers lets you correlate error spikes with specific deployments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pipeline Performance Optimization
&lt;/h2&gt;

&lt;p&gt;A slow pipeline kills developer productivity. If your pipeline takes 20 minutes, developers context-switch while waiting, and feedback arrives too late to be useful. Target under 10 minutes for the full pipeline — under 5 minutes is excellent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cache Dependencies
&lt;/h3&gt;

&lt;p&gt;Every major CI platform supports dependency caching. Use it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Cache target&lt;/th&gt;
&lt;th&gt;Typical savings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;~/.npm&lt;/code&gt; or &lt;code&gt;node_modules&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;30-90 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.cache/pip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;20-60 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ruby&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vendor/bundle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;30-90 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/go/pkg/mod&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;15-45 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Parallelize Test Suites
&lt;/h3&gt;

&lt;p&gt;Split tests across multiple runners. GitHub Actions supports matrix strategies that shard your test suite automatically. Three shards typically give you a 2.5-3x speedup for minimal additional cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Incremental Builds
&lt;/h3&gt;

&lt;p&gt;Only rebuild what changed. Monorepo tools like Nx and Turborepo track file dependencies and skip unchanged packages. For simpler projects, check whether source files changed before running expensive steps:&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="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;Check if build needed&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;changes&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;if git diff --name-only HEAD~1 | grep -q '^src/'; then&lt;/span&gt;
      &lt;span class="s"&gt;echo "build_needed=true" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
    &lt;span class="s"&gt;fi&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;Build&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.changes.outputs.build_needed == '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 run build&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Artifact Reuse
&lt;/h3&gt;

&lt;p&gt;Build once, deploy the same artifact everywhere. Upload build artifacts in the build job and download them in subsequent jobs instead of rebuilding. This ensures what you tested is exactly what you deploy — no &lt;q&gt;it worked on CI&lt;/q&gt; surprises.&lt;/p&gt;




&lt;h2&gt;
  
  
  Measuring Pipeline Effectiveness
&lt;/h2&gt;

&lt;p&gt;The DORA (DevOps Research and Assessment) metrics are the industry standard for measuring how well your delivery process performs. Track these four metrics to know whether your pipeline is actually helping.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Elite&lt;/th&gt;
&lt;th&gt;High&lt;/th&gt;
&lt;th&gt;Medium&lt;/th&gt;
&lt;th&gt;Low&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment frequency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;On-demand (multiple/day)&lt;/td&gt;
&lt;td&gt;Weekly to monthly&lt;/td&gt;
&lt;td&gt;Monthly to 6-monthly&lt;/td&gt;
&lt;td&gt;Fewer than once per 6 months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lead time for changes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Less than 1 hour&lt;/td&gt;
&lt;td&gt;1 day to 1 week&lt;/td&gt;
&lt;td&gt;1 week to 1 month&lt;/td&gt;
&lt;td&gt;More than 1 month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mean time to recovery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Less than 1 hour&lt;/td&gt;
&lt;td&gt;Less than 1 day&lt;/td&gt;
&lt;td&gt;1 day to 1 week&lt;/td&gt;
&lt;td&gt;More than 1 week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Change failure rate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0-15%&lt;/td&gt;
&lt;td&gt;16-30%&lt;/td&gt;
&lt;td&gt;31-45%&lt;/td&gt;
&lt;td&gt;46-60%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Source: DORA State of DevOps Report&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You do not need to be &lt;q&gt;elite&lt;/q&gt; across all four metrics. Focus on the ones that hurt most. If your change failure rate is high, invest in testing. If your lead time is long, look for pipeline bottlenecks and manual approval gates that could be automated.&lt;/p&gt;

&lt;p&gt;The relationship between these metrics matters too. Deploying more frequently &lt;em&gt;reduces&lt;/em&gt; change failure rate because each deployment is smaller and easier to reason about. Faster mean time to recovery comes from having reliable rollback and good monitoring — not from deploying less often.&lt;/p&gt;




&lt;h2&gt;
  
  
  CI/CD for Small Teams
&lt;/h2&gt;

&lt;p&gt;Most CI/CD content assumes you have a dedicated platform team, a Kubernetes cluster, and a budget for enterprise tools. If that is not you, here is a pragmatic approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start With Two Tools
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions for CI&lt;/strong&gt; handles building, testing, and code quality checks. The free tier includes 2,000 minutes per month for private repositories — enough for most small teams. GitLab CI/CD and Bitbucket Pipelines offer similar free tiers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;\DeployHQ for CD&lt;/strong&gt; handles getting your code onto servers. It connects to your GitHub repository, runs build commands on deployment, and transfers files to your servers over SSH, SFTP, or to cloud storage. The separation between CI and CD tools means you can swap either one independently.&lt;/p&gt;

&lt;p&gt;For a comparison of deployment tools available, the &lt;a href="https://dev.to/deployhq/best-software-deployment-tools-in-2026-3aaa-temp-slug-4453201"&gt;best software deployment tools guide&lt;/a&gt; covers the landscape.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Do Not Need (Yet)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Jenkins&lt;/strong&gt; : Powerful but requires its own server, maintenance, and plugin management. Overkill for teams under 10.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ArgoCD/Flux&lt;/strong&gt; : GitOps controllers for Kubernetes. If you are not running Kubernetes, skip these entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom deployment scripts&lt;/strong&gt; : They work until they do not. A managed tool like DeployHQ eliminates an entire class of &lt;q&gt;it works on my machine&lt;/q&gt; deployment bugs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microservices&lt;/strong&gt; : A monolith with a clean CI/CD pipeline ships faster than microservices with manual deployments.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cost Considerations
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Free tier&lt;/th&gt;
&lt;th&gt;Paid from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;2,000 min/month (private)&lt;/td&gt;
&lt;td&gt;$0.008/min overage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitLab CI/CD&lt;/td&gt;
&lt;td&gt;400 min/month&lt;/td&gt;
&lt;td&gt;$10/month for 10,000 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;\DeployHQ&lt;/td&gt;
&lt;td&gt;1 project, 5 deploys/day&lt;/td&gt;
&lt;td&gt;From $4/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bitbucket Pipelines&lt;/td&gt;
&lt;td&gt;50 min/month&lt;/td&gt;
&lt;td&gt;$15/month for 2,500 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A small team can run a complete CI/CD pipeline for under $20/month. Start there. Add complexity — and cost — only when you have evidence that you need it.&lt;/p&gt;




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

&lt;p&gt;A good CI/CD pipeline does not have to be complicated. Start with automated tests on every push, a single staging environment, and a reliable deployment tool. Add deployment strategies, security scanning, and performance optimization as your team and application grow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Sign up for \DeployHQ&lt;/a&gt; to handle the deployment side of your pipeline. Connect your repository, configure your server, and deploy with confidence.&lt;/p&gt;




&lt;p&gt;If you have questions about setting up your pipeline or need help with your deployment configuration, reach out to us at &lt;a href="mailto:support@deployhq.com"&gt;support@deployhq.com&lt;/a&gt; or find us on Twitter at &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;@deployhq&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devopsinfrastructure</category>
      <category>tutorials</category>
      <category>cicd</category>
      <category>deployhq</category>
    </item>
    <item>
      <title>Best Software Deployment Tools in 2026</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Fri, 13 Mar 2026 11:38:59 +0000</pubDate>
      <link>https://dev.to/deployhq/best-software-deployment-tools-in-2026-3g9o</link>
      <guid>https://dev.to/deployhq/best-software-deployment-tools-in-2026-3g9o</guid>
      <description>&lt;p&gt;Choosing a deployment tool is one of those decisions that shapes your entire workflow. Pick the wrong one and you'll spend more time fighting your tooling than shipping features.&lt;/p&gt;

&lt;p&gt;The problem is that &lt;q&gt;deployment tool&lt;/q&gt; means different things to different teams. A Rails developer deploying to a VPS needs something completely different from a platform team managing Kubernetes clusters. A freelancer shipping WordPress sites has different requirements to an enterprise running blue-green deployments across multiple regions.&lt;/p&gt;

&lt;p&gt;This guide compares the most popular software deployment tools in 2026, organized by what you're actually deploying to — so you can skip straight to the category that fits your setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we evaluated these tools
&lt;/h2&gt;

&lt;p&gt;We assessed each tool across five criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Setup complexity&lt;/strong&gt; : How long does it take to go from zero to first deployment?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment targets&lt;/strong&gt; : What can you deploy to? (Servers, containers, CDN, PaaS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build support&lt;/strong&gt; : Can the tool run build commands before deployment?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback capability&lt;/strong&gt; : How quickly can you undo a bad deployment?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing&lt;/strong&gt; : What does it actually cost for a small team (1-5 developers)?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Deploys to&lt;/th&gt;
&lt;th&gt;Build pipeline&lt;/th&gt;
&lt;th&gt;Free tier&lt;/th&gt;
&lt;th&gt;Starting price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Teams deploying to their own servers&lt;/td&gt;
&lt;td&gt;SSH, SFTP, S3, DigitalOcean Spaces&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;1 project&lt;/td&gt;
&lt;td&gt;$4/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Octopus Deploy&lt;/td&gt;
&lt;td&gt;Enterprise CD with complex release orchestration&lt;/td&gt;
&lt;td&gt;Servers, Kubernetes, cloud services&lt;/td&gt;
&lt;td&gt;Limited (CI separate)&lt;/td&gt;
&lt;td&gt;10 targets&lt;/td&gt;
&lt;td&gt;$13/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS CodeDeploy&lt;/td&gt;
&lt;td&gt;AWS-native deployments&lt;/td&gt;
&lt;td&gt;EC2, ECS, Lambda&lt;/td&gt;
&lt;td&gt;No (use CodeBuild)&lt;/td&gt;
&lt;td&gt;Free with AWS&lt;/td&gt;
&lt;td&gt;AWS costs only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netlify&lt;/td&gt;
&lt;td&gt;Static sites and Jamstack&lt;/td&gt;
&lt;td&gt;Netlify CDN&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;100GB bandwidth&lt;/td&gt;
&lt;td&gt;$19/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;Next.js and frontend frameworks&lt;/td&gt;
&lt;td&gt;Vercel Edge Network&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;100GB bandwidth&lt;/td&gt;
&lt;td&gt;$20/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Railway&lt;/td&gt;
&lt;td&gt;Full-stack apps on managed infra&lt;/td&gt;
&lt;td&gt;Railway containers&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;$5 credit&lt;/td&gt;
&lt;td&gt;$5/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;CI/CD tightly integrated with GitHub&lt;/td&gt;
&lt;td&gt;Anywhere (via scripts)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;2,000 min/mo&lt;/td&gt;
&lt;td&gt;$4/user/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Argo CD&lt;/td&gt;
&lt;td&gt;GitOps for Kubernetes&lt;/td&gt;
&lt;td&gt;Kubernetes clusters&lt;/td&gt;
&lt;td&gt;No (CI separate)&lt;/td&gt;
&lt;td&gt;Free (open source)&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Best for deploying to your own servers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  DeployHQ
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; is a dedicated deployment tool built specifically for teams that deploy to servers they control — VPS instances, cloud servers, shared hosting, or on-premise hardware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt; : Connect your Git repository (GitHub, GitLab, Bitbucket, or any Git remote), configure your server connection (SSH, SFTP, FTP, or cloud storage), and &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; handles the rest. When you push to a designated branch, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; runs your &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;build commands&lt;/a&gt;, transfers only the changed files, and executes any post-deployment scripts on your server.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Setup takes minutes, not hours — connect repo, add server, deploy&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;Build pipelines&lt;/a&gt; run compilation and dependency installation on DeployHQ's servers&lt;/li&gt;
&lt;li&gt;Only transfers changed files (not full re-uploads), making deployments fast&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.deployhq.com/features/zero-downtime-deployments" rel="noopener noreferrer"&gt;Zero-downtime deployments&lt;/a&gt; via atomic symlink switching&lt;/li&gt;
&lt;li&gt;One-click rollback to any previous deployment&lt;/li&gt;
&lt;li&gt;Works with any server you can connect to over SSH or SFTP&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.deployhq.com/deploy-from-github" rel="noopener noreferrer"&gt;Deploy from GitHub&lt;/a&gt;, &lt;a href="https://www.deployhq.com/deploy-from-gitlab" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;, or Bitbucket with automatic triggers&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Not designed for container orchestration (Docker, Kubernetes)&lt;/li&gt;
&lt;li&gt;No built-in CI (pair with GitHub Actions or GitLab CI for testing)&lt;/li&gt;
&lt;li&gt;UI-focused — less scriptable than CLI-first tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt; : Free for 1 project. Paid plans from $4/month for 5 projects. &lt;a href="https://www.deployhq.com/pricing" rel="noopener noreferrer"&gt;See pricing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt; : Web agencies, freelancers, and development teams deploying PHP, Ruby, Python, Node.js, or static sites to VPS or cloud servers. Especially strong for teams managing &lt;a href="https://www.deployhq.com/for-agencies" rel="noopener noreferrer"&gt;multiple client projects&lt;/a&gt; who need a simple, reliable deployment workflow without DevOps overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Octopus Deploy
&lt;/h3&gt;

&lt;p&gt;Octopus &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;Deploy&lt;/a&gt; is an enterprise-grade continuous delivery platform focused on release orchestration — managing complex deployments across multiple environments, approval workflows, and compliance requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt; : Octopus sits downstream of your CI server. Your CI pipeline (Jenkins, GitHub Actions, TeamCity) produces a package, and Octopus handles deploying that package through your environments (dev → staging → production) with configurable approval gates, variable management, and runbooks.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Powerful release orchestration with multi-environment promotion&lt;/li&gt;
&lt;li&gt;Deployment targets include servers, Kubernetes, Azure, AWS, and more&lt;/li&gt;
&lt;li&gt;Runbooks for operational tasks beyond deployment&lt;/li&gt;
&lt;li&gt;Tenanted deployments for SaaS vendors deploying to multiple customers&lt;/li&gt;
&lt;li&gt;Strong audit logging and compliance features&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Complex setup — expect hours to days for initial configuration&lt;/li&gt;
&lt;li&gt;Requires a separate CI tool (doesn't build or test code)&lt;/li&gt;
&lt;li&gt;Pricing scales with deployment targets, which can get expensive&lt;/li&gt;
&lt;li&gt;Overkill for small teams or simple deployment workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt; : Free for up to 10 deployment targets. Cloud plans from $13/month. Self-hosted available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt; : Enterprise teams with complex release processes, multiple environments, and compliance requirements. SaaS companies deploying to customer-specific infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best for AWS-native deployments
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AWS CodeDeploy
&lt;/h3&gt;

&lt;p&gt;AWS CodeDeploy automates deployments to EC2 instances, ECS containers, and Lambda functions. It's part of the broader AWS developer tools suite (CodePipeline, CodeBuild, CodeCommit).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt; : You define a deployment configuration (appspec.yml) that tells CodeDeploy how to stop the old version, install the new one, and start it up. CodeDeploy handles rolling deployments, blue-green deployments, and automatic rollback based on CloudWatch alarms.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Native integration with all AWS services&lt;/li&gt;
&lt;li&gt;Blue-green deployments for EC2 and ECS&lt;/li&gt;
&lt;li&gt;Automatic rollback on deployment failure or alarm triggers&lt;/li&gt;
&lt;li&gt;No additional cost (you pay only for the underlying AWS resources)&lt;/li&gt;
&lt;li&gt;Works with Auto Scaling groups&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;AWS-only — doesn't deploy to non-AWS infrastructure&lt;/li&gt;
&lt;li&gt;Requires significant AWS knowledge to configure&lt;/li&gt;
&lt;li&gt;No build capability (use CodeBuild or an external CI tool)&lt;/li&gt;
&lt;li&gt;Configuration via YAML and CLI — no visual deployment dashboard&lt;/li&gt;
&lt;li&gt;Steep learning curve compared to dedicated deployment tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt; : Free. You pay only for the AWS resources you use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt; : Teams already invested in the AWS ecosystem who want tight integration with EC2, ECS, and Lambda without adding another vendor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best for static sites and frontend frameworks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Netlify
&lt;/h3&gt;

&lt;p&gt;Netlify pioneered the Jamstack deployment model: push to Git, Netlify builds your site, and deploys it to a global CDN. It handles static sites, serverless functions, and edge computing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt; : Connect your repository, configure a build command (e.g., &lt;code&gt;npm run build&lt;/code&gt;), and every push triggers a new deployment. Netlify serves your site from its CDN with automatic HTTPS, form handling, and serverless functions.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;Deploy&lt;/a&gt; previews for every pull request&lt;/li&gt;
&lt;li&gt;Instant rollback (every deployment is immutable)&lt;/li&gt;
&lt;li&gt;Built-in forms, identity, and serverless functions&lt;/li&gt;
&lt;li&gt;Split testing (A/B deployments)&lt;/li&gt;
&lt;li&gt;Global CDN with automatic HTTPS&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Built for static sites and Jamstack — not for traditional server-side applications&lt;/li&gt;
&lt;li&gt;Serverless functions have execution limits (10-second timeout on free tier)&lt;/li&gt;
&lt;li&gt;Bandwidth limits can surprise you on popular sites&lt;/li&gt;
&lt;li&gt;Vendor lock-in for Netlify-specific features (forms, identity)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt; : Free tier with 100GB bandwidth. Pro from $19/month per member.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt; : Static sites, Jamstack applications, and marketing sites built with frameworks like Gatsby, Hugo, Next.js (static export), or Eleventy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vercel
&lt;/h3&gt;

&lt;p&gt;Vercel is the company behind Next.js, and their platform is optimized for deploying Next.js applications — though it supports other frameworks too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt; : Similar to Netlify — connect your repository, push code, and Vercel builds and deploys. The key difference is deep Next.js integration: server-side rendering, API routes, incremental static regeneration, and edge functions all work out of the box.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Best-in-class Next.js support (server components, ISR, middleware)&lt;/li&gt;
&lt;li&gt;Edge functions for low-latency server-side logic&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;Deploy&lt;/a&gt; previews with comments for team collaboration&lt;/li&gt;
&lt;li&gt;Analytics and speed insights built in&lt;/li&gt;
&lt;li&gt;Excellent developer experience and documentation&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Optimized for Next.js — other frameworks get fewer features&lt;/li&gt;
&lt;li&gt;Pricing can spike with high traffic (bandwidth and function invocations)&lt;/li&gt;
&lt;li&gt;Not suitable for traditional backend applications&lt;/li&gt;
&lt;li&gt;Vendor lock-in for Vercel-specific features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt; : Free tier (hobby). Pro from $20/month per member.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt; : Next.js applications. If you're building with Next.js, Vercel is the path of least resistance. For other frameworks, evaluate against Netlify and Cloudflare Pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best for full-stack apps on managed infrastructure
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Railway
&lt;/h3&gt;

&lt;p&gt;Railway provides a modern PaaS experience: deploy any application (Node.js, Python, Go, Rust, Docker) from a Git repository, and Railway handles the infrastructure — servers, databases, networking, and scaling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt; : Connect your repository or push a Docker image. Railway detects your framework, builds your application, and runs it on managed infrastructure. You can add databases (PostgreSQL, MySQL, Redis, MongoDB) with one click.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Deploys almost anything — not limited to static sites&lt;/li&gt;
&lt;li&gt;One-click databases and services&lt;/li&gt;
&lt;li&gt;Simple pricing based on usage (compute + memory)&lt;/li&gt;
&lt;li&gt;Good developer experience with CLI and dashboard&lt;/li&gt;
&lt;li&gt;Supports private networking between services&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;You don't control the underlying infrastructure&lt;/li&gt;
&lt;li&gt;Less mature than Heroku or Render for some edge cases&lt;/li&gt;
&lt;li&gt;Pricing can be unpredictable for compute-heavy applications&lt;/li&gt;
&lt;li&gt;Limited configuration for advanced networking or custom domains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt; : $5 one-time credit on the free trial. Usage-based pricing starting from $5/month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt; : Side projects, startups, and small teams who want to deploy full-stack applications without managing servers. Good alternative to Heroku.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best for CI/CD pipelines
&lt;/h2&gt;

&lt;h3&gt;
  
  
  GitHub Actions
&lt;/h3&gt;

&lt;p&gt;GitHub Actions is a CI/CD platform built into GitHub. It can build, test, and deploy code triggered by any GitHub event (push, pull request, release, schedule).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt; : Define workflows in YAML files (&lt;code&gt;.github/workflows/&lt;/code&gt;). Each workflow consists of jobs that run on GitHub-hosted or self-hosted runners. You can deploy to any target by writing the appropriate deployment steps — or use pre-built actions from the GitHub Marketplace.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Integrated directly into GitHub — no separate tool to manage&lt;/li&gt;
&lt;li&gt;Massive marketplace of pre-built actions&lt;/li&gt;
&lt;li&gt;Matrix builds for testing across multiple versions/platforms&lt;/li&gt;
&lt;li&gt;Free for public repositories&lt;/li&gt;
&lt;li&gt;Self-hosted runners for private infrastructure&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Deployment is &lt;q&gt;roll your own&lt;/q&gt; — you write the scripts&lt;/li&gt;
&lt;li&gt;No deployment dashboard, release history, or one-click rollback&lt;/li&gt;
&lt;li&gt;YAML configuration can become complex for multi-environment deployments&lt;/li&gt;
&lt;li&gt;Not a deployment tool — it's a CI/CD platform that &lt;em&gt;can&lt;/em&gt; deploy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt; : Free for public repos (2,000 minutes/month for private). Team plan from $4/user/month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt; : Teams already on GitHub who want CI/CD without adding another vendor. Pairs well with a dedicated deployment tool — use GitHub Actions for CI (build + test) and &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; for CD (deployment to servers).&lt;/p&gt;

&lt;h3&gt;
  
  
  Argo CD
&lt;/h3&gt;

&lt;p&gt;Argo CD is an open-source, GitOps-based continuous delivery tool for Kubernetes. It watches a Git repository containing Kubernetes manifests and automatically syncs them to your cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt; : You define your desired cluster state in Git (Kubernetes YAML, Helm charts, Kustomize). Argo CD continuously compares the live cluster state to the Git state and can automatically reconcile differences — or alert you to drift.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;GitOps model — Git is the single source of truth&lt;/li&gt;
&lt;li&gt;Real-time visualization of Kubernetes resources&lt;/li&gt;
&lt;li&gt;Automatic drift detection and sync&lt;/li&gt;
&lt;li&gt;Multi-cluster support&lt;/li&gt;
&lt;li&gt;Open source and free&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Kubernetes-only — not for traditional server deployments&lt;/li&gt;
&lt;li&gt;Requires understanding of Kubernetes concepts&lt;/li&gt;
&lt;li&gt;No CI capability (pair with a CI tool for build and test)&lt;/li&gt;
&lt;li&gt;Complex initial setup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt; : Free and open source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt; : Teams running Kubernetes who want a GitOps workflow. Not relevant if you're deploying to traditional servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose the right deployment tool
&lt;/h2&gt;

&lt;p&gt;The decision tree is simpler than most comparison articles make it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[What are you deploying to?] --&amp;gt; B{Your own servers?}
    B --&amp;gt;|VPS, cloud, on-prem| C[DeployHQ]
    B --&amp;gt;|No| D{Kubernetes?}
    D --&amp;gt;|Yes| E[Argo CD + CI tool]
    D --&amp;gt;|No| F{Static site?}
    F --&amp;gt;|Yes| G{Next.js?}
    G --&amp;gt;|Yes| H[Vercel]
    G --&amp;gt;|No| I[Netlify or Cloudflare Pages]
    F --&amp;gt;|No| J{AWS only?}
    J --&amp;gt;|Yes| K[AWS CodeDeploy]
    J --&amp;gt;|No| L{Want managed infra?}
    L --&amp;gt;|Yes| M[Railway]
    L --&amp;gt;|No| N{Enterprise release orchestration?}
    N --&amp;gt;|Yes| O[Octopus Deploy]
    N --&amp;gt;|No| C

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key questions:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Do you manage your own servers?&lt;/strong&gt; If you deploy to VPS instances, cloud servers, or on-premise hardware via SSH/SFTP, &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; gives you the simplest path to automated deployments.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Are you running Kubernetes?&lt;/strong&gt; Use Argo CD or Flux for GitOps-based deployment. These tools are purpose-built for container orchestration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Is it a static site or Jamstack app?&lt;/strong&gt; Netlify, Vercel, or Cloudflare Pages. Pick based on your framework — Vercel for Next.js, Netlify or Cloudflare for everything else.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Do you want managed infrastructure?&lt;/strong&gt; Railway, Render, or Fly.io remove the need to manage servers entirely. Good for startups and side projects.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Do you need enterprise release orchestration?&lt;/strong&gt; Octopus &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;Deploy&lt;/a&gt; handles complex multi-environment promotion, approval gates, and compliance workflows.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most teams deploying web applications to servers they control will get the most value from &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; — it's focused on doing one thing well: getting your code from Git to your server, reliably, with build pipelines and zero-downtime deployments built in.&lt;/p&gt;




&lt;p&gt;Ready to try it? &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;Sign up for DeployHQ&lt;/a&gt; and deploy your first project in under 5 minutes — no credit card required.&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 on Twitter &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;@deployhq&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devopsinfrastructure</category>
      <category>whatis</category>
      <category>softwaredeploymenttools</category>
      <category>2026</category>
    </item>
    <item>
      <title>What Is Software Deployment? A Complete Guide</title>
      <dc:creator>DeployHQ</dc:creator>
      <pubDate>Fri, 13 Mar 2026 11:38:53 +0000</pubDate>
      <link>https://dev.to/deployhq/what-is-software-deployment-a-complete-guide-53on</link>
      <guid>https://dev.to/deployhq/what-is-software-deployment-a-complete-guide-53on</guid>
      <description>&lt;p&gt;Software deployment is the process of moving code from a development environment to a place where it can be used — typically a production server. It's the bridge between writing code and making it available to users.&lt;/p&gt;

&lt;p&gt;That might sound straightforward, but in practice, deployment involves configuration, testing, coordination, and risk management. Understanding how deployment works — and how to do it well — is fundamental to shipping software reliably.&lt;/p&gt;

&lt;p&gt;This guide covers what software deployment means, the steps involved, common strategies, and how tools like &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; simplify the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does software deployment mean?
&lt;/h2&gt;

&lt;p&gt;At its core, software deployment is the act of taking code that has been written, tested, and approved, and placing it in an environment where it runs for its intended audience. That environment is usually a production server, but it can also be a staging server, a test environment, or a content delivery network.&lt;/p&gt;

&lt;p&gt;Deployment is not the same as development. Development is writing the code. Deployment is getting that code running somewhere.&lt;/p&gt;

&lt;p&gt;It's also not the same as a &lt;em&gt;release&lt;/em&gt;. A deployment is a technical event — moving code to a server. A release is a business event — making a feature available to users. You can deploy code without releasing it (for example, using &lt;a href="https://www.deployhq.com/blog/what-are-feature-flags" rel="noopener noreferrer"&gt;feature flags&lt;/a&gt; to keep new functionality hidden until you're ready). This distinction matters because it means you can deploy frequently and safely, decoupling technical risk from business timing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why software deployment matters
&lt;/h2&gt;

&lt;p&gt;Every line of code is worthless until it's running in production. Deployment is what turns development effort into user value, and how you handle it directly affects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reliability&lt;/strong&gt; : A poor deployment process is the single most common cause of production outages. Broken deployments take down websites, corrupt data, and erode user trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt; : Teams that deploy frequently ship features faster, fix bugs sooner, and respond to market changes more quickly. The &lt;a href="https://dora.dev/research/2023/dora-report/" rel="noopener noreferrer"&gt;2023 DORA report&lt;/a&gt; found that elite teams deploy on demand — multiple times per day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer experience&lt;/strong&gt; : Manual, error-prone deployments create anxiety and slow teams down. Automated deployments free developers to focus on building rather than babysitting releases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business outcomes&lt;/strong&gt; : Faster, safer deployments mean shorter time-to-market, fewer incidents, and happier customers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The software deployment process
&lt;/h2&gt;

&lt;p&gt;While every team's workflow is different, most deployment processes follow the same general steps:&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 committed] --&amp;gt; B[Build]
    B --&amp;gt; C[Test]
    C --&amp;gt; D[Stage]
    D --&amp;gt; E[Deploy to production]
    E --&amp;gt; F[Verify &amp;amp; monitor]

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  1. Code is committed to version control
&lt;/h3&gt;

&lt;p&gt;Deployment starts with code being pushed to a &lt;a href="https://blog.deployhq.com/git/creating-a-repository" rel="noopener noreferrer"&gt;Git repository&lt;/a&gt;. This is the trigger — either a developer pushes to a specific branch (like &lt;code&gt;main&lt;/code&gt; or &lt;code&gt;production&lt;/code&gt;), or a pull request is merged.&lt;/p&gt;

&lt;p&gt;Most modern deployment tools, including &lt;a href="https://www.deployhq.com/features/automatic-deployments" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt;, can watch your repository and trigger deployments automatically when new commits land on a designated branch.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Build
&lt;/h3&gt;

&lt;p&gt;Before code can run on a server, it often needs to be compiled, bundled, or otherwise transformed. This build step might involve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compiling TypeScript to JavaScript&lt;/li&gt;
&lt;li&gt;Bundling frontend assets with Webpack or Vite&lt;/li&gt;
&lt;li&gt;Installing dependencies with &lt;code&gt;npm install&lt;/code&gt; or &lt;code&gt;composer install&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Running database migrations&lt;/li&gt;
&lt;li&gt;Generating static files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools like &lt;a href="https://www.deployhq.com/features/build-pipelines" rel="noopener noreferrer"&gt;DeployHQ's build pipeline&lt;/a&gt; run these commands on a dedicated build server before the result is sent to your production server — keeping your deployment clean and consistent.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Test
&lt;/h3&gt;

&lt;p&gt;Automated tests should run before code reaches production. This includes unit tests, integration tests, and sometimes end-to-end tests. If tests fail, the deployment should stop.&lt;/p&gt;

&lt;p&gt;In a &lt;a href="https://blog.deployhq.com/blog/what-is-ci-cd" rel="noopener noreferrer"&gt;CI/CD pipeline&lt;/a&gt;, testing typically happens in a continuous integration (CI) service like GitHub Actions, GitLab CI, or CircleCI. The deployment tool then takes over for the continuous delivery (CD) part — pushing tested code to your servers.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Stage
&lt;/h3&gt;

&lt;p&gt;A staging environment is a replica of production where you verify that everything works as expected before going live. Staging catches issues that automated tests might miss — visual bugs, configuration problems, integration failures.&lt;/p&gt;

&lt;p&gt;Not every team uses staging. Smaller teams might deploy directly to production with safeguards like canary deployments or feature flags. But for applications where downtime is costly, a staging step is worth the effort.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Deploy to production
&lt;/h3&gt;

&lt;p&gt;This is the moment code reaches your live servers. Depending on your setup, this might involve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uploading files via SFTP or SSH&lt;/li&gt;
&lt;li&gt;Pulling the latest code from Git on the server&lt;/li&gt;
&lt;li&gt;Replacing a running container with an updated image&lt;/li&gt;
&lt;li&gt;Swapping traffic between server groups (blue-green deployment)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For teams deploying to their own servers — VPS instances on DigitalOcean, AWS EC2, Hetzner, or on-premise hardware — a tool like &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; handles the file transfer, runs post-deployment commands, and manages the entire workflow through a simple interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Verify and monitor
&lt;/h3&gt;

&lt;p&gt;Deployment doesn't end when files land on the server. You need to verify that the application is running correctly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Health checks confirm the application responds&lt;/li&gt;
&lt;li&gt;Error monitoring (Sentry, Bugsnag) catches exceptions&lt;/li&gt;
&lt;li&gt;Performance monitoring tracks response times&lt;/li&gt;
&lt;li&gt;Log aggregation reveals warnings or failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If something goes wrong, you need the ability to &lt;a href="https://www.deployhq.com/features/zero-downtime-deployments" rel="noopener noreferrer"&gt;roll back&lt;/a&gt; to the previous version quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Software deployment strategies
&lt;/h2&gt;

&lt;p&gt;Not all deployments work the same way. The strategy you choose depends on your risk tolerance, infrastructure, and how much downtime you can accept.&lt;/p&gt;

&lt;h3&gt;
  
  
  Direct deployment (replace and restart)
&lt;/h3&gt;

&lt;p&gt;The simplest approach: replace the old files with new ones and restart the application. This is what most small teams do, and it works well when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your application can tolerate a few seconds of downtime&lt;/li&gt;
&lt;li&gt;You have a single server&lt;/li&gt;
&lt;li&gt;Deployments are infrequent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The downside is that if something goes wrong, your site is down until you fix it or roll back.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zero-downtime deployment
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com/features/zero-downtime-deployments" rel="noopener noreferrer"&gt;Zero-downtime deployment&lt;/a&gt; ensures users never see an error page during a release. The new version is prepared alongside the old one, and traffic is switched over only when the new version is ready.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; supports this through atomic deployments — uploading new files to a separate directory, then switching a symlink once the upload and build steps are complete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blue-green deployment
&lt;/h3&gt;

&lt;p&gt;You maintain two identical production environments: &lt;q&gt;blue&lt;/q&gt; (current) and &lt;q&gt;green&lt;/q&gt; (new). You deploy to the inactive environment, test it, then switch traffic from blue to green. If anything goes wrong, you switch back instantly.&lt;/p&gt;

&lt;p&gt;This requires double the infrastructure but provides the fastest possible rollback.&lt;/p&gt;

&lt;h3&gt;
  
  
  Canary deployment
&lt;/h3&gt;

&lt;p&gt;Instead of deploying to all servers at once, you deploy to a small subset first (the &lt;q&gt;canary&lt;/q&gt;). You monitor that subset for errors, and if everything looks good, you gradually roll out to the rest. This limits the blast radius of a bad deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rolling deployment
&lt;/h3&gt;

&lt;p&gt;Similar to canary, but you update servers one at a time (or in small batches) until all servers are running the new version. At any point during the rollout, some servers run the old version and some run the new one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[New version ready] --&amp;gt; B[Deploy to Server 1]
    B --&amp;gt; C{Healthy?}
    C --&amp;gt;|Yes| D[Deploy to Server 2]
    C --&amp;gt;|No| E[Roll back Server 1]
    D --&amp;gt; F{Healthy?}
    F --&amp;gt;|Yes| G[Deploy to Server 3]
    F --&amp;gt;|No| H[Roll back Servers 1-2]
    G --&amp;gt; I[All servers updated]

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common deployment failures (and how to avoid them)
&lt;/h2&gt;

&lt;p&gt;Understanding why deployments fail helps you build a process that prevents them:&lt;/p&gt;

&lt;h3&gt;
  
  
  Missing dependencies
&lt;/h3&gt;

&lt;p&gt;The code works on your machine because you installed a package months ago. The production server doesn't have it. &lt;strong&gt;Fix&lt;/strong&gt; : Always install dependencies as part of your build step, and never rely on manually installed packages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment configuration drift
&lt;/h3&gt;

&lt;p&gt;Your staging server has different environment variables, a different database version, or a different OS version than production. &lt;strong&gt;Fix&lt;/strong&gt; : Use infrastructure as code and keep environments as identical as possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database migration failures
&lt;/h3&gt;

&lt;p&gt;A migration runs in the wrong order, or a migration assumes data exists that doesn't. &lt;strong&gt;Fix&lt;/strong&gt; : Test migrations against a copy of production data. Make migrations reversible where possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  File permission issues
&lt;/h3&gt;

&lt;p&gt;Uploaded files have the wrong permissions, so the web server can't read them or the application can't write to a cache directory. &lt;strong&gt;Fix&lt;/strong&gt; : Set file permissions explicitly in your deployment configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;q&gt;It worked in staging&lt;/q&gt;
&lt;/h3&gt;

&lt;p&gt;Staging doesn't perfectly mirror production — different traffic patterns, different data volumes, different third-party integrations. &lt;strong&gt;Fix&lt;/strong&gt; : Monitor closely after every production deployment and have a fast rollback plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where deployment fits in the development lifecycle
&lt;/h2&gt;

&lt;p&gt;Software deployment is one stage in a broader workflow. Here's how it connects to the rest:&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[Plan] --&amp;gt; B[Develop]
    B --&amp;gt; C[Commit &amp;amp; push]
    C --&amp;gt; D[CI: Build &amp;amp; test]
    D --&amp;gt; E[CD: Deploy]
    E --&amp;gt; F[Monitor]
    F --&amp;gt;|Feedback| A

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plan&lt;/strong&gt; : Define what to build&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Develop&lt;/strong&gt; : Write the code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit &amp;amp; push&lt;/strong&gt; : Save changes to &lt;a href="https://blog.deployhq.com/git/creating-a-repository" rel="noopener noreferrer"&gt;Git&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI (Continuous Integration)&lt;/strong&gt;: Automatically build and test on every push&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CD (Continuous Delivery/Deployment)&lt;/strong&gt;: Automatically deploy tested code to servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor&lt;/strong&gt; : Watch for issues, gather feedback, improve&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CI part is typically handled by services like GitHub Actions, GitLab CI, or Jenkins. The CD part — actually getting the code onto your servers — is where &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; fits. It connects to your repository, watches for changes, runs your build commands, and deploys the result to your servers over SSH, SFTP, or to cloud storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment automation vs manual deployment
&lt;/h2&gt;

&lt;p&gt;Manual deployment — SSHing into a server, pulling code, running commands by hand — works when you're just starting out. But it doesn't scale:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Manual deployment&lt;/th&gt;
&lt;th&gt;Automated deployment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Consistency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Depends on who runs it&lt;/td&gt;
&lt;td&gt;Same process every time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Minutes to hours&lt;/td&gt;
&lt;td&gt;Seconds to minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Error rate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High (human error)&lt;/td&gt;
&lt;td&gt;Low (scripted)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Audit trail&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None unless you document it&lt;/td&gt;
&lt;td&gt;Automatic logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frequency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Discouraged (too risky)&lt;/td&gt;
&lt;td&gt;Encouraged (low risk)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rollback&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual and stressful&lt;/td&gt;
&lt;td&gt;One-click or automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://blog.deployhq.com/blog/deployment-automation-a-quick-overview" rel="noopener noreferrer"&gt;Deployment automation&lt;/a&gt; removes the human from the process, making deployments faster, safer, and more frequent. This is what enables teams to deploy daily — or even multiple times per day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a deployment approach
&lt;/h2&gt;

&lt;p&gt;The right deployment setup depends on your situation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploying to your own servers (VPS, cloud, on-premise)?&lt;/strong&gt;You need a tool that connects to your server over SSH or SFTP and handles file transfer, build steps, and configuration. &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; is built for exactly this — it works with any server you can connect to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using a Platform-as-a-Service (PaaS)?&lt;/strong&gt;Platforms like Heroku, Railway, or Render handle deployment as part of their service. You push code, they build and deploy it. Less control, but less setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running containers?&lt;/strong&gt; Container-based deployments (Docker, Kubernetes) package your application and its dependencies together. You build a container image, push it to a registry, and orchestrate deployment across your infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static sites?&lt;/strong&gt; Static site generators (Next.js, Hugo, Jekyll) can deploy to CDNs like Cloudflare Pages, Netlify, or Vercel. Build once, serve everywhere.&lt;/p&gt;

&lt;p&gt;For many teams — especially those deploying web applications to their own servers — a tool like &lt;a href="https://www.deployhq.com" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; provides the right balance of automation, control, and simplicity without the complexity of container orchestration or the lock-in of a PaaS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Software deployment checklist
&lt;/h2&gt;

&lt;p&gt;Before deploying to production, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[] All tests pass in CI&lt;/li&gt;
&lt;li&gt;[] Build completes without errors&lt;/li&gt;
&lt;li&gt;[] Environment variables are configured correctly&lt;/li&gt;
&lt;li&gt;[] Database migrations have been tested&lt;/li&gt;
&lt;li&gt;[] Staging deployment has been verified (if applicable)&lt;/li&gt;
&lt;li&gt;[] Rollback plan is in place&lt;/li&gt;
&lt;li&gt;[] Team is aware of the deployment&lt;/li&gt;
&lt;li&gt;[] Monitoring and alerting are active&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a more detailed checklist, see our &lt;a href="https://dev.to/deployhq/the-ultimate-deployment-checklist-ensuring-smooth-and-successful-releases-47lm"&gt;ultimate deployment checklist&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started with software deployment
&lt;/h2&gt;

&lt;p&gt;If you're deploying for the first time, start simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Get your code into Git&lt;/strong&gt; — if it's not in a &lt;a href="https://blog.deployhq.com/git/creating-a-repository" rel="noopener noreferrer"&gt;Git repository&lt;/a&gt; yet, start there&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose a server&lt;/strong&gt; — a VPS from DigitalOcean, Hetzner, or AWS is a good starting point&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up a deployment tool&lt;/strong&gt; — &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;sign up for DeployHQ&lt;/a&gt;, connect your repository, and configure your server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy&lt;/strong&gt; — push to your deployment branch and watch it go live&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate&lt;/strong&gt; — enable automatic deployments so every push triggers a deploy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From there, you can layer on build pipelines, staging environments, zero-downtime deployments, and monitoring as your application grows.&lt;/p&gt;




&lt;p&gt;Ready to simplify your deployment workflow? &lt;a href="https://www.deployhq.com/signup" rel="noopener noreferrer"&gt;DeployHQ&lt;/a&gt; connects to your Git repository and deploys to any server over SSH, SFTP, or to cloud storage — with build pipelines, zero-downtime deployments, and one-click rollbacks built in.&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 on Twitter &lt;a href="https://x.com/deployhq" rel="noopener noreferrer"&gt;@deployhq&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devopsinfrastructure</category>
      <category>whatis</category>
      <category>softwaredeployment</category>
      <category>deployhq</category>
    </item>
  </channel>
</rss>
