<?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: Haripriya Veluchamy</title>
    <description>The latest articles on DEV Community by Haripriya Veluchamy (@techwithhari).</description>
    <link>https://dev.to/techwithhari</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%2F1914764%2Fbc8a04cf-4e71-485f-8880-5b49f05c9560.png</url>
      <title>DEV Community: Haripriya Veluchamy</title>
      <link>https://dev.to/techwithhari</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/techwithhari"/>
    <language>en</language>
    <item>
      <title>I Built an AI SEO Monitor That Remembers Everything</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Fri, 10 Apr 2026 14:16:39 +0000</pubDate>
      <link>https://dev.to/techwithhari/i-built-an-ai-seo-monitor-that-remembers-everything-4l7i</link>
      <guid>https://dev.to/techwithhari/i-built-an-ai-seo-monitor-that-remembers-everything-4l7i</guid>
      <description>&lt;p&gt;Most SEO monitoring tools give you a snapshot: today's clicks, today's issues, today's recommendations. You fix something, come back tomorrow, and the tool has no idea what you did or whether it helped.&lt;/p&gt;

&lt;p&gt;I wanted something smarter a system that &lt;em&gt;remembers&lt;/em&gt; site's history, correlates code changes with ranking shifts, and gives AI-generated insights that get better every day. Here's what I built and how.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem With Basic SEO Monitoring
&lt;/h2&gt;

&lt;p&gt;A typical monitoring setup looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch Google Search Console data&lt;/li&gt;
&lt;li&gt;Run a Lighthouse audit&lt;/li&gt;
&lt;li&gt;Send a Slack message with today's numbers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's fine. But it can't answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Did that metadata fix I deployed 10 days ago actually improve rankings?"&lt;/li&gt;
&lt;li&gt;"This recommendation has been flagged for 15 days why hasn't it been fixed?"&lt;/li&gt;
&lt;li&gt;"Clicks dropped this week was it a code change or an algorithm shift?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To answer those questions, you need &lt;em&gt;memory&lt;/em&gt;. That's where Cognee comes in.&lt;/p&gt;




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

&lt;p&gt;&lt;a href="https://github.com/topoteretes/cognee" rel="noopener noreferrer"&gt;Cognee&lt;/a&gt; is a knowledge graph SDK. Instead of storing data as flat rows in a database, it extracts entities and relationships and stores them as nodes and edges in a graph (Neo4j) with vector embeddings in a vector database (ChromaDB).&lt;/p&gt;

&lt;p&gt;Think of it like this: a normal database stores &lt;em&gt;"clicks = 262 on April 8"&lt;/em&gt;. A knowledge graph stores &lt;em&gt;"keyword 'vibe trading' ranked at position 1.78 on April 8, which is 12 spots better than March 25, and that improvement happened 3 days after a metadata fix was deployed"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The difference matters when you want AI to reason across weeks of history not just today.&lt;/p&gt;




&lt;h2&gt;
  
  
  System Architecture
&lt;/h2&gt;

&lt;p&gt;The full pipeline runs daily via GitHub Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PHASE 1 — Parallel data collection
├── Lighthouse audit (performance, SEO scores)
├── Broken links check
├── Meta tags validation
├── Core Web Vitals (via PageSpeed API)
└── Google Search Console (clicks, CTR, position, queries)

PHASE 2 — Main analysis job
├── git-change-detector.js    → scans commits, classifies SEO-relevant changes
├── cognee_ingest.py          → writes today's data to Neo4j + ChromaDB
├── cognee-store-updater.js   → updates 30-day rolling JSON snapshot
├── audit-scraper.js          → fetches live pages, scores SEO/GEO/AEO signals
├── audit-ingest.py           → stores audit scores in the knowledge graph
├── cognee-analyzer.js        → builds enriched AI context, calls Azure OpenAI
├── send-ai-slack.js          → posts daily report to Slack
└── cognee-blob-sync.js       → backs up knowledge graph to Azure Blob Storage

PHASE 3 — Weekly (Sundays)
└── competitor-monitor.js     → fetches competitor pages, scores them, posts comparison
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why Cognee? The Knowledge Graph Advantage
&lt;/h2&gt;

&lt;p&gt;Every day, &lt;code&gt;cognee_ingest.py&lt;/code&gt; builds a structured document containing today's GSC metrics, top queries, AI recommendations, and recent git commits. Azure OpenAI reads this and extracts entities keywords, positions, dates, code changes which Cognee writes to Neo4j as connected nodes. graph starts to grow.&lt;/p&gt;

&lt;p&gt;After 30 days, the graph contains nodes like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;Keyword:&lt;/span&gt; &lt;span class="s2"&gt;"platform name"&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="n"&gt;RANKED_AT&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Position&lt;/span&gt;&lt;span class="dl"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1.78&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;date:&lt;/span&gt; &lt;span class="n"&gt;April&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;CodeChange:&lt;/span&gt; &lt;span class="s2"&gt;"metadata fix"&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HAPPENED_BEFORE&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;MetricSnapshot:&lt;/span&gt; &lt;span class="n"&gt;April&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;Recommendation:&lt;/span&gt; &lt;span class="s2"&gt;"add FAQ schema"&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FLAGGED_ON&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;Date:&lt;/span&gt; &lt;span class="n"&gt;April&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;Recommendation:&lt;/span&gt; &lt;span class="s2"&gt;"add FAQ schema"&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FLAGGED_ON&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;Date:&lt;/span&gt; &lt;span class="n"&gt;April&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;...&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when the AI runs its daily analysis, it doesn't just see today's data. It sees patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keyword velocity&lt;/strong&gt;: which keywords improved or dropped more than 5 positions in 14 days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stuck recommendations&lt;/strong&gt;: same issue flagged 3+ days in a row, still unactioned&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code change impact&lt;/strong&gt;: did clicks or position change after a specific deploy?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Slack report reflects this. Instead of &lt;em&gt;"your CTR is 18.94%"&lt;/em&gt;, it says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Your site has more than doubled daily clicks over the past month (106% growth), driven by a metadata fix on March 26 and header overlap fixes on March 28. Short-term momentum is slowing 7-day clicks are -3% suggesting you need to now expand content around fast-moving branded keywords."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a different class of insight.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Audit Scrape SEO/GEO/AEO Scoring
&lt;/h2&gt;

&lt;p&gt;Beyond GSC data, &lt;code&gt;audit-scraper.js&lt;/code&gt; fetches your actual pages daily and scores them across three dimensions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SEO&lt;/strong&gt; classic signals: title tag, meta description, H1, canonical, OG tags, schema markup, JS-gated content detection&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GEO&lt;/strong&gt; (Generative Engine Optimization) how well AI search engines like Perplexity or ChatGPT Search can read and cite your content: structured data presence, content density, crawlability&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AEO&lt;/strong&gt; (Answer Engine Optimization) featured snippet and voice search readiness: FAQ schema, article schema, H2 density, word count&lt;/p&gt;

&lt;p&gt;Each page gets a score out of 10. The system flags critical issues (JS-gated content = crawlers see a blank page, missing H1 = no primary ranking signal) and sends a separate Slack message with the audit digest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🔴 SEO Audit — 2026-04-08
Scores: SEO 9/10 | GEO 9/10 | AEO 3/10 | Combined 21/30

Critical Issues:
🚨 missing_h1 on Pricing — Missing primary ranking signal
🚨 js_gated_content on Pricing — Crawlers see blank page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Code Change Impact Tracking
&lt;/h2&gt;

&lt;p&gt;This is the part I'm most proud of. &lt;code&gt;git-change-detector.js&lt;/code&gt; scans git commits and classifies them it looks for commit messages mentioning SEO-related terms (metadata, schema, redirect, canonical, performance, etc.) and logs them with their date.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;change-impact-tracker.js&lt;/code&gt; then cross-references those commits with GSC metrics. For each logged change, it compares the 7-day window before vs after deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ Migrate to new partition keys (2026-03-30)
   → Position improved 6.7 spots (17.39 → 10.71)

✅ API pagination fix (2026-03-18)
   → Clicks grew 81% (127 → 229.7/day)

⏳ content deploy (2026-03-13)
   → Monitoring... (not enough post-deploy data yet)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This surfaces directly in the Slack report under "Code Change Tracker". Over time, it tells you which types of changes actually move the needle.&lt;/p&gt;




&lt;h2&gt;
  
  
  Storage Architecture
&lt;/h2&gt;

&lt;p&gt;Three layers, each with a different purpose:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What it stores&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;Neo4j (Azure VM)&lt;/td&gt;
&lt;td&gt;Graph nodes + edges — keywords, positions, code changes, relationships&lt;/td&gt;
&lt;td&gt;Multi-hop reasoning: "which keyword improved after which deploy?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChromaDB (Azure VM)&lt;/td&gt;
&lt;td&gt;Vector embeddings of all entities&lt;/td&gt;
&lt;td&gt;Semantic search across history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cognee-knowledge.json (Azure Blob)&lt;/td&gt;
&lt;td&gt;30-day rolling JSON snapshots&lt;/td&gt;
&lt;td&gt;Fast daily reads without querying the graph every run&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The JSON file is the workhorse for the daily Slack report. Neo4j and ChromaDB are queried for deeper pattern analysis and become increasingly valuable as history accumulates.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Things I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cognee initializes config at import time.&lt;/strong&gt; If you set environment variables after &lt;code&gt;import cognee&lt;/code&gt;, they're ignored. You have to call &lt;code&gt;cognee.config.set_graph_db_config()&lt;/code&gt; directly after import to update the live config object. This cost me several hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;mistralai&lt;/code&gt; import conflict.&lt;/strong&gt; Cognee's dependency &lt;code&gt;instructor==1.14.x&lt;/code&gt; tries to import &lt;code&gt;Mistral&lt;/code&gt; from &lt;code&gt;mistralai&lt;/code&gt; at import time regardless of whether you use it. Fix: inject a fake &lt;code&gt;mistralai&lt;/code&gt; module into &lt;code&gt;sys.modules&lt;/code&gt; before importing Cognee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JS-gated content is invisible to the audit scraper.&lt;/strong&gt; If your page renders entirely client-side, the raw HTML fetch returns fewer than 80 words. The scraper flags this as &lt;code&gt;js_gated_content&lt;/code&gt; — which is actually useful because it means Google probably can't index it either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The knowledge graph gets smarter non-linearly.&lt;/strong&gt; Day 1 the system is just a fancier GSC dashboard. Day 7 you start seeing real code change verdicts. Day 30 the AI recommendations start referencing patterns that span weeks. The value compounds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions&lt;/strong&gt; — pipeline orchestration, daily cron&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt; — audit scraper, Cognee analyzer, Slack formatting, git change detection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt; — Cognee SDK ingestion (cognee_ingest.py, audit-ingest.py)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cognee 0.5.3&lt;/strong&gt; — knowledge graph SDK&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neo4j Community&lt;/strong&gt; — graph database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ChromaDB&lt;/strong&gt; — vector database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure OpenAI&lt;/strong&gt; — GPT-4.1 for analysis, text-embedding-3-large for vectors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Blob Storage&lt;/strong&gt; — knowledge graph backup/restore&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure VM (Standard B2s)&lt;/strong&gt; — hosts Neo4j + ChromaDB via Docker Compose&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Search Console API&lt;/strong&gt; — real click/impression/position data&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>data</category>
      <category>automation</category>
    </item>
    <item>
      <title>🚀 Beyond RAG: Simulating the Future with MiroFish</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Tue, 07 Apr 2026 17:15:58 +0000</pubDate>
      <link>https://dev.to/techwithhari/beyond-rag-simulating-the-future-with-mirofish-1dal</link>
      <guid>https://dev.to/techwithhari/beyond-rag-simulating-the-future-with-mirofish-1dal</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv6ddqtlaihgvsly4jv6f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv6ddqtlaihgvsly4jv6f.png" alt=" " width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr7th9i8ljlisllk66m6w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr7th9i8ljlisllk66m6w.png" alt=" " width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lately, most of us have been working with RAG systems retrieving context, grounding responses, improving accuracy.&lt;/p&gt;

&lt;p&gt;But what if instead of just &lt;em&gt;retrieving knowledge&lt;/em&gt;, we could &lt;strong&gt;simulate outcomes&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;I recently came across &lt;strong&gt;MiroFish&lt;/strong&gt;, and decided to test it out.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 What I Tried
&lt;/h2&gt;

&lt;p&gt;I cloned the repo, ran it locally, and fed it a simple scenario:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What happens when an AI assistant is introduced into a company’s daily workflow?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of a static answer, it generated a &lt;strong&gt;multi-agent simulation over time&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 What Makes It Different
&lt;/h2&gt;

&lt;p&gt;Unlike traditional systems, MiroFish:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creates a &lt;strong&gt;virtual environment&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Generates multiple &lt;strong&gt;agents (employees, managers, etc.)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Simulates &lt;strong&gt;interactions over time&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Produces a &lt;strong&gt;temporal report (day-by-day evolution)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means you’re not just asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What will happen?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You’re observing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How things evolve step by step.”&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  📊 Sample Insights from My Test
&lt;/h2&gt;

&lt;p&gt;From a 14-day simulation, I observed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📈 Initial boost in productivity&lt;/li&gt;
&lt;li&gt;⚖️ Diverging employee satisfaction&lt;/li&gt;
&lt;li&gt;🔁 Emerging dependency on AI&lt;/li&gt;
&lt;li&gt;🧩 Different behaviors across teams&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It felt less like querying an LLM… and more like watching a system evolve.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Where This Can Be Useful
&lt;/h2&gt;

&lt;p&gt;This kind of simulation opens up interesting possibilities:&lt;/p&gt;

&lt;h3&gt;
  
  
  🏢 Organization &amp;amp; Product
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;AI adoption strategies&lt;/li&gt;
&lt;li&gt;Remote work policy changes&lt;/li&gt;
&lt;li&gt;Feature rollout impact&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📦 Business Decisions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Pricing experiments&lt;/li&gt;
&lt;li&gt;Customer behavior prediction&lt;/li&gt;
&lt;li&gt;Growth strategy testing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🌍 Macro Scenarios
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Economic shifts&lt;/li&gt;
&lt;li&gt;Supply chain disruptions&lt;/li&gt;
&lt;li&gt;Policy or geopolitical changes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔄 RAG vs Simulation (My Take)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RAG&lt;/td&gt;
&lt;td&gt;Retrieves and explains existing knowledge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simulation (MiroFish)&lt;/td&gt;
&lt;td&gt;Models and predicts possible futures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both are powerful but they solve very different problems.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ Final Thoughts
&lt;/h2&gt;

&lt;p&gt;We’re slowly moving from:&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;“Answering questions”&lt;/em&gt;&lt;br&gt;
to&lt;br&gt;
👉 &lt;em&gt;“Rehearsing decisions”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;MiroFish feels like an early step in that direction.&lt;/p&gt;

&lt;p&gt;Still experimenting, but this approach definitely opens up a new way of thinking about AI systems.&lt;/p&gt;




&lt;p&gt;If you’ve tried something similar or have ideas for scenarios to test — would love to hear 👇&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>machinelearning</category>
      <category>data</category>
    </item>
    <item>
      <title>Everyone Suddenly Said “RAG is Dead”</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Sat, 04 Apr 2026 13:41:22 +0000</pubDate>
      <link>https://dev.to/techwithhari/everyone-suddenly-said-rag-is-dead-2k37</link>
      <guid>https://dev.to/techwithhari/everyone-suddenly-said-rag-is-dead-2k37</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fes0btfad6ekd3zumkwdp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fes0btfad6ekd3zumkwdp.png" alt=" " width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh86r7ktdp3qr8c84f9do.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh86r7ktdp3qr8c84f9do.png" alt=" " width="800" height="145"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxpf2c31823bu4994y4gt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxpf2c31823bu4994y4gt.png" alt=" " width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lately I keep seeing this everywhere:&lt;/p&gt;

&lt;p&gt;“RAG is dead”&lt;br&gt;
“Vector search is outdated”&lt;br&gt;
“Reasoning-based retrieval is the future”&lt;/p&gt;

&lt;p&gt;And suddenly… everyone is talking like vector search is useless.&lt;/p&gt;

&lt;p&gt;I’m not against the hype. These things happen.&lt;/p&gt;

&lt;p&gt;But honestly, this whole idea didn’t just click for me immediately.&lt;/p&gt;

&lt;p&gt;Because for me, this problem was already in my head for a long time.&lt;/p&gt;

&lt;p&gt;Not because of hype.&lt;/p&gt;

&lt;p&gt;Just because of my use case.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;What I Was Actually Trying to Figure Out&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I read a lot of long tech blogs and architecture posts.&lt;/p&gt;

&lt;p&gt;After reading, I always have questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“why did they do this?”&lt;/li&gt;
&lt;li&gt;“what’s the tradeoff here?”&lt;/li&gt;
&lt;li&gt;“what happens if we change this design?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I wanted a system where I can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;paste a document&lt;/li&gt;
&lt;li&gt;ask questions&lt;/li&gt;
&lt;li&gt;actually get useful answers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At some point I started thinking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;should I just stick with vector RAG?&lt;br&gt;
or should I try something like PageIndex / reasoning-based retrieval?&lt;br&gt;
or even something like Agent-style flow later?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That curiosity is what pushed me to build this.&lt;/p&gt;

&lt;p&gt;Not the “RAG is dead” trend.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;So I Built a Simple Comparison&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Nothing fancy.&lt;/p&gt;

&lt;p&gt;Just a small app where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;same document&lt;/li&gt;
&lt;li&gt;same question&lt;/li&gt;
&lt;li&gt;same model&lt;/li&gt;
&lt;li&gt;only retrieval changes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Pipeline 1 — Vector RAG&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;split document&lt;/li&gt;
&lt;li&gt;embed&lt;/li&gt;
&lt;li&gt;store in ChromaDB&lt;/li&gt;
&lt;li&gt;retrieve top-k&lt;/li&gt;
&lt;li&gt;answer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is what most of us are already doing.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Pipeline 2 — PageIndex&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;build a tree structure from the document&lt;/li&gt;
&lt;li&gt;let the model navigate it&lt;/li&gt;
&lt;li&gt;pick relevant sections&lt;/li&gt;
&lt;li&gt;answer from that&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This felt very different.&lt;/p&gt;

&lt;p&gt;Not “searching”.&lt;/p&gt;

&lt;p&gt;More like… “reading with guidance”.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;What I Noticed&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The difference is actually deeper than I expected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vector RAG:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;find similar chunks&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;blockquote&gt;
&lt;p&gt;figure out &lt;em&gt;where&lt;/em&gt; the answer should be, then go there&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That “where” part is interesting.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;One Example&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I tested with a Netflix architecture article.&lt;/p&gt;

&lt;p&gt;Question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why did they use live origin instead of only CDN?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Vector RAG
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;faster (~7s)&lt;/li&gt;
&lt;li&gt;decent answer&lt;/li&gt;
&lt;li&gt;but retrieval had some noise&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  PageIndex
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;slower (~11s)&lt;/li&gt;
&lt;li&gt;answer felt more precise&lt;/li&gt;
&lt;li&gt;citations were cleaner&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;My Honest Take&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Vector RAG is not dead.&lt;/p&gt;

&lt;p&gt;But…&lt;/p&gt;

&lt;p&gt;Blind chunking + embedding + top-k is not enough anymore (at least for some cases).&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Where I See the Difference&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Vector RAG works well when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you have multiple documents&lt;/li&gt;
&lt;li&gt;you want speed&lt;/li&gt;
&lt;li&gt;you just need “good enough” answers&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;PageIndex works well when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;single long document&lt;/li&gt;
&lt;li&gt;structured content&lt;/li&gt;
&lt;li&gt;you want cleaner reasoning&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;What I’m Actually Thinking Now&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I don’t think this is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“one replaces the other”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Feels more like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;both solve different parts of the problem&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What I’m more interested in now is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;can I combine them?&lt;/li&gt;
&lt;li&gt;use vector search to find documents&lt;/li&gt;
&lt;li&gt;then use something like PageIndex inside that?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That feels more practical.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Why I’m Exploring This&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For my use case, I’m also thinking about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;can I plug this into an agent flow later?&lt;/li&gt;
&lt;li&gt;how does retrieval affect agent decisions?&lt;/li&gt;
&lt;li&gt;does better retrieval reduce hallucination in multi-step tasks?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s where this is going for me.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Final Thought&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Honestly, I didn’t build this to prove anything.&lt;/p&gt;

&lt;p&gt;Just to understand.&lt;/p&gt;

&lt;p&gt;And one thing became clear very fast:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;hype says “X is dead”&lt;br&gt;
reality says “it depends”&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;If you’re building something similar, I’d really suggest:&lt;/p&gt;

&lt;p&gt;Don’t pick a side early.&lt;/p&gt;

&lt;p&gt;Test both.&lt;/p&gt;

&lt;p&gt;You’ll understand the difference immediately.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>algorithms</category>
      <category>rag</category>
    </item>
    <item>
      <title>Harness Engineering: The Concept I Didn't Know I Needed</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Wed, 25 Mar 2026 18:24:15 +0000</pubDate>
      <link>https://dev.to/techwithhari/harness-engineering-the-concept-i-didnt-know-i-needed-5nf</link>
      <guid>https://dev.to/techwithhari/harness-engineering-the-concept-i-didnt-know-i-needed-5nf</guid>
      <description>&lt;p&gt;Honestly, when I first heard the term &lt;strong&gt;Harness Engineering&lt;/strong&gt;, I thought it was just another buzzword.&lt;/p&gt;

&lt;p&gt;I already knew about Prompt Engineering. I had heard about Context Engineering. I thought, okay this is probably just the same thing with a fancier name.&lt;/p&gt;

&lt;p&gt;But then I started actually using agentic tools like Cursor and Windsurf in my day-to-day work. And something clicked.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Wait... this thing is not just answering my question. It's planning, building, testing, fixing — all on its own. How?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's when I went deeper. And what I found actually changed how I think about building with AI.&lt;/p&gt;




&lt;h2&gt;
  
  
  First What Even is a Context Window?
&lt;/h2&gt;

&lt;p&gt;Before we get into Harness Engineering, need to understand one thing.&lt;/p&gt;

&lt;p&gt;Every AI model has something called a &lt;strong&gt;context window&lt;/strong&gt;. Think of it like a whiteboard. The model can only see what's written on that whiteboard right now. Once the conversation gets too long, old stuff disappears. And when you start a brand new chat the whiteboard is completely blank.&lt;/p&gt;

&lt;p&gt;That's the core problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AI has no memory between sessions. Every new session, it starts fresh.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a simple question answer task, that's fine. But what if the task takes &lt;em&gt;days&lt;/em&gt;?&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Harness Engineering?
&lt;/h2&gt;

&lt;p&gt;Let me show you how this concept evolved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prompt Engineering   → How do I ask better questions?
Context Engineering  → How do I manage what's inside one session?
Harness Engineering  → How do I make an agent work across many sessions?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Harness Engineering&lt;/strong&gt; is not about writing better prompts. It's about designing the &lt;em&gt;system around the model&lt;/em&gt; so the agent always knows where it is, what it has done, and what it needs to do next. Even after the context window resets completely.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Moment I Really Got It
&lt;/h2&gt;

&lt;p&gt;When I was exploring how tools like Cursor work under the hood, I realized something.&lt;/p&gt;

&lt;p&gt;When Cursor builds a feature for you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It scans your codebase&lt;/li&gt;
&lt;li&gt;Makes a plan&lt;/li&gt;
&lt;li&gt;Implements step by step&lt;/li&gt;
&lt;li&gt;Runs tests automatically&lt;/li&gt;
&lt;li&gt;Fixes bugs it finds&lt;/li&gt;
&lt;li&gt;Continues without you prompting every single move&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is Harness Engineering. The tool is not just "smart." Someone designed a system that makes it &lt;em&gt;stay on track&lt;/em&gt; even as context windows reset.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Real Example: Building an App with an Agent
&lt;/h2&gt;

&lt;p&gt;Let's say you ask an AI agent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Build me a complete Food Delivery App."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is not a one-session task. Here's what happens &lt;strong&gt;without&lt;/strong&gt; any harness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Session 1:
Agent builds Login page, starts Restaurant list...
Context window fills up. Stops.

Session 2:
Agent starts fresh. No memory.
Builds Login page again. 😵
Duplicate code. Broken app. Confused agent.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now with &lt;strong&gt;Harness Engineering&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Before any coding starts, an &lt;strong&gt;Initializer Agent&lt;/strong&gt; sets up three simple things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;features.json&lt;/strong&gt; — Every task with a status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"task"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Login Page"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"task"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Restaurant List"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"task"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cart System"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;progress.txt&lt;/strong&gt; — A running log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Last completed: Nothing yet
Next task: Login Page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;setup.sh&lt;/strong&gt; — A script to spin up the dev server automatically.&lt;/p&gt;

&lt;p&gt;Now every new session just does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read progress.txt  → know where to continue
Read features.json → pick the next pending task
Run setup.sh       → environment is ready
Build → Test → Update files → Git commit
Session ends cleanly. Next session picks up exactly here.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent has &lt;strong&gt;no memory&lt;/strong&gt; but it doesn't need memory. The &lt;em&gt;system&lt;/em&gt; remembers for it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 3 Things That Actually Make This Work
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Legible Environment
&lt;/h3&gt;

&lt;p&gt;Every session should be able to answer three questions just by reading files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is the goal?&lt;/li&gt;
&lt;li&gt;What is done?&lt;/li&gt;
&lt;li&gt;What is next?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feature lists, progress logs, git history, docs — these are not optional. They are the foundation.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Verification Before Moving On
&lt;/h3&gt;

&lt;p&gt;Agents have a habit of saying "Done!" when things are actually broken. I've seen this personally with Claude Code and Cursor.&lt;/p&gt;

&lt;p&gt;The fix is giving the agent real tools to &lt;em&gt;test its own work&lt;/em&gt; like running the app, checking the UI, catching bugs end to end. Not just saying it worked. Actually proving it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Use Simple, Familiar Tools
&lt;/h3&gt;

&lt;p&gt;This one surprised me the most.&lt;/p&gt;

&lt;p&gt;Vercel built a very fancy, specialized agent with custom tools and heavy prompt engineering. It worked but barely. Fragile. Slow.&lt;/p&gt;

&lt;p&gt;Then they removed almost all the custom tools and replaced everything with one simple batch command tool.&lt;/p&gt;

&lt;p&gt;Result? &lt;strong&gt;3.5x faster. 37% fewer tokens. Success rate went from 80% to 100%.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why? Because models like Claude have seen billions of lines of code using &lt;code&gt;git&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;npm&lt;/code&gt;. They understand these natively. Custom tools are unfamiliar territory.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Simple tools the model already knows &amp;gt; Fancy tools you built from scratch.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  How This Connects to MCP
&lt;/h2&gt;

&lt;p&gt;If you've worked with MCP (Model Context Protocol) before, this connects directly.&lt;/p&gt;

&lt;p&gt;In a Harness Engineering setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;Host&lt;/strong&gt; (Claude Desktop, Cursor) is your computer&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;MCP Client&lt;/strong&gt; is like an adapter built into the host, you don't touch it&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;MCP Server&lt;/strong&gt; is what &lt;em&gt;you&lt;/em&gt; build your custom tools, your file readers, your test runners&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your MCP Server becomes the hands of your long-running agent. It reads progress files, runs tests, queries databases, and verifies work all between sessions.&lt;/p&gt;

&lt;p&gt;You only build the server. The host handles the rest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes to Avoid
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Letting the agent "one-shot" the whole task it will run out of context and leave things half done&lt;/li&gt;
&lt;li&gt;Not giving the agent a way to test its own work it will always claim success&lt;/li&gt;
&lt;li&gt;Building overly specialized tools simpler is almost always better&lt;/li&gt;
&lt;li&gt;No clean state at end of each session the next session will be confused&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  A Simple Harness Checklist
&lt;/h2&gt;

&lt;p&gt;Before building a long-running agent system, make sure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Feature list exists with pass/fail status per task&lt;/li&gt;
&lt;li&gt;[ ] Progress file updated at end of every session&lt;/li&gt;
&lt;li&gt;[ ] Git commits made with descriptive messages&lt;/li&gt;
&lt;li&gt;[ ] Dev environment spins up automatically (setup script)&lt;/li&gt;
&lt;li&gt;[ ] Agent has real testing tools not just unit tests&lt;/li&gt;
&lt;li&gt;[ ] Generic tools used wherever possible&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The models today are genuinely capable. The missing piece is almost never the model itself.&lt;/p&gt;

&lt;p&gt;It's the system around it.&lt;/p&gt;

&lt;p&gt;That's what Harness Engineering is. Not a new model. Not a new prompt trick. Just smart system design that lets an agent stay on track across sessions, verify its own work, and actually finish what it started.&lt;/p&gt;

&lt;p&gt;Once I understood this, the way I think about building AI-powered tools completely changed.&lt;/p&gt;

&lt;p&gt;If you're building anything agentic even something small think about what happens when the context window resets. Does your agent know how to pick up where it left off?&lt;/p&gt;

&lt;p&gt;If yes, you're already doing Harness Engineering. 😊&lt;/p&gt;




</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Why Your PostgreSQL Keeps Running Out of Connections</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Tue, 17 Mar 2026 17:31:11 +0000</pubDate>
      <link>https://dev.to/techwithhari/why-your-postgresql-keeps-running-out-of-connections-5ceo</link>
      <guid>https://dev.to/techwithhari/why-your-postgresql-keeps-running-out-of-connections-5ceo</guid>
      <description>&lt;p&gt;PostgreSQL connection errors are one of those things that look terrifying when they hit production.&lt;/p&gt;

&lt;p&gt;I used to think:&lt;br&gt;
"Why is the database refusing connections?"&lt;br&gt;
"Did something crash?"&lt;br&gt;
"Is the server overloaded?" 😅&lt;/p&gt;

&lt;p&gt;Recently, while working on a production system, I ran into the classic &lt;code&gt;TooManyConnectionsError&lt;/code&gt;. Not once twice. On two different services. Same database, same root cause.&lt;/p&gt;

&lt;p&gt;That experience helped me clearly understand why this happens and how to fix it properly.&lt;/p&gt;

&lt;p&gt;This post is me breaking that down in a simple way, based on what actually worked.&lt;/p&gt;
&lt;h2&gt;
  
  
  What does TooManyConnectionsError actually mean?
&lt;/h2&gt;

&lt;p&gt;In simple terms, PostgreSQL has a limit on how many connections it allows at the same time.&lt;/p&gt;

&lt;p&gt;This limit is set by &lt;code&gt;max_connections&lt;/code&gt; and is usually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;50–100 for small/basic tiers&lt;/li&gt;
&lt;li&gt;100–200 for general purpose tiers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When your application tries to open more connections than this limit, PostgreSQL says no. That's the error.&lt;/p&gt;

&lt;p&gt;The important thing to understand:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The database isn't down. It just has no room for new connections.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  But I'm using a connection pool... why is this still happening?
&lt;/h2&gt;

&lt;p&gt;This is the part that confused me.&lt;/p&gt;

&lt;p&gt;I was already using a connection pool. Every tutorial says "use a pool" and I did. So why was I still exhausting connections?&lt;/p&gt;

&lt;p&gt;Here's what I found out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem wasn't that I didn't have a pool. The problem was I had too many pools.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let me explain.&lt;/p&gt;
&lt;h2&gt;
  
  
  How the bug actually works
&lt;/h2&gt;

&lt;p&gt;Most people write their database client class something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DatabaseClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_pool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks clean right? Every instance gets its own pool. Professional.&lt;/p&gt;

&lt;p&gt;But here's what happens in a real application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Health checker creates a &lt;code&gt;DatabaseClient()&lt;/code&gt; → pool of 10&lt;/li&gt;
&lt;li&gt;Dependency checker creates a &lt;code&gt;DatabaseClient()&lt;/code&gt; → another pool of 10&lt;/li&gt;
&lt;li&gt;Each worker/task creates a &lt;code&gt;DatabaseClient()&lt;/code&gt; → another pool of 10 each&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have 6 workers, that's already:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;10 + 10 + (6 × 10) = 80 connections from ONE container&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Run 2 containers? That's 160.&lt;/p&gt;

&lt;p&gt;Your database allows 100.&lt;/p&gt;

&lt;p&gt;💥 &lt;code&gt;TooManyConnectionsError&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And the worst part? Each pool &lt;em&gt;individually&lt;/em&gt; looks reasonable. It's only when you add them all up that it explodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is embarrassingly simple
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One process, one pool. Everything borrows from it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of each class creating its own pool, create ONE pool at the module/process level and every instance uses that shared pool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Created once at module level
&lt;/span&gt;&lt;span class="n"&gt;_shared_pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_shared_pool&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_shared_pool&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_shared_pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_pool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_shared_pool&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DatabaseClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;get_shared_pool&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it doesn't matter how many &lt;code&gt;DatabaseClient()&lt;/code&gt; instances you create. They all share the same 5 connections.&lt;/p&gt;

&lt;p&gt;That's it. That's the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I learned the hard way
&lt;/h2&gt;

&lt;p&gt;Here are some gotchas that bit me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The close() trap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your client class has a &lt;code&gt;close()&lt;/code&gt; method that closes the pool, and some other code calls it mid-process congratulations, you just killed the pool for everyone.&lt;/p&gt;

&lt;p&gt;Make &lt;code&gt;close()&lt;/code&gt; a no-op on individual instances. Only close the shared pool when the entire process shuts down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The cascade effect&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When the database runs out of connections, it doesn't just fail your query. It also fails your health check. And when the health check fails, your orchestrator thinks the container is unhealthy and might restart it. Which creates new pools. Which makes things worse.&lt;/p&gt;

&lt;p&gt;I literally got two alerts 30 seconds apart. First one: &lt;code&gt;TooManyConnectionsError&lt;/code&gt;. Second one: &lt;code&gt;dependency_check_failed&lt;/code&gt;. Same container, same root cause. One bug, two pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Make pool size configurable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use an environment variable like &lt;code&gt;PG_POOL_MAX_SIZE=5&lt;/code&gt;. When you're debugging at 2 AM, you don't want to redeploy just to change a number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Do the napkin math&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before deploying, always calculate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pool_max_size × max_replicas &amp;lt; max_connections - admin_headroom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pool max: 5&lt;/li&gt;
&lt;li&gt;Total replicas across all services: 10&lt;/li&gt;
&lt;li&gt;Total: 50&lt;/li&gt;
&lt;li&gt;Database max_connections: 100&lt;/li&gt;
&lt;li&gt;Admin headroom: 20&lt;/li&gt;
&lt;li&gt;50 &amp;lt; 80 ✅&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the math doesn't work, either reduce pool sizes or put a connection pooler like PgBouncer in front of the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes I see (and made myself)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Creating a new pool per class instance instead of sharing one&lt;/li&gt;
&lt;li&gt;Not accounting for multiple services hitting the same database&lt;/li&gt;
&lt;li&gt;Forgetting that container scaling = more pools = more connections&lt;/li&gt;
&lt;li&gt;Not closing pools gracefully on shutdown (idle connections linger)&lt;/li&gt;
&lt;li&gt;No rollback plan when the fix itself has a bug 😅&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A simple checklist before you deploy
&lt;/h2&gt;

&lt;p&gt;Before pushing connection pool changes to production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Single shared pool at module/process level&lt;/li&gt;
&lt;li&gt;✅ Pool size is configurable via env variable&lt;/li&gt;
&lt;li&gt;✅ &lt;code&gt;close()&lt;/code&gt; on individual instances is a no-op&lt;/li&gt;
&lt;li&gt;✅ Shared pool closes on process shutdown&lt;/li&gt;
&lt;li&gt;✅ Napkin math checks out&lt;/li&gt;
&lt;li&gt;✅ Code compiles/passes syntax check (trust me on this one 😅)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Connection pool exhaustion doesn't have to be scary.&lt;/p&gt;

&lt;p&gt;If I had to summarize everything in one line:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Many pools = trouble. One shared pool = peace.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The bug is almost never that you forgot to use a pool. It's that you accidentally created too many of them.&lt;/p&gt;

&lt;p&gt;Once you understand this, &lt;code&gt;TooManyConnectionsError&lt;/code&gt; stops being a 1 AM panic and becomes just another thing you know how to handle.&lt;/p&gt;

&lt;p&gt;Hope this helps someone who just got paged for the first time because their database "ran out of connections"... 🙂&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>devops</category>
      <category>python</category>
      <category>cloud</category>
    </item>
    <item>
      <title>EnvHub: A Zero-Knowledge Secret Manager Built with GitHub Copilot CLI</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Sun, 15 Feb 2026 16:11:25 +0000</pubDate>
      <link>https://dev.to/techwithhari/envhub-a-zero-knowledge-secret-manager-built-with-github-copilot-cli-3akc</link>
      <guid>https://dev.to/techwithhari/envhub-a-zero-knowledge-secret-manager-built-with-github-copilot-cli-3akc</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-01-21"&gt;GitHub Copilot CLI Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I built &lt;strong&gt;EnvHub&lt;/strong&gt; a secure, versioned environment variable manager that finally treats your &lt;code&gt;.env&lt;/code&gt; files with the same respect as your code.&lt;/p&gt;

&lt;p&gt;Show the Full Application flow here &lt;a href="https://youtu.be/54do2TvHB3Y" rel="noopener noreferrer"&gt;https://youtu.be/54do2TvHB3Y&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But before I show you what it does, let me tell you &lt;em&gt;why&lt;/em&gt; it exists.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem That Kept Happening
&lt;/h2&gt;

&lt;p&gt;Every developer has done this at least once:&lt;/p&gt;

&lt;p&gt;You're working on a feature. You pull the latest code. You run the app. It breaks. You spend 30 minutes debugging only to realize someone added a new environment variable and forgot to tell the team.&lt;/p&gt;

&lt;p&gt;Or worse: you accidentally overwrote your &lt;code&gt;.env&lt;/code&gt; with an old version and replaced new config with outdated values. Production keys? Gone. New API endpoints? Reverted.&lt;/p&gt;

&lt;p&gt;I've lost count of how many times this happened to me and my team:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;"Hey, can you Slack me the new &lt;code&gt;.env&lt;/code&gt;?"&lt;/em&gt; Insecure and lazy&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"Wait, which version of the &lt;code&gt;.env&lt;/code&gt; are you using?"&lt;/em&gt; Pure chaos&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"Who changed the DATABASE_URL and when?"&lt;/em&gt; No audit trail whatsoever&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"I just overwrote the new config with my old &lt;code&gt;.env&lt;/code&gt;..."&lt;/em&gt; Silent disasters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We treat code with version control with Git, pull requests, code reviews. But we treat our most sensitive configuration database passwords, API keys, encryption secrets like scratch paper.&lt;/p&gt;

&lt;p&gt;That's insane.&lt;/p&gt;

&lt;p&gt;So I decided to build something better.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Vision: Git, But For Secrets
&lt;/h2&gt;

&lt;p&gt;I wanted a tool that worked the way developers already think:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Push&lt;/strong&gt; your &lt;code&gt;.env&lt;/code&gt; to a central place (like &lt;code&gt;git push&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull&lt;/strong&gt; it down on any machine (like &lt;code&gt;git pull&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History&lt;/strong&gt; of every change with who, what, and when (like &lt;code&gt;git log&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encrypted&lt;/strong&gt; so even if someone accesses the storage, they can't read the secrets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And critically: &lt;strong&gt;Zero-knowledge architecture.&lt;/strong&gt; I didn't want to build another SaaS where I hold everyone's secrets. That's a liability nightmare and honestly, I wouldn't trust a random developer with my production credentials either.&lt;/p&gt;

&lt;p&gt;The solution? &lt;strong&gt;You deploy EnvHub to YOUR Vercel account, with YOUR storage, encrypted with YOUR keys.&lt;/strong&gt; I literally cannot access your data even if I wanted to.&lt;/p&gt;




&lt;h2&gt;
  
  
  What EnvHub Actually Does
&lt;/h2&gt;

&lt;p&gt;Let me walk you through the complete experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Web Dashboard
&lt;/h3&gt;

&lt;p&gt;When you first log in (via GitHub OAuth no new passwords to remember), you see a clean, dark-themed dashboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftiqowqifrtm2831smu90.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftiqowqifrtm2831smu90.png" alt=" " width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the left sidebar, you have your &lt;strong&gt;Workspace Explorer&lt;/strong&gt;. It's organized hierarchically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;📁 Project (e.g., "my-startup")
   └── 🖥️ Service (e.g., "backend-api")
       └── 🔵 Environment (e.g., "production")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click on any environment, and you see all your variables displayed clearly keys and values visible right there, no extra clicks needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frlkcdyhzizsx1jc9bbro.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frlkcdyhzizsx1jc9bbro.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Want to edit? Click the edit button, and you get a full editor. You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add variables individually&lt;/strong&gt; one at a time with key-value inputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bulk upload&lt;/strong&gt; paste your entire &lt;code&gt;.env&lt;/code&gt; file content at once&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every save requires a &lt;strong&gt;change reason&lt;/strong&gt; because six months from now, you'll want to know why someone changed the &lt;code&gt;STRIPE_API_KEY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ok68s32vriiz42j5gz3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ok68s32vriiz42j5gz3.png" alt=" " width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Below the variables, you'll see the &lt;strong&gt;Version History&lt;/strong&gt; table. Every single change is recorded:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;User&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v3&lt;/td&gt;
&lt;td&gt;Feb 14, 2026&lt;/td&gt;
&lt;td&gt;@harivelu0&lt;/td&gt;
&lt;td&gt;Updated Stripe to production keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v2&lt;/td&gt;
&lt;td&gt;Feb 10, 2026&lt;/td&gt;
&lt;td&gt;@teammate&lt;/td&gt;
&lt;td&gt;Added Redis connection string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v1&lt;/td&gt;
&lt;td&gt;Feb 1, 2026&lt;/td&gt;
&lt;td&gt;@harivelu0&lt;/td&gt;
&lt;td&gt;Initial setup&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbwuas2sme7sgemy5sknc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbwuas2sme7sgemy5sknc.png" alt=" " width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on any version to view what the variables looked like at that point in time. &lt;strong&gt;Full time-travel debugging.&lt;/strong&gt; Accidentally overwrote something? Just check the previous version.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnwhd2x8s3ba0u29hfi5e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnwhd2x8s3ba0u29hfi5e.png" alt=" " width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The CLI (Where the Real Power Lives)
&lt;/h3&gt;

&lt;p&gt;The web dashboard is great for browsing and quick edits. But for developers who live in the terminal, the CLI is where it gets powerful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install it with one command:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;https://your-envhub-instance.vercel.app/cli/envhub_cli-2.0.3-py3-none-any.whl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Initialize it to point to your instance:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;envhub init &lt;span class="nt"&gt;--api-url&lt;/span&gt; https://your-envhub-instance.vercel.app/api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why do we need this step? Because EnvHub is &lt;strong&gt;self-hosted&lt;/strong&gt; everyone deploys their own instance. Your company's EnvHub might live at &lt;code&gt;envhub.mycompany.com&lt;/code&gt; while another team uses &lt;code&gt;secrets.startup.io&lt;/code&gt;. The init command tells the CLI where YOUR backend lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Login using your existing GitHub credentials:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;envhub login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This uses the GitHub CLI (&lt;code&gt;gh&lt;/code&gt;) under the hood, so if you're already logged into &lt;code&gt;gh&lt;/code&gt;, you're good to go. No new passwords, no separate accounts.&lt;/p&gt;

&lt;p&gt;Now you're ready.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push your local .env to the cloud:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;envhub push &lt;span class="nt"&gt;-p&lt;/span&gt; my-startup &lt;span class="nt"&gt;-s&lt;/span&gt; backend-api &lt;span class="nt"&gt;-e&lt;/span&gt; production &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"Added new payment gateway keys"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzp5th3s1a26it88evpay.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzp5th3s1a26it88evpay.png" alt=" " width="800" height="142"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-r&lt;/code&gt; flag is the change reason required, so your team always knows why things changed. &lt;code&gt;-p&lt;/code&gt; flag refers project name, &lt;code&gt;-s&lt;/code&gt; refers service and &lt;code&gt;-e refers environment&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pull variables to any machine:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Print to console (great for reviewing or piping)&lt;/span&gt;
envhub pull &lt;span class="nt"&gt;-p&lt;/span&gt; my-startup &lt;span class="nt"&gt;-s&lt;/span&gt; backend-api &lt;span class="nt"&gt;-e&lt;/span&gt; production

&lt;span class="c"&gt;# Save directly to a file&lt;/span&gt;
envhub pull &lt;span class="nt"&gt;-p&lt;/span&gt; my-startup &lt;span class="nt"&gt;-s&lt;/span&gt; backend-api &lt;span class="nt"&gt;-e&lt;/span&gt; production &lt;span class="nt"&gt;-o&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, &lt;code&gt;pull&lt;/code&gt; outputs to the console. This is intentional  it lets you review what you're getting before saving it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;View the audit history:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;envhub &lt;span class="nb"&gt;history&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; my-startup &lt;span class="nt"&gt;-s&lt;/span&gt; backend-api &lt;span class="nt"&gt;-e&lt;/span&gt; production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┏━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Version ┃ Date                ┃ User        ┃ Reason                          ┃
┡━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ 3       │ 2026-02-14 09:30:00 │ @harivelu0  │ Added new payment gateway keys  │
│ 2       │ 2026-02-10 14:22:00 │ @teammate   │ Added Redis connection string   │
│ 1       │ 2026-02-01 11:00:00 │ @harivelu0  │ Initial setup                   │
└─────────┴─────────────────────┴─────────────┴─────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New developer joining the team? &lt;code&gt;envhub pull&lt;/code&gt;. Spinning up a new server? &lt;code&gt;envhub pull&lt;/code&gt;. Debugging why production broke last Tuesday? &lt;code&gt;envhub history&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Security Model (I Take This Seriously)
&lt;/h2&gt;

&lt;p&gt;Let me be clear about how EnvHub protects your secrets.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Zero-Knowledge Architecture
&lt;/h3&gt;

&lt;p&gt;When you deploy EnvHub, you deploy it to &lt;strong&gt;your own Vercel account&lt;/strong&gt;. Your data lives in &lt;strong&gt;your own Vercel Blob storage&lt;/strong&gt;. The encryption key is &lt;strong&gt;your own key&lt;/strong&gt; that you generate.&lt;/p&gt;

&lt;p&gt;I, as the creator of EnvHub, have absolutely zero access to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your Vercel account&lt;/li&gt;
&lt;li&gt;Your Blob storage&lt;/li&gt;
&lt;li&gt;Your encryption key&lt;/li&gt;
&lt;li&gt;Your secrets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even if someone subpoenas me, I literally cannot hand over your data because I don't have it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Encryption at Rest
&lt;/h3&gt;

&lt;p&gt;Every single variable value is encrypted using &lt;strong&gt;Fernet (AES-128 symmetric encryption)&lt;/strong&gt; before it's stored.&lt;/p&gt;

&lt;p&gt;Here's what actually gets saved in Vercel Blob:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4yizaq0wrmoormw67xmt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4yizaq0wrmoormw67xmt.png" alt=" " width="800" height="242"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Without your &lt;code&gt;ENVHUB_MASTER_KEY&lt;/code&gt;, those encrypted values are worthless.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Organization Gating
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jcyoqhbpzq1us70eu3z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jcyoqhbpzq1us70eu3z.png" alt=" " width="800" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Set the &lt;code&gt;ALLOWED_ORGS&lt;/code&gt; environment variable to your GitHub organization name, and only members of that organization can log in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ALLOWED_ORGS=my-company,partner-org
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Random person finds your EnvHub URL? They can't even access anything. Access denied.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Audit Everything
&lt;/h3&gt;

&lt;p&gt;Every single change records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who&lt;/strong&gt; made the change (GitHub username)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When&lt;/strong&gt; they made it (timestamp)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What&lt;/strong&gt; they changed (full variable snapshot)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt; they changed it (required change reason)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Six months later when production breaks, you'll know exactly who to ask and what changed.&lt;/p&gt;




&lt;h2&gt;
  
  
  How GitHub Copilot CLI Made This Possible
&lt;/h2&gt;

&lt;p&gt;Here's my situation: &lt;strong&gt;I work in DevOps.&lt;/strong&gt; I'm comfortable with infrastructure, CI/CD pipelines, cloud services, React, and Next.js. But building a distributable Python CLI tool? That was new territory for me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Copilot CLI was my guide through unfamiliar terrain.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The First Conversation: "How Do I Structure This?"
&lt;/h3&gt;

&lt;p&gt;I knew what I wanted to build. I didn't know &lt;em&gt;how&lt;/em&gt; to build a proper CLI.&lt;/p&gt;

&lt;p&gt;I asked Copilot:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How do I create a Python CLI that can authenticate with a Next.js API using GitHub OAuth?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Copilot gave me a clear strategy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use the &lt;code&gt;typer&lt;/code&gt; library for CLI structure (modern, type-hint based)&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;rich&lt;/code&gt; library for beautiful terminal output and created folder structure and initialize with code snippets&lt;/li&gt;
&lt;li&gt;Don't reinvent auth use the GitHub CLI (&lt;code&gt;gh auth token&lt;/code&gt;) to get the user's existing token&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last point was the key insight. Instead of building a complex OAuth flow for the terminal, I just grab the token that's already there from the GitHub CLI. Users are already logged into &lt;code&gt;gh&lt;/code&gt; for their daily work. Why make them log in again?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_auth_headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&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;p&gt;&lt;strong&gt;Secure authentication in 4 lines of code.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Battle: Cross-Platform Path Handling
&lt;/h3&gt;

&lt;p&gt;My CLI worked perfectly on my Linux. Then I tested it on Windows.&lt;/p&gt;

&lt;p&gt;Immediate crash.&lt;/p&gt;

&lt;p&gt;The problem? I was building file paths with forward slashes (&lt;code&gt;/&lt;/code&gt;), but Windows uses backslashes (&lt;code&gt;\&lt;/code&gt;). When uploading to Vercel Blob, the paths were getting mangled.&lt;/p&gt;

&lt;p&gt;I asked Copilot:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"My CLI works on Linu but fails on Windows because of path separators. How do I handle this?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Copilot showed me how to normalize paths consistently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path_str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path_str&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;as_posix&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Always use forward slashes
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple fix. Now EnvHub runs seamlessly on Windows, Mac, and Linux.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Optimization: Smart "No-Change" Detection
&lt;/h3&gt;

&lt;p&gt;Early testers reported a problem: they'd accidentally run &lt;code&gt;envhub push&lt;/code&gt; twice and create duplicate versions with identical content. The history log was getting cluttered with meaningless entries.&lt;/p&gt;

&lt;p&gt;I asked Copilot:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How can I detect if the variables being pushed are identical to the current version and skip the write?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Copilot helped me implement a comparison check on the backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Check if variables actually changed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentBundle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBundle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentBundle&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentBundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentBundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No changes detected. Version not incremented.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now EnvHub only creates a new version when something actually changes. Clean history, lower storage costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Encryption Decision
&lt;/h3&gt;

&lt;p&gt;I knew I needed encryption at rest. I had some familiarity with the options AES, RSA, and others but I wanted to make sure I picked the right approach for this specific use case.&lt;/p&gt;

&lt;p&gt;I asked Copilot:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What's a good symmetric encryption approach that works in both Python and Node.js?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer: &lt;strong&gt;Fernet&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Fernet is built on AES-128-CBC with HMAC for authentication. It's secure, widely used, and has solid libraries for both Python and Node.js. Copilot pointed me to the exact packages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python: &lt;code&gt;cryptography&lt;/code&gt; library&lt;/li&gt;
&lt;li&gt;Node.js: &lt;code&gt;fernet&lt;/code&gt; package
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python (CLI side)
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cryptography.fernet&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Fernet&lt;/span&gt;
&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Fernet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_key&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Fernet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my secret value&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node.js (API side)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fernet&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fernet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;fernet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;fernet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The right tool for the job, implemented quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Distribution Problem
&lt;/h3&gt;

&lt;p&gt;Here's a challenge I didn't anticipate: how do users install the CLI?&lt;/p&gt;

&lt;p&gt;I couldn't publish to PyPI (the public Python package index) because each user's CLI needs to point to their specific EnvHub instance. My EnvHub lives at &lt;code&gt;envhub-harivelu.vercel.app&lt;/code&gt;. Your EnvHub lives at &lt;code&gt;envhub-yourcompany.vercel.app&lt;/code&gt;. We can't share the same hardcoded package.&lt;/p&gt;

&lt;p&gt;I asked Copilot:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How can I distribute a Python CLI where each deployment has a different backend URL?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Copilot's solution was elegant: &lt;strong&gt;host the &lt;code&gt;.whl&lt;/code&gt; file on the EnvHub instance itself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each deployed EnvHub instance serves its own CLI package at &lt;code&gt;/cli/envhub_cli-2.0.3-py3-none-any.whl&lt;/code&gt;. Users install directly from their instance:&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;https://your-instance.vercel.app/cli/envhub_cli-2.0.3-py3-none-any.whl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then they run &lt;code&gt;envhub init&lt;/code&gt; to configure the API URL. Decentralized, self-contained, and each team gets their own setup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🎥 &lt;strong&gt;Watch the Full Walkthrough:&lt;/strong&gt; &lt;a href="https://youtu.be/54do2TvHB3Y" rel="noopener noreferrer"&gt;https://youtu.be/54do2TvHB3Y&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Try the Live Demo:&lt;/strong&gt; &lt;a href="https://env-hub-nu.vercel.app/" rel="noopener noreferrer"&gt;https://env-hub-nu.vercel.app/&lt;/a&gt;&lt;br&gt;
&lt;em&gt;(Click "Continue with Demo Account" you can only access the sandboxed &lt;code&gt;demo-project&lt;/code&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;🐙 &lt;strong&gt;Source Code:&lt;/strong&gt; &lt;a href="https://github.com/Harivelu0/EnvHub" rel="noopener noreferrer"&gt;github.com/Harivelu0/EnvHub&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Login Experience&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff5159dhgh9xs2g8b5qxa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff5159dhgh9xs2g8b5qxa.png" alt=" " width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Clean, dark-themed login with GitHub OAuth. No new passwords.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Dashboard&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft24zq1c9fh9ckxe3iyyw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft24zq1c9fh9ckxe3iyyw.png" alt=" " width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Your entire workspace at a glance. Projects, services, environments.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Variable Editor&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frcf0qr0oxat4anjv5x27.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frcf0qr0oxat4anjv5x27.png" alt=" " width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Add variables individually or bulk upload. Change reason required.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version History&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkp6kwa8d4123jj1neoiz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkp6kwa8d4123jj1neoiz.png" alt=" " width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Full audit trail. Click any version to see that point in time.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI in Action&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsei0i86h32a7molelwpj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsei0i86h32a7molelwpj.png" alt=" " width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsjcz2t7161xwth5btgzd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsjcz2t7161xwth5btgzd.png" alt=" " width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0s0nu9x8ehjn5mcloypc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0s0nu9x8ehjn5mcloypc.png" alt=" " width="800" height="142"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Push, pull, and history right from your terminal.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Try It Yourself (Complete Setup Guide)
&lt;/h2&gt;

&lt;p&gt;Want to deploy your own EnvHub? Here's the full walkthrough.&lt;/p&gt;
&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;A GitHub account&lt;/li&gt;
&lt;li&gt;A Vercel account (free tier works)&lt;/li&gt;
&lt;li&gt;Python 3.8+ installed&lt;/li&gt;
&lt;li&gt;GitHub CLI (&lt;code&gt;gh&lt;/code&gt;) installed &lt;a href="https://cli.github.com/" rel="noopener noreferrer"&gt;get it here&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Step 1: Clone and Deploy
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone the repository&lt;/span&gt;
git clone https://github.com/Harivelu0/EnvHub.git
&lt;span class="nb"&gt;cd &lt;/span&gt;EnvHub

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Deploy to Vercel&lt;/span&gt;
vercel deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Vercel will ask you some questions. Accept the defaults. At the end, you'll get a URL like &lt;code&gt;https://envhub-yourname.vercel.app&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Create a GitHub OAuth App
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://github.com/settings/developers" rel="noopener noreferrer"&gt;GitHub Developer Settings&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"New OAuth App"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Fill in:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Application name:&lt;/strong&gt; EnvHub (or whatever you want)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Homepage URL:&lt;/strong&gt; &lt;code&gt;https://envhub-yourname.vercel.app&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization callback URL:&lt;/strong&gt; &lt;code&gt;https://envhub-yourname.vercel.app/api/auth/callback/github&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Register application"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;Client ID&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Generate a new client secret"&lt;/strong&gt; and copy the &lt;strong&gt;Client Secret&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Step 3: Create Vercel Blob Storage
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://vercel.com/dashboard" rel="noopener noreferrer"&gt;Vercel Dashboard&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click on &lt;strong&gt;"Storage"&lt;/strong&gt; in the sidebar&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create Database"&lt;/strong&gt; → Select &lt;strong&gt;"Blob"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Name it something like &lt;code&gt;envhub-storage&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;Read/Write Token&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Step 4: Generate Your Encryption Key
&lt;/h3&gt;

&lt;p&gt;Run this command to generate a secure Fernet key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll get something like: &lt;code&gt;Sn-S2vY5W4HuScQ60IG8JXiK9aIMmC-SadbyY1NxWBY=&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep this safe!&lt;/strong&gt; If you lose this key, you lose access to all your encrypted variables.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Configure Environment Variables
&lt;/h3&gt;

&lt;p&gt;In your Vercel Dashboard:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to your EnvHub project&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Settings"&lt;/strong&gt; → &lt;strong&gt;"Environment Variables"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Add these variables:&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;Variable&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GITHUB_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your OAuth App Client ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GITHUB_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your OAuth App Client Secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NEXTAUTH_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;openssl rand -base64 32&lt;/code&gt; and paste the result&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NEXTAUTH_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://envhub-yourname.vercel.app&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BLOB_READ_WRITE_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your Vercel Blob token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ENVHUB_MASTER_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your Fernet key from Step 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ALLOWED_ORGS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your GitHub organization name (optional but recommended)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ALLOWED_USERS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your GitHub username (optional, for personal use)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 6: Redeploy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vercel &lt;span class="nt"&gt;--prod&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 7: Install and Configure the CLI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the CLI from your instance&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;https://envhub-yourname.vercel.app/cli/envhub_cli-2.0.3-py3-none-any.whl

&lt;span class="c"&gt;# Point it to your instance&lt;/span&gt;
envhub init &lt;span class="nt"&gt;--api-url&lt;/span&gt; https://envhub-yourname.vercel.app/api

&lt;span class="c"&gt;# Login via GitHub&lt;/span&gt;
envhub login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 8: Push Your First Environment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a test .env file&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"DATABASE_URL=postgres://localhost:5432/mydb"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"API_KEY=sk_test_12345"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .env

&lt;span class="c"&gt;# Push it to EnvHub&lt;/span&gt;
envhub push &lt;span class="nt"&gt;-p&lt;/span&gt; my-project &lt;span class="nt"&gt;-s&lt;/span&gt; backend &lt;span class="nt"&gt;-e&lt;/span&gt; dev &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"Initial setup"&lt;/span&gt;

&lt;span class="c"&gt;# Verify it worked&lt;/span&gt;
envhub pull &lt;span class="nt"&gt;-p&lt;/span&gt; my-project &lt;span class="nt"&gt;-s&lt;/span&gt; backend &lt;span class="nt"&gt;-e&lt;/span&gt; dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;You're done!&lt;/strong&gt; You now have a fully functional, encrypted, versioned secret manager.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Tech Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&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;strong&gt;Frontend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Next.js 16 + React 19&lt;/td&gt;
&lt;td&gt;Latest features, seamless API routes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Styling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tailwind CSS 4&lt;/td&gt;
&lt;td&gt;Fast iteration on the dark theme&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NextAuth.js + GitHub OAuth&lt;/td&gt;
&lt;td&gt;No new passwords, org gating built-in&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;Vercel Blob&lt;/td&gt;
&lt;td&gt;Serverless, no database to manage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Encryption&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fernet (AES-128)&lt;/td&gt;
&lt;td&gt;Industry standard, cross-platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CLI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Python + Typer + Rich&lt;/td&gt;
&lt;td&gt;Clean terminal UI, good DX&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The entire backend is &lt;strong&gt;serverless&lt;/strong&gt;. No database servers to maintain, no scaling headaches. Vercel Blob handles storage, API routes handle logic.&lt;/p&gt;




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

&lt;p&gt;EnvHub is already useful for small teams, but I have plans:&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud-Agnostic Storage
&lt;/h3&gt;

&lt;p&gt;Currently uses Vercel Blob. Some enterprises need their data in AWS S3 or Azure Blob Storage for compliance. I'm abstracting the storage layer to support multiple providers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enterprise Identity (RBAC)
&lt;/h3&gt;

&lt;p&gt;Right now, access control is binary: you're in the allowed org or you're not. For larger teams, I want role-based access:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DevOps Lead → All projects&lt;/li&gt;
&lt;li&gt;Backend Dev → Backend services only&lt;/li&gt;
&lt;li&gt;Intern → Read-only on dev environments&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Audit Log Export
&lt;/h3&gt;

&lt;p&gt;For SOC2 compliance, security teams need to export audit logs to SIEM tools (Splunk, Azure Sentinel). Adding a one-click export feature.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Scratch your own itch.&lt;/strong&gt; The best tools come from real frustration. I built EnvHub because I was tired of the &lt;code&gt;.env&lt;/code&gt; chaos. That frustration carried me through the hard parts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. AI accelerates learning.&lt;/strong&gt; GitHub Copilot CLI didn't build EnvHub for me. But it helped me learn CLI development faster than I would have on my own. I still made the architecture decisions and debugged the weird issues. Copilot just shortened the learning curve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Security can't be an afterthought.&lt;/strong&gt; It would've been easier to build EnvHub as a hosted SaaS. But zero-knowledge architecture is the right choice for a secrets tool. Your secrets should be &lt;em&gt;yours&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Developer experience matters.&lt;/strong&gt; A secure tool that's annoying to use gets ignored. I spent real time on the CLI output formatting and dashboard UI. Good tools should feel good to use.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;EnvHub started as frustration with the way we handle environment variables and became a tool I'm genuinely proud of.&lt;/p&gt;

&lt;p&gt;It solves a real problem. It's secure by design. It's open source. And building it taught me that with tools like GitHub Copilot CLI, you can pick up new skills faster than ever.&lt;/p&gt;

&lt;p&gt;If you've ever Slacked a &lt;code&gt;.env&lt;/code&gt; file to a teammate, or overwritten new config with old values, or spent an hour debugging a missing variable EnvHub is for you.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;⭐ Star the repo:&lt;/strong&gt; &lt;a href="https://github.com/Harivelu0/EnvHub" rel="noopener noreferrer"&gt;https://github.com/Harivelu0/EnvHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🐛 Found a bug?&lt;/strong&gt; &lt;a href="https://github.com/Harivelu0/EnvHub/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;💬 Questions?&lt;/strong&gt; Drop a comment below!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Thanks for reading. Now go secure your secrets.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>cli</category>
      <category>githubcopilot</category>
    </item>
    <item>
      <title>Canary Deployments in Azure Container Apps: A Complete Guide</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Tue, 03 Feb 2026 12:36:07 +0000</pubDate>
      <link>https://dev.to/techwithhari/canary-deployments-in-azure-container-apps-a-complete-guide-ke8</link>
      <guid>https://dev.to/techwithhari/canary-deployments-in-azure-container-apps-a-complete-guide-ke8</guid>
      <description>&lt;h2&gt;
  
  
  Why Do We Need Canary Deployments?
&lt;/h2&gt;

&lt;p&gt;Imagine you push a new update to production. Everything looked fine in testing, but suddenly your users start experiencing errors. By the time you notice, thousands of users are affected. You scramble to rollback, but the damage is done.&lt;/p&gt;

&lt;p&gt;This is exactly what canary deployments prevent.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem with Traditional Deployments
&lt;/h3&gt;

&lt;p&gt;In a traditional deployment, when you push new code, 100% of your traffic immediately goes to the new version. If something breaks, all your users are affected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Traditional Deployment:

Before:  [Old Version] ████████████ 100% traffic
After:   [New Version] ████████████ 100% traffic  ← If broken, everyone is affected!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Canary Solution
&lt;/h3&gt;

&lt;p&gt;Canary deployment gets its name from the old mining practice of bringing canaries into coal mines. If dangerous gases were present, the canary would die first, warning miners to evacuate.&lt;/p&gt;

&lt;p&gt;Similarly, in canary deployments, we send a small portion of traffic to the new version first. If something goes wrong, only a small percentage of users are affected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Canary Deployment:

Step 1:  [Old Version] ██████████ 90%
         [New Version] ██ 10%        ← Test with small traffic

Step 2:  [Old Version] ██████ 50%
         [New Version] ██████ 50%    ← Gradually increase

Step 3:  [New Version] ████████████ 100%  ← Full rollout after validation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How Azure Container Apps Supports Canary Deployments
&lt;/h2&gt;

&lt;p&gt;Azure Container Apps has built-in support for traffic splitting through its revision system. Every time you deploy, a new revision is created. You can then control how much traffic goes to each revision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Concepts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Revision&lt;/strong&gt;: A snapshot of your container app at a specific point in time. Each deployment creates a new revision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traffic Weight&lt;/strong&gt;: The percentage of traffic each revision receives. All weights must add up to 100%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Active Revision&lt;/strong&gt;: A revision that is running and can receive traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inactive Revision&lt;/strong&gt;: A revision that exists but receives no traffic and consumes no resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation: Single Region Canary Deployment
&lt;/h2&gt;

&lt;p&gt;Let's build a complete canary deployment pipeline for a single-region setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Deployment Flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Push Code
    │
    ▼
Build Image (tagged with git SHA)
    │
    ▼
Save Current Stable Revision
    │
    ▼
Deploy New Revision (Canary)
    │
    ▼
Health Check (5 attempts)
    │
    ├── PASS ──▶ Split Traffic 50/50
    │
    └── FAIL ──▶ Auto Rollback + Alert
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 1: Build and Deploy with Unique Tags
&lt;/h3&gt;

&lt;p&gt;Never use the &lt;code&gt;latest&lt;/code&gt; tag for production deployments. It's mutable and causes inconsistencies.&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;Deploy Application&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;# Use git SHA for unique, immutable image tag&lt;/span&gt;
    &lt;span class="s"&gt;IMAGE_TAG="${{ github.sha }}"&lt;/span&gt;
    &lt;span class="s"&gt;REVISION_SUFFIX=$(echo "$IMAGE_TAG" | cut -c1-8)&lt;/span&gt;

    &lt;span class="s"&gt;# Build and push with unique tag&lt;/span&gt;
    &lt;span class="s"&gt;az acr build \&lt;/span&gt;
      &lt;span class="s"&gt;--registry $REGISTRY_NAME \&lt;/span&gt;
      &lt;span class="s"&gt;--image $APP_NAME:$IMAGE_TAG \&lt;/span&gt;
      &lt;span class="s"&gt;--file Dockerfile .&lt;/span&gt;

    &lt;span class="s"&gt;# Deploy with revision suffix for easy identification&lt;/span&gt;
    &lt;span class="s"&gt;az containerapp update \&lt;/span&gt;
      &lt;span class="s"&gt;--name $APP_NAME \&lt;/span&gt;
      &lt;span class="s"&gt;--resource-group $RESOURCE_GROUP \&lt;/span&gt;
      &lt;span class="s"&gt;--image "$ACR_SERVER/$APP_NAME:$IMAGE_TAG" \&lt;/span&gt;
      &lt;span class="s"&gt;--revision-suffix "$REVISION_SUFFIX"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Health Check
&lt;/h3&gt;

&lt;p&gt;Before routing traffic, verify the new deployment is healthy.&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;Health Check&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;FQDN=$(az containerapp show \&lt;/span&gt;
      &lt;span class="s"&gt;--name $APP_NAME \&lt;/span&gt;
      &lt;span class="s"&gt;--resource-group $RESOURCE_GROUP \&lt;/span&gt;
      &lt;span class="s"&gt;--query properties.configuration.ingress.fqdn \&lt;/span&gt;
      &lt;span class="s"&gt;--output tsv)&lt;/span&gt;

    &lt;span class="s"&gt;HEALTH_PASSED=false&lt;/span&gt;
    &lt;span class="s"&gt;for i in {1..5}; do&lt;/span&gt;
      &lt;span class="s"&gt;if curl -sf --max-time 5 "https://${FQDN}/health" &amp;gt; /dev/null 2&amp;gt;&amp;amp;1; then&lt;/span&gt;
        &lt;span class="s"&gt;HEALTH_PASSED=true&lt;/span&gt;
        &lt;span class="s"&gt;break&lt;/span&gt;
      &lt;span class="s"&gt;fi&lt;/span&gt;
      &lt;span class="s"&gt;sleep 10&lt;/span&gt;
    &lt;span class="s"&gt;done&lt;/span&gt;

    &lt;span class="s"&gt;echo "HEALTH_PASSED=${HEALTH_PASSED}" &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tries 5 times with 10-second intervals. Why 5 attempts? Because containers need time to start, connect to databases, and warm up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Traffic Splitting
&lt;/h3&gt;

&lt;p&gt;If health check passes, split traffic between stable and canary.&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;Route 50/50 Traffic&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;env.HEALTH_PASSED == 'true'&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;STABLE="${{ env.STABLE_REVISION }}"&lt;/span&gt;
    &lt;span class="s"&gt;CANARY="${{ env.CANARY_REVISION }}"&lt;/span&gt;

    &lt;span class="s"&gt;if [[ -n "${STABLE}" &amp;amp;&amp;amp; "${STABLE}" != "${CANARY}" ]]; then&lt;/span&gt;
      &lt;span class="s"&gt;az containerapp ingress traffic set \&lt;/span&gt;
        &lt;span class="s"&gt;--name $APP_NAME \&lt;/span&gt;
        &lt;span class="s"&gt;--resource-group $RESOURCE_GROUP \&lt;/span&gt;
        &lt;span class="s"&gt;--traffic-weight ${STABLE}=50 ${CANARY}=50&lt;/span&gt;
    &lt;span class="s"&gt;else&lt;/span&gt;
      &lt;span class="s"&gt;# First deployment - no stable exists&lt;/span&gt;
      &lt;span class="s"&gt;az containerapp ingress traffic set \&lt;/span&gt;
        &lt;span class="s"&gt;--name $APP_NAME \&lt;/span&gt;
        &lt;span class="s"&gt;--resource-group $RESOURCE_GROUP \&lt;/span&gt;
        &lt;span class="s"&gt;--traffic-weight ${CANARY}=100&lt;/span&gt;
    &lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Auto Rollback on Failure
&lt;/h3&gt;

&lt;p&gt;If health check fails, immediately rollback to protect users.&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;Auto Rollback&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;env.HEALTH_PASSED == 'false'&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;# Route all traffic back to stable&lt;/span&gt;
    &lt;span class="s"&gt;az containerapp ingress traffic set \&lt;/span&gt;
      &lt;span class="s"&gt;--name $APP_NAME \&lt;/span&gt;
      &lt;span class="s"&gt;--resource-group $RESOURCE_GROUP \&lt;/span&gt;
      &lt;span class="s"&gt;--traffic-weight ${{ env.STABLE_REVISION }}=100&lt;/span&gt;

    &lt;span class="s"&gt;# Deactivate broken revision to free resources&lt;/span&gt;
    &lt;span class="s"&gt;az containerapp revision deactivate \&lt;/span&gt;
      &lt;span class="s"&gt;--name $APP_NAME \&lt;/span&gt;
      &lt;span class="s"&gt;--resource-group $RESOURCE_GROUP \&lt;/span&gt;
      &lt;span class="s"&gt;--revision "${{ env.CANARY_REVISION }}" || true&lt;/span&gt;

    &lt;span class="s"&gt;# Send alert to team&lt;/span&gt;
    &lt;span class="s"&gt;curl -X POST "$WEBHOOK_URL" \&lt;/span&gt;
      &lt;span class="s"&gt;-H "Content-Type: application/json" \&lt;/span&gt;
      &lt;span class="s"&gt;-d '{"text":"Auto rollback triggered for '$APP_NAME'"}'&lt;/span&gt;

    &lt;span class="s"&gt;exit 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementation: Multi-Region Canary Deployment
&lt;/h2&gt;

&lt;p&gt;For applications deployed across multiple regions, we need a sequential approach to prevent global outages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Sequential Deployment?
&lt;/h3&gt;

&lt;p&gt;If you deploy to all regions simultaneously and there's a bug, all regions go down together. Sequential deployment means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deploy to Region 1&lt;/li&gt;
&lt;li&gt;Health check Region 1&lt;/li&gt;
&lt;li&gt;If healthy, deploy to Region 2&lt;/li&gt;
&lt;li&gt;Health check Region 2&lt;/li&gt;
&lt;li&gt;Continue...&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any region fails, stop the rollout immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Region Deployment Flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Deploy to US Region
    │
    ▼
Health Check US
    │
    ├── FAIL ──▶ Stop! Don't deploy to other regions
    │
    └── PASS ──▶ Deploy to EU Region
                    │
                    ▼
                Health Check EU
                    │
                    ├── FAIL ──▶ Rollback EU, keep US on canary
                    │
                    └── PASS ──▶ All regions on 50/50 split
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Multi-Region Implementation
&lt;/h3&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;Deploy to All Regions&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;REGIONS="us eu uk"&lt;/span&gt;
    &lt;span class="s"&gt;REVISION_SUFFIX=$(echo "${{ github.sha }}" | cut -c1-8)&lt;/span&gt;

    &lt;span class="s"&gt;for REGION in $REGIONS; do&lt;/span&gt;
      &lt;span class="s"&gt;APP_NAME="myapp-${REGION}-prod"&lt;/span&gt;

      &lt;span class="s"&gt;echo "Deploying to $REGION..."&lt;/span&gt;

      &lt;span class="s"&gt;# Deploy&lt;/span&gt;
      &lt;span class="s"&gt;az containerapp update \&lt;/span&gt;
        &lt;span class="s"&gt;--name $APP_NAME \&lt;/span&gt;
        &lt;span class="s"&gt;--resource-group $RESOURCE_GROUP \&lt;/span&gt;
        &lt;span class="s"&gt;--image $IMAGE_NAME \&lt;/span&gt;
        &lt;span class="s"&gt;--revision-suffix "$REVISION_SUFFIX"&lt;/span&gt;

      &lt;span class="s"&gt;# Health check this region before proceeding&lt;/span&gt;
      &lt;span class="s"&gt;FQDN=$(az containerapp show \&lt;/span&gt;
        &lt;span class="s"&gt;--name $APP_NAME \&lt;/span&gt;
        &lt;span class="s"&gt;--resource-group $RESOURCE_GROUP \&lt;/span&gt;
        &lt;span class="s"&gt;--query "properties.configuration.ingress.fqdn" \&lt;/span&gt;
        &lt;span class="s"&gt;--output tsv)&lt;/span&gt;

      &lt;span class="s"&gt;HEALTHY=false&lt;/span&gt;
      &lt;span class="s"&gt;for i in {1..5}; do&lt;/span&gt;
        &lt;span class="s"&gt;if curl -sf "https://$FQDN/health" &amp;gt; /dev/null 2&amp;gt;&amp;amp;1; then&lt;/span&gt;
          &lt;span class="s"&gt;HEALTHY=true&lt;/span&gt;
          &lt;span class="s"&gt;break&lt;/span&gt;
        &lt;span class="s"&gt;fi&lt;/span&gt;
        &lt;span class="s"&gt;sleep 10&lt;/span&gt;
      &lt;span class="s"&gt;done&lt;/span&gt;

      &lt;span class="s"&gt;if [[ "$HEALTHY" != "true" ]]; then&lt;/span&gt;
        &lt;span class="s"&gt;echo "Region $REGION failed health check. Stopping deployment."&lt;/span&gt;
        &lt;span class="s"&gt;exit 1&lt;/span&gt;
      &lt;span class="s"&gt;fi&lt;/span&gt;

      &lt;span class="s"&gt;echo "$REGION deployed and healthy"&lt;/span&gt;
    &lt;span class="s"&gt;done&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Route Traffic All Regions&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;REGIONS="us eu uk"&lt;/span&gt;

    &lt;span class="s"&gt;for REGION in $REGIONS; do&lt;/span&gt;
      &lt;span class="s"&gt;APP_NAME="myapp-${REGION}-prod"&lt;/span&gt;
      &lt;span class="s"&gt;CANARY="${APP_NAME}--${REVISION_SUFFIX}"&lt;/span&gt;

      &lt;span class="s"&gt;# Get stable revision for this region&lt;/span&gt;
      &lt;span class="s"&gt;STABLE=$(az containerapp revision list \&lt;/span&gt;
        &lt;span class="s"&gt;--name $APP_NAME \&lt;/span&gt;
        &lt;span class="s"&gt;--resource-group $RESOURCE_GROUP \&lt;/span&gt;
        &lt;span class="s"&gt;--query "[?properties.trafficWeight&amp;gt;\`0\` &amp;amp;&amp;amp; name!='${CANARY}'] | [0].name" \&lt;/span&gt;
        &lt;span class="s"&gt;-o tsv)&lt;/span&gt;

      &lt;span class="s"&gt;if [[ -n "$STABLE" ]]; then&lt;/span&gt;
        &lt;span class="s"&gt;az containerapp ingress traffic set \&lt;/span&gt;
          &lt;span class="s"&gt;--name $APP_NAME \&lt;/span&gt;
          &lt;span class="s"&gt;--resource-group $RESOURCE_GROUP \&lt;/span&gt;
          &lt;span class="s"&gt;--traffic-weight ${STABLE}=50 ${CANARY}=50&lt;/span&gt;
      &lt;span class="s"&gt;fi&lt;/span&gt;
    &lt;span class="s"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Manual Promotion and Rollback
&lt;/h2&gt;

&lt;p&gt;After the canary has been running for a while and you've validated it's working correctly, you need to promote it to 100% or rollback if issues are found.&lt;/p&gt;

&lt;h3&gt;
  
  
  Promotion Workflow
&lt;/h3&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Canary:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Promote&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Rollback'&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;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Action&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;perform'&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;choice&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;promote-100&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rollback&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;canary-action&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Azure Login&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;azure/login@v2&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;creds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_CREDENTIALS }}&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;Get Current Revisions&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;revisions&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;REVISIONS=$(az containerapp revision list \&lt;/span&gt;
            &lt;span class="s"&gt;--name $APP_NAME \&lt;/span&gt;
            &lt;span class="s"&gt;--resource-group $RESOURCE_GROUP \&lt;/span&gt;
            &lt;span class="s"&gt;--query "[?properties.trafficWeight&amp;gt;\`0\`] | sort_by(@, &amp;amp;properties.trafficWeight)" \&lt;/span&gt;
            &lt;span class="s"&gt;-o json)&lt;/span&gt;

          &lt;span class="s"&gt;# Highest traffic = stable, lowest = canary&lt;/span&gt;
          &lt;span class="s"&gt;STABLE=$(echo "$REVISIONS" | jq -r 'last | .name')&lt;/span&gt;
          &lt;span class="s"&gt;CANARY=$(echo "$REVISIONS" | jq -r 'first | .name')&lt;/span&gt;

          &lt;span class="s"&gt;echo "STABLE=${STABLE}" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
          &lt;span class="s"&gt;echo "CANARY=${CANARY}" &amp;gt;&amp;gt; $GITHUB_OUTPUT&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;Execute Action&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;case "${{ github.event.inputs.action }}" in&lt;/span&gt;
            &lt;span class="s"&gt;promote-100)&lt;/span&gt;
              &lt;span class="s"&gt;# Send 100% to canary&lt;/span&gt;
              &lt;span class="s"&gt;az containerapp ingress traffic set \&lt;/span&gt;
                &lt;span class="s"&gt;--name $APP_NAME \&lt;/span&gt;
                &lt;span class="s"&gt;--resource-group $RESOURCE_GROUP \&lt;/span&gt;
                &lt;span class="s"&gt;--traffic-weight ${{ steps.revisions.outputs.CANARY }}=100&lt;/span&gt;

              &lt;span class="s"&gt;# Deactivate old stable&lt;/span&gt;
              &lt;span class="s"&gt;az containerapp revision deactivate \&lt;/span&gt;
                &lt;span class="s"&gt;--name $APP_NAME \&lt;/span&gt;
                &lt;span class="s"&gt;--resource-group $RESOURCE_GROUP \&lt;/span&gt;
                &lt;span class="s"&gt;--revision "${{ steps.revisions.outputs.STABLE }}" || true&lt;/span&gt;
              &lt;span class="s"&gt;;;&lt;/span&gt;

            &lt;span class="s"&gt;rollback)&lt;/span&gt;
              &lt;span class="s"&gt;# Send 100% back to stable&lt;/span&gt;
              &lt;span class="s"&gt;az containerapp ingress traffic set \&lt;/span&gt;
                &lt;span class="s"&gt;--name $APP_NAME \&lt;/span&gt;
                &lt;span class="s"&gt;--resource-group $RESOURCE_GROUP \&lt;/span&gt;
                &lt;span class="s"&gt;--traffic-weight ${{ steps.revisions.outputs.STABLE }}=100&lt;/span&gt;

              &lt;span class="s"&gt;# Deactivate broken canary&lt;/span&gt;
              &lt;span class="s"&gt;az containerapp revision deactivate \&lt;/span&gt;
                &lt;span class="s"&gt;--name $APP_NAME \&lt;/span&gt;
                &lt;span class="s"&gt;--resource-group $RESOURCE_GROUP \&lt;/span&gt;
                &lt;span class="s"&gt;--revision "${{ steps.revisions.outputs.CANARY }}" || true&lt;/span&gt;
              &lt;span class="s"&gt;;;&lt;/span&gt;
          &lt;span class="s"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cleaning Up Inactive Revisions
&lt;/h2&gt;

&lt;p&gt;Over time, you'll accumulate many inactive revisions. While they don't consume compute resources, they clutter your revision list. Deactivating revisions after promotion or rollback keeps things clean.&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;# Deactivate a specific revision&lt;/span&gt;
az containerapp revision deactivate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$APP_NAME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--revision&lt;/span&gt; &lt;span class="s2"&gt;"myapp--abc12345"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;|| true&lt;/code&gt; after deactivation commands ensures the pipeline doesn't fail if the revision is already inactive.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  1. Use Immutable Image Tags
&lt;/h3&gt;

&lt;p&gt;Never use &lt;code&gt;latest&lt;/code&gt;. Always tag images with git SHA or build number.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ myapp:latest
✅ myapp:a1b2c3d4e5f6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Start with Higher Canary Percentage for Low Traffic
&lt;/h3&gt;

&lt;p&gt;If you have few users, start with 50/50. You need enough traffic on the canary to detect issues.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Low traffic:  50/50 split (need volume to detect issues)
High traffic: 10/90 split (even 10% is thousands of users)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Implement Proper Health Checks
&lt;/h3&gt;

&lt;p&gt;Your &lt;code&gt;/health&lt;/code&gt; endpoint should verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application is running&lt;/li&gt;
&lt;li&gt;Database connections work&lt;/li&gt;
&lt;li&gt;Critical dependencies are reachable&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Set Up Alerts
&lt;/h3&gt;

&lt;p&gt;Always send alerts on rollback. Your team needs to know when deployments fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Use Revision Suffixes
&lt;/h3&gt;

&lt;p&gt;Name revisions with git SHA prefix for easy identification.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;myapp--a1b2c3d4  ← Easy to trace back to commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Canary deployments are essential for safe production releases. With Azure Container Apps, you get native support for traffic splitting that makes implementation straightforward.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Deploy new code as a separate revision&lt;/li&gt;
&lt;li&gt;Run health checks before routing traffic&lt;/li&gt;
&lt;li&gt;Split traffic gradually (50/50 for low traffic, 10/90 for high traffic)&lt;/li&gt;
&lt;li&gt;Auto-rollback on health check failure&lt;/li&gt;
&lt;li&gt;Use sequential deployment for multi-region setups&lt;/li&gt;
&lt;li&gt;Clean up inactive revisions after promotion/rollback&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The initial setup takes effort, but the peace of mind knowing your deployments are safe is worth it. No more 2 AM panic calls because a bad deployment took down production.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>azure</category>
      <category>devops</category>
      <category>containers</category>
    </item>
    <item>
      <title>I Fixed a Terraform State Lock Issue in GitHub Actions Here’s What I Learned</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Tue, 20 Jan 2026 15:47:44 +0000</pubDate>
      <link>https://dev.to/techwithhari/i-fixed-a-terraform-state-lock-issue-in-github-actions-heres-what-i-learned-ml8</link>
      <guid>https://dev.to/techwithhari/i-fixed-a-terraform-state-lock-issue-in-github-actions-heres-what-i-learned-ml8</guid>
      <description>&lt;p&gt;When you start running Terraform inside CI/CD pipelines like GitHub Actions, state management becomes a &lt;em&gt;silent troublemaker&lt;/em&gt;.&lt;br&gt;
Recently, I hit one of those classic issues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Error acquiring the state lock
state blob is already locked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the pipeline refused to continue.&lt;/p&gt;

&lt;p&gt;If you’ve ever seen this, trust me you’re not alone. Here’s exactly how it happened, how I fixed it, and what you should always remember to avoid getting stuck like this again.&lt;/p&gt;




&lt;h1&gt;
  
  
  ❗ The Issue: Terraform State Blob Got “Leased”
&lt;/h1&gt;

&lt;p&gt;Terraform uses &lt;strong&gt;state locking&lt;/strong&gt; to prevent multiple processes from modifying the state at the same time.&lt;/p&gt;

&lt;p&gt;In my case, the state was stored in &lt;strong&gt;Azure Blob Storage&lt;/strong&gt;.&lt;br&gt;
During a GitHub Actions run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The job acquired a lease on the blob&lt;/li&gt;
&lt;li&gt;The job got interrupted&lt;/li&gt;
&lt;li&gt;The lease never got released&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So when the next GitHub Actions workflow started, Terraform immediately failed with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;state blob is already locked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I opened the Azure Blob container, the state file showed:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Leased&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That’s when I realized the lock was stuck at the storage level.&lt;/p&gt;




&lt;h1&gt;
  
  
  🔧 How I Diagnosed It
&lt;/h1&gt;

&lt;p&gt;Three things told me the lock was real:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Terraform showed the same lock ID repeatedly&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ID: 6a8f9229-xxxx
Who: runner@githubactions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;2. My local &lt;code&gt;terraform force-unlock&lt;/code&gt; failed&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The CLI told me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local state cannot be unlocked by another process
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This showed that the lock wasn’t local it was in Azure.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Azure Blob Portal confirmed “Leased” status&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Inside the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;newsraag-prod.tfstate State: Leased
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the GitHub Actions runner died, but the backend still thought it was running.&lt;/p&gt;




&lt;h1&gt;
  
  
  🛠️ The Fix: Break the Lease Manually
&lt;/h1&gt;

&lt;p&gt;The only clean solution:&lt;/p&gt;

&lt;h3&gt;
  
  
  👉 &lt;strong&gt;Break the lease directly in Azure Blob Storage&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Azure Portal&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Open your &lt;strong&gt;Storage Account&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Containers&lt;/strong&gt; → open your Terraform state container&lt;/li&gt;
&lt;li&gt;Click your &lt;code&gt;.tfstate&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Break Lease&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Within seconds, the blob state changed to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State: Broken&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After this, I re-ran the GitHub Action it worked perfectly.&lt;/p&gt;




&lt;h1&gt;
  
  
  🧠 What You Should Remember (Very Important)
&lt;/h1&gt;

&lt;p&gt;State lock issues happen in almost every team using Terraform.&lt;br&gt;
Here are the important lessons I learned — and the practices you should follow too.&lt;/p&gt;


&lt;h1&gt;
  
  
  ✅ &lt;strong&gt;1. Never Run Parallel Terraform Jobs for the Same State&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;If two pipelines hit the same state file → deadlock.&lt;/p&gt;

&lt;p&gt;Use GitHub Actions concurrency control:&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;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform-prod&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  ✅ &lt;strong&gt;2. Add Lock Timeout to Terraform Commands&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Instead of failing instantly, Terraform will wait for the lock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan &lt;span class="nt"&gt;-lock-timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300s
terraform apply &lt;span class="nt"&gt;-lock-timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  ✅ &lt;strong&gt;3. Don’t Cancel Terraform Pipelines Mid-Way&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Cancelling the job leaves Azure Blob leases behind.&lt;/p&gt;

&lt;p&gt;Let the job finish unless absolutely necessary.&lt;/p&gt;




&lt;h1&gt;
  
  
  ✅ &lt;strong&gt;4. Use Separate State Files for Each Environment&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Do &lt;strong&gt;NOT&lt;/strong&gt; use the same state file for dev, stage, and prod.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dev.tfstate
stage.tfstate
prod.tfstate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  ✅ &lt;strong&gt;5. Break Lease ONLY When You’re Sure No Job Is Running&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Breaking the lease while Terraform is actively writing = corrupted state.&lt;/p&gt;

&lt;p&gt;Always confirm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No other Terraform processes&lt;/li&gt;
&lt;li&gt;No pipeline in progress&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  ✨ Bonus: Recommended GitHub Actions Pattern
&lt;/h1&gt;

&lt;p&gt;Here’s the workflow I now use:&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;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform-${{ github.ref_name }}&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;Terraform Init&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;terraform init -lock-timeout=300s&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;Terraform Plan&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;terraform plan -lock-timeout=300s&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;Terraform Apply&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'&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;terraform apply -lock-timeout=300s -auto-approve&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids 99% of locking issues.&lt;/p&gt;




&lt;h1&gt;
  
  
  🏁 Final Thoughts
&lt;/h1&gt;

&lt;p&gt;This issue taught me one big lesson &lt;strong&gt;state is the heart of Terraform&lt;/strong&gt;, and anything that interrupts it can break the entire pipeline.&lt;/p&gt;

&lt;p&gt;But once you understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how locking works&lt;/li&gt;
&lt;li&gt;where leases get stuck&lt;/li&gt;
&lt;li&gt;how to break them safely&lt;/li&gt;
&lt;li&gt;and how to prevent them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…you’ll never panic when you see the “state blob is already locked” error again.&lt;/p&gt;

&lt;p&gt;If you’re using Terraform in CI/CD pipelines, keep this checklist handy. It will save you hours of debugging.&lt;/p&gt;




</description>
      <category>terraform</category>
      <category>devops</category>
      <category>cloud</category>
      <category>azure</category>
    </item>
    <item>
      <title>Best Ways to Migrate a Database (What I Learned Doing It in Production)</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Fri, 16 Jan 2026 13:11:38 +0000</pubDate>
      <link>https://dev.to/techwithhari/best-ways-to-migrate-a-database-what-i-learned-doing-it-in-production-2ipj</link>
      <guid>https://dev.to/techwithhari/best-ways-to-migrate-a-database-what-i-learned-doing-it-in-production-2ipj</guid>
      <description>&lt;p&gt;Database migration is one of those things that &lt;em&gt;sounds&lt;/em&gt; scary especially when it’s &lt;strong&gt;production data&lt;/strong&gt;. I used to think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What if data is lost?”&lt;br&gt;
“What if the app goes down?”&lt;br&gt;
“What if I mess this up?” 😅&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Recently, while working on a production system, I had to migrate a database across environments. The database wasn’t huge, but it &lt;em&gt;was live&lt;/em&gt;. That experience helped me clearly understand &lt;strong&gt;which migration approach to use and when&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post is me breaking that down in a &lt;strong&gt;simple, beginner-friendly way&lt;/strong&gt;, based on what actually works in real projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  What do we really mean by Database Migration?
&lt;/h2&gt;

&lt;p&gt;In simple terms, database migration means &lt;strong&gt;moving data from one database to another&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This could be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One cloud account to another&lt;/li&gt;
&lt;li&gt;One region to another&lt;/li&gt;
&lt;li&gt;On‑prem to cloud&lt;/li&gt;
&lt;li&gt;Old DB to a new DB engine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The challenge is never just copying data.&lt;/p&gt;

&lt;p&gt;The real challenge is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How do we move data safely without breaking production?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The 4 Common Ways People Migrate Databases
&lt;/h2&gt;

&lt;p&gt;From what I’ve seen, almost every migration falls into one of these categories.&lt;/p&gt;




&lt;h2&gt;
  
  
  1️⃣ Dump &amp;amp; Restore (The simplest and most common)
&lt;/h2&gt;

&lt;p&gt;This is the approach most people start with.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Take a logical dump from the source database&lt;/li&gt;
&lt;li&gt;Restore it into the target database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL → &lt;code&gt;pg_dump&lt;/code&gt; / &lt;code&gt;pg_restore&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;MySQL → &lt;code&gt;mysqldump&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When this works best
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Database size is small to medium&lt;/li&gt;
&lt;li&gt;One‑time migration&lt;/li&gt;
&lt;li&gt;A short maintenance window is okay&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why I like this approach
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Very easy to understand&lt;/li&gt;
&lt;li&gt;No extra tools required&lt;/li&gt;
&lt;li&gt;Works almost everywhere&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Downside
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You need downtime&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 &lt;strong&gt;If you’re new to database migration, this is the best place to start.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2️⃣ Managed Migration Tools (Cloud‑native way)
&lt;/h2&gt;

&lt;p&gt;Cloud providers give managed services to handle migrations for you.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Azure Database Migration Service&lt;/li&gt;
&lt;li&gt;AWS Database Migration Service&lt;/li&gt;
&lt;li&gt;GCP Database Migration Service&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You give source DB details&lt;/li&gt;
&lt;li&gt;You give target DB details&lt;/li&gt;
&lt;li&gt;Cloud handles the data movement&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When to use this
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Production databases&lt;/li&gt;
&lt;li&gt;Medium to large data&lt;/li&gt;
&lt;li&gt;You want minimal downtime&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pros
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Less manual work&lt;/li&gt;
&lt;li&gt;Safer for production&lt;/li&gt;
&lt;li&gt;Monitoring built‑in&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Slight learning curve&lt;/li&gt;
&lt;li&gt;Setup takes time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 &lt;strong&gt;This is what I’d pick for most production workloads.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3️⃣ Logical Replication (Advanced, but powerful)
&lt;/h2&gt;

&lt;p&gt;This is where things get serious.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Source DB keeps running&lt;/li&gt;
&lt;li&gt;Data changes stream continuously to target&lt;/li&gt;
&lt;li&gt;Final cutover takes minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Best for
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Large databases&lt;/li&gt;
&lt;li&gt;Near‑zero downtime&lt;/li&gt;
&lt;li&gt;Business‑critical systems&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Trade‑off
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;More complex&lt;/li&gt;
&lt;li&gt;Requires strong DB understanding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 &lt;strong&gt;Amazing when needed, but not beginner friendly.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4️⃣ Backup &amp;amp; Restore (Good for recovery, not migration)
&lt;/h2&gt;

&lt;p&gt;Some platforms allow restoring backups directly.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Azure point in time restore&lt;/li&gt;
&lt;li&gt;RDS snapshots&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reality check
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Mostly limited to same account&lt;/li&gt;
&lt;li&gt;Not designed for cross‑account migration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 &lt;strong&gt;Great for disaster recovery, not real migrations.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How I Decide Which Migration Method to Use
&lt;/h2&gt;

&lt;p&gt;This simple table helped me a lot:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;What to choose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Small DB&lt;/td&gt;
&lt;td&gt;Dump &amp;amp; Restore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production DB&lt;/td&gt;
&lt;td&gt;Managed Migration Tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large DB&lt;/td&gt;
&lt;td&gt;Logical Replication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recovery&lt;/td&gt;
&lt;td&gt;Backup Restore&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Common Mistakes I See (and almost made)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Migrating during peak traffic&lt;/li&gt;
&lt;li&gt;Forgetting extensions and roles&lt;/li&gt;
&lt;li&gt;Version mismatch between databases&lt;/li&gt;
&lt;li&gt;No rollback plan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trust me these things matter.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Simple Production Migration Checklist
&lt;/h2&gt;

&lt;p&gt;Before migrating, I always make sure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backup exists&lt;/li&gt;
&lt;li&gt;Writes are stopped&lt;/li&gt;
&lt;li&gt;Schema and data are migrated&lt;/li&gt;
&lt;li&gt;Data is validated&lt;/li&gt;
&lt;li&gt;App connection is switched cleanly&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Database migration doesn’t have to be complicated.&lt;/p&gt;

&lt;p&gt;If I had to summarize everything in one line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Small DB → Dump &amp;amp; Restore&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Production DB → Managed Tool&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Large DB → Replication&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you understand this, migrations stop being scary and start becoming just another engineering task.&lt;/p&gt;

&lt;p&gt;Hope this helps someone who’s about to migrate their first production database...&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Docker Compose: Orchestrating Multi-Container Applications 🧩</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Wed, 14 Jan 2026 08:54:00 +0000</pubDate>
      <link>https://dev.to/techwithhari/docker-compose-orchestrating-multi-container-applications-1n7b</link>
      <guid>https://dev.to/techwithhari/docker-compose-orchestrating-multi-container-applications-1n7b</guid>
      <description>&lt;p&gt;After working with individual Docker containers for a while, I quickly realized that most real-world applications require multiple containers working together. Managing these with individual &lt;code&gt;docker run&lt;/code&gt; commands became tedious and error-prone. That's where Docker Compose comes in - it's been a game-changer for my workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Docker Compose? 🤔
&lt;/h2&gt;

&lt;p&gt;Docker Compose is a tool for defining and running multi-container Docker applications. With a single YAML file and a few commands, you can configure all your application's services and spin up your entire stack with one command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use Docker Compose?
&lt;/h2&gt;

&lt;p&gt;Before diving into the how, let me explain why I switched to Docker Compose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity&lt;/strong&gt;: Define your entire application stack in a single file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproducibility&lt;/strong&gt;: Everyone on your team gets the exact same environment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service coordination&lt;/strong&gt;: Automatic networking between containers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables&lt;/strong&gt;: Easy configuration management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-command operations&lt;/strong&gt;: Start, stop, and rebuild your entire application stack&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Writing docker-compose.yml Files
&lt;/h2&gt;

&lt;p&gt;The heart of Docker Compose is the &lt;code&gt;docker-compose.yml&lt;/code&gt; file. Here's a basic example:&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;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;3'&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;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&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;80:80"&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&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_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break down the structure:&lt;/p&gt;

&lt;h3&gt;
  
  
  Basic Structure
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;version&lt;/strong&gt;: Specifies the Compose file format version&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;services&lt;/strong&gt;: Defines the containers to be created&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;volumes&lt;/strong&gt;: (Optional) Defines named volumes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;networks&lt;/strong&gt;: (Optional) Defines custom networks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Defining Services
&lt;/h3&gt;

&lt;p&gt;Each service represents a container. You can configure:&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&lt;/span&gt;                 &lt;span class="c1"&gt;# Use an existing image&lt;/span&gt;
    &lt;span class="c1"&gt;# OR&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./web&lt;/span&gt;                 &lt;span class="c1"&gt;# Build from a Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                       &lt;span class="c1"&gt;# More build options&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./web&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile.dev&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;8080:80"&lt;/span&gt;                &lt;span class="c1"&gt;# Port mapping&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                 &lt;span class="c1"&gt;# Environment variables&lt;/span&gt;
      &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;development&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;               &lt;span class="c1"&gt;# Or use an env file&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                     &lt;span class="c1"&gt;# Mount volumes&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./web:/usr/share/nginx/html&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                  &lt;span class="c1"&gt;# Service dependencies&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;api&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;always&lt;/span&gt;              &lt;span class="c1"&gt;# Restart policy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Managing Application Stacks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A Complete Example: Web App with Database and Cache
&lt;/h3&gt;

&lt;p&gt;Here's a more realistic example of an application stack I might use:&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;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;3'&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;web&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="s"&gt;./frontend&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;3000:3000"&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;./frontend:/app&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/app/node_modules&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;REACT_APP_API_URL=http://api:5000&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;

  &lt;span class="na"&gt;api&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="s"&gt;./backend&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;5000:5000"&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;./backend:/app&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/app/node_modules&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_HOST=db&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;REDIS_HOST=redis&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:13&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;environment&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_PASSWORD=secretpassword&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER=myuser&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB=myapp&lt;/span&gt;

  &lt;span class="na"&gt;redis&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;redis:alpine&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;redis-data:/data&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;redis-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This setup includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A React frontend&lt;/li&gt;
&lt;li&gt;A backend API&lt;/li&gt;
&lt;li&gt;A PostgreSQL database&lt;/li&gt;
&lt;li&gt;A Redis cache&lt;/li&gt;
&lt;li&gt;Persistent volumes for data&lt;/li&gt;
&lt;li&gt;Environment configuration&lt;/li&gt;
&lt;li&gt;Service dependencies&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;There are several ways to manage environment variables:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inline in the compose file&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&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;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NODE_ENV=development&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;API_URL=http://api:5000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;From a .env file&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&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;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./web.env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;From the shell environment&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&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;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NODE_ENV&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For sensitive data, I prefer using &lt;code&gt;.env&lt;/code&gt; files that are git-ignored.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compose Commands
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Building and Running Services
&lt;/h3&gt;

&lt;p&gt;These are the commands I use most often:&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;# Start all services&lt;/span&gt;
docker-compose up

&lt;span class="c"&gt;# Start in detached mode (background)&lt;/span&gt;
docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# Build or rebuild services&lt;/span&gt;
docker-compose build

&lt;span class="c"&gt;# Build and start&lt;/span&gt;
docker-compose up &lt;span class="nt"&gt;--build&lt;/span&gt;

&lt;span class="c"&gt;# Stop services&lt;/span&gt;
docker-compose down

&lt;span class="c"&gt;# Stop and remove volumes&lt;/span&gt;
docker-compose down &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Viewing Logs and Status
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# View logs of all services&lt;/span&gt;
docker-compose logs

&lt;span class="c"&gt;# Follow logs of a specific service&lt;/span&gt;
docker-compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; web

&lt;span class="c"&gt;# See running services&lt;/span&gt;
docker-compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Docker Compose has fundamentally changed how I develop and deploy multi-container applications. What used to take dozens of commands and careful coordination can now be defined in a single file and launched with one command.&lt;/p&gt;

&lt;p&gt;The key benefits I've experienced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Development environments that perfectly match production&lt;/li&gt;
&lt;li&gt;Easy onboarding for new team members&lt;/li&gt;
&lt;li&gt;Consistent deployments across environments&lt;/li&gt;
&lt;li&gt;Simplified local development workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're working with Docker and managing more than one container, Compose should definitely be part of your toolkit.&lt;/p&gt;

&lt;p&gt;In the next post, I'll discuss how to move your Docker Compose setups towards production-ready deployments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Next up: "Docker in Production: From Development to Deployment"&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>containers</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
    <item>
      <title>5 Dockerfile Misconfigurations You Should Avoid</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Sun, 11 Jan 2026 16:48:33 +0000</pubDate>
      <link>https://dev.to/techwithhari/5-dockerfile-misconfigurations-you-should-avoid-17pl</link>
      <guid>https://dev.to/techwithhari/5-dockerfile-misconfigurations-you-should-avoid-17pl</guid>
      <description>&lt;p&gt;When I started learning Docker and optimizing my containers, I realized that most of my issues weren’t about missing tools they were about &lt;strong&gt;how I wrote my Dockerfiles&lt;/strong&gt;. Over time, I identified a few mistakes I repeatedly made, and I want to share them so others can avoid the same pitfalls.&lt;/p&gt;

&lt;h3&gt;
  
  
  0️⃣ Don’t Forget to Open Docker Desktop
&lt;/h3&gt;

&lt;p&gt;When I started building images locally, I got &lt;strong&gt;stuck for almost a day&lt;/strong&gt; without realizing the problem: I hadn’t opened &lt;strong&gt;Docker Desktop&lt;/strong&gt;! 😅&lt;/p&gt;

&lt;p&gt;If you’re building locally, &lt;strong&gt;always make sure Docker is running&lt;/strong&gt; before you start it can save you hours of frustration.&lt;/p&gt;

&lt;h3&gt;
  
  
  1️⃣ Running Containers as Root
&lt;/h3&gt;

&lt;p&gt;When I first ran containers, I didn’t think much about users. By default, Docker runs as root which felt convenient but was risky.&lt;/p&gt;

&lt;p&gt;✅ What I learned:&lt;br&gt;
Always create and use a &lt;strong&gt;non-root user&lt;/strong&gt; inside your container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;-m&lt;/span&gt; appuser
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "app.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’s a small change, but it adds a &lt;strong&gt;huge layer of security&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  2️⃣ Using Untagged or Heavy Base Images
&lt;/h3&gt;

&lt;p&gt;Early on, I would just use &lt;code&gt;python:latest&lt;/code&gt; or big base images for convenience. The result? &lt;strong&gt;Bloated, unpredictable containers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;✅ What I learned:&lt;br&gt;
Use &lt;strong&gt;versioned tags&lt;/strong&gt; and &lt;strong&gt;multistage builds&lt;/strong&gt; to keep images clean and stable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build Stage&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;maven:3.9.6-eclipse-temurin-21&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;mvn clean package

&lt;span class="c"&gt;# Runtime Stage&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; eclipse-temurin:21-jre-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/target/app.jar /app/&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["java", "-jar", "app.jar"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach keeps your &lt;strong&gt;final image smaller and more predictable&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  3️⃣ Using COPY . . Without Thinking
&lt;/h3&gt;

&lt;p&gt;I used &lt;code&gt;COPY . .&lt;/code&gt; in almost every Dockerfile at first. It was easy until I realized I was dragging in &lt;strong&gt;build artifacts, configs, and secrets&lt;/strong&gt; by mistake.&lt;/p&gt;

&lt;p&gt;✅ What I learned:&lt;br&gt;
Be &lt;strong&gt;explicit&lt;/strong&gt; about what you copy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; target/app.jar /app/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A little extra effort here avoids &lt;strong&gt;heavier and riskier images&lt;/strong&gt; later.&lt;/p&gt;




&lt;h3&gt;
  
  
  4️⃣ Splitting Updates and Installs into Separate Layers
&lt;/h3&gt;

&lt;p&gt;I once ran &lt;code&gt;apt-get update&lt;/code&gt; and &lt;code&gt;apt-get install&lt;/code&gt; in separate RUN commands. It worked… until the cache broke during updates.&lt;/p&gt;

&lt;p&gt;✅ What I learned:&lt;br&gt;
Combine updates, installs, and cleanup in a &lt;strong&gt;single layer&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl vim &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fewer layers = cleaner cache = smaller images.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  5️⃣ Installing Extra Dependencies
&lt;/h3&gt;

&lt;p&gt;I used to install packages with all the extras, thinking it wouldn’t matter. It did images grew larger and the attack surface increased.&lt;/p&gt;

&lt;p&gt;✅ What I learned:&lt;br&gt;
Install &lt;strong&gt;only what’s necessary&lt;/strong&gt; and clean up after.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; curl &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Less clutter, more control.&lt;/p&gt;




&lt;h3&gt;
  
  
  🧭 Takeaways From My Docker Journey
&lt;/h3&gt;

&lt;p&gt;Building Dockerfiles the right way is all about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Least privilege&lt;/strong&gt; (non-root user)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimal base images&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Explicit COPY instructions&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single-layer installs&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trimmed dependencies&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After applying these lessons, my containers became &lt;strong&gt;smaller, safer, and more predictable&lt;/strong&gt; exactly how modern DevOps should be. &lt;/p&gt;




</description>
      <category>docker</category>
      <category>containers</category>
      <category>devops</category>
      <category>aws</category>
    </item>
    <item>
      <title>Automating Terraform Import for Existing Azure Infrastructure via Pipeline</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Mon, 05 Jan 2026 13:43:00 +0000</pubDate>
      <link>https://dev.to/techwithhari/automating-terraform-import-for-existing-azure-infrastructure-via-pipeline-23d5</link>
      <guid>https://dev.to/techwithhari/automating-terraform-import-for-existing-azure-infrastructure-via-pipeline-23d5</guid>
      <description>&lt;p&gt;Recently, I faced an interesting challenge while working with Terraform and Azure. I had already deployed a bunch of resources container apps, storage accounts, and more manually. Later, my team wanted to &lt;strong&gt;replicate this setup automatically&lt;/strong&gt; in another Azure account using Terraform pipelines.&lt;/p&gt;

&lt;p&gt;At first, it sounded straightforward just write Terraform code for everything, run the pipeline, and it’s done. But reality hit me hard:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do you make Terraform aware of resources that &lt;strong&gt;already exist&lt;/strong&gt;?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Terraform relies on its &lt;strong&gt;state file&lt;/strong&gt; to track infrastructure. If a resource exists in Azure but isn’t in Terraform’s state, running &lt;code&gt;terraform apply&lt;/code&gt; will attempt to &lt;strong&gt;recreate it&lt;/strong&gt;, which either fails or risks breaking things.&lt;/p&gt;

&lt;p&gt;A few complications in my setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We were using a &lt;strong&gt;remote backend&lt;/strong&gt; for the state (so the state wasn’t local).&lt;/li&gt;
&lt;li&gt;Manual &lt;code&gt;terraform import&lt;/code&gt; was not practical for dozens of resources.&lt;/li&gt;
&lt;li&gt;We wanted the &lt;strong&gt;pipeline to be fully automated&lt;/strong&gt;, without manual interventions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, I was stuck until the idea hit me.&lt;/p&gt;




&lt;h2&gt;
  
  
  The “Aha” Moment
&lt;/h2&gt;

&lt;p&gt;I thought:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Terraform can create resources via pipeline automatically…&lt;br&gt;
Why can’t I &lt;strong&gt;import existing resources via pipeline&lt;/strong&gt; too?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And that’s when I realized I could &lt;strong&gt;automate the import process&lt;/strong&gt; itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;Here’s what I did:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create a Separate Import Pipeline
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;I wrote a dedicated pipeline that loops over existing Azure resources.&lt;/li&gt;
&lt;li&gt;For each resource, it runs &lt;code&gt;terraform import&lt;/code&gt; using Terraform CLI.&lt;/li&gt;
&lt;li&gt;The state gets automatically recorded in the &lt;strong&gt;remote backend&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Sync the State
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;After the import pipeline runs, the Terraform state now accurately reflects &lt;strong&gt;all existing resources&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;No more mismatch between Azure and Terraform.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Run the Regular Terraform Pipeline
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Now, the normal pipeline (&lt;code&gt;terraform apply&lt;/code&gt;) works seamlessly.&lt;/li&gt;
&lt;li&gt;Terraform can manage both &lt;strong&gt;existing&lt;/strong&gt; and &lt;strong&gt;new resources&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The setup is now fully &lt;strong&gt;repeatable&lt;/strong&gt; in other Azure accounts.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;This approach solved multiple challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automates importing resources no manual effort.&lt;/li&gt;
&lt;li&gt;Works with remote backends.&lt;/li&gt;
&lt;li&gt;Ensures Terraform pipelines can manage &lt;strong&gt;existing and new infra&lt;/strong&gt; seamlessly.&lt;/li&gt;
&lt;li&gt;Makes infrastructure replication across accounts practical and safe.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Terraform Import is pipeline-friendly&lt;/strong&gt;: You don’t need to run it manually for each resource.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote backend doesn’t block imports&lt;/strong&gt;: You just need a process to sync state first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Planning is critical&lt;/strong&gt;: When onboarding existing infrastructure into IaC, always think about &lt;strong&gt;state synchronization&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;This experience taught me that Terraform is incredibly flexible if you combine &lt;strong&gt;automation&lt;/strong&gt; with a little bit of creativity. Sometimes, the solution is not in writing more code, but in &lt;strong&gt;automating the right process&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;💡 &lt;strong&gt;Pro Tip:&lt;/strong&gt; Always test imports on non-production environments first to avoid accidental overrides.&lt;/p&gt;




</description>
      <category>terraform</category>
      <category>cicd</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
