<?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>Forem: Yoni Gofman</title>
    <description>The latest articles on Forem by Yoni Gofman (@yonigofman).</description>
    <link>https://forem.com/yonigofman</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%2F3846370%2F79011ed6-49af-4942-b59b-34f3b4476536.jpeg</url>
      <title>Forem: Yoni Gofman</title>
      <link>https://forem.com/yonigofman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/yonigofman"/>
    <language>en</language>
    <item>
      <title>Zero-Downtime Argo CD Migrations: The Ultimate Guide to ApplicationSet Refactoring</title>
      <dc:creator>Yoni Gofman</dc:creator>
      <pubDate>Sat, 28 Mar 2026 12:15:40 +0000</pubDate>
      <link>https://forem.com/yonigofman/zero-downtime-migration-moving-resources-between-argo-cd-applicationsets-4d2h</link>
      <guid>https://forem.com/yonigofman/zero-downtime-migration-moving-resources-between-argo-cd-applicationsets-4d2h</guid>
      <description>&lt;p&gt;In a GitOps world, your &lt;code&gt;ApplicationSet&lt;/code&gt; is the source of truth for your fleet. But what happens when you need to rename a set, refactor your generators, or migrate workloads to a completely new Kubernetes cluster?&lt;/p&gt;

&lt;p&gt;The default behavior of Argo CD is &lt;strong&gt;Cascading Deletion&lt;/strong&gt;: delete the parent (AppSet), and you delete the children (Applications) and the grandchildren (K8s Resources). In production, this is a nightmare.&lt;/p&gt;

&lt;p&gt;This guide covers how to bypass the "Delete-everything" trap using the &lt;strong&gt;Orphan Strategy&lt;/strong&gt; for three real-world scenarios.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Foundation: The "Orphan" Safety Net
&lt;/h2&gt;

&lt;p&gt;Before performing any migration, you must ensure that deleting the &lt;code&gt;ApplicationSet&lt;/code&gt; doesn't trigger a cleanup of your production environment. &lt;/p&gt;

&lt;p&gt;Add this to your &lt;code&gt;ApplicationSet&lt;/code&gt; spec:&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;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;syncPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;preserveResourcesOnDeletion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does:&lt;/strong&gt; It tells the controller to remove the &lt;code&gt;ApplicationSet&lt;/code&gt; and the &lt;code&gt;Application&lt;/code&gt; CRDs but leave the actual Kubernetes resources (Deployments, Services, etc.) running in the cluster.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Pruning Pitfall: Don't Let Self-Heal Kill Your Migration
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This is the most critical part of the migration.&lt;/strong&gt; If your Root App or ApplicationSet has &lt;code&gt;Prune: true&lt;/code&gt; and &lt;code&gt;SelfHeal: true&lt;/code&gt; enabled, Argo CD will try to delete anything that doesn't match the Git state &lt;strong&gt;the second you make a change&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Strategy:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Disable Pruning Temporarily:&lt;/strong&gt; Before starting the migration, set &lt;code&gt;prune: false&lt;/code&gt; in your SyncPolicy.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Verify via Dry-Run:&lt;/strong&gt; Use the CLI to see what &lt;em&gt;would&lt;/em&gt; be pruned:
&lt;code&gt;argocd app sync &amp;lt;app-name&amp;gt; --dry-run --prune&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Use &lt;code&gt;PrunePropagationPolicy=orphan&lt;/code&gt;:&lt;/strong&gt; If you are deleting via &lt;code&gt;kubectl&lt;/code&gt;, you can specify the propagation policy to ensure resources aren't reaped.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  3. Scenario A: In-Place Refactoring (Renaming/Repo Change)
&lt;/h2&gt;

&lt;p&gt;Use this when you want to rename an AppSet or move its definition to a different Git repository without causing downtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step-by-Step:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Patch the Existing Set:&lt;/strong&gt; Apply the &lt;code&gt;preserveResourcesOnDeletion: true&lt;/code&gt; patch.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The "Detachment":&lt;/strong&gt; Delete the old &lt;code&gt;ApplicationSet&lt;/code&gt;. 

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Result:&lt;/strong&gt; Your Applications will now appear in the Argo CD UI as "Orphans".&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Deploy the New Set:&lt;/strong&gt; Create the new &lt;code&gt;ApplicationSet&lt;/code&gt;. Ensure the &lt;code&gt;template&lt;/code&gt; generates Applications with the &lt;strong&gt;exact same name&lt;/strong&gt; as the orphaned ones.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Pro-Tip: Use Server-Side Apply (SSA)&lt;/strong&gt;&lt;br&gt;
To avoid metadata conflicts when the new manager takes over, enable SSA in the template:&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;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;syncPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;syncOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ServerSideApply=true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Scenario B: Target Cluster Migration
&lt;/h2&gt;

&lt;p&gt;Use this when you are moving workloads from an old cluster to a brand new one.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Strategy: Side-by-Side Provisioning
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Dual-Targeting:&lt;/strong&gt; Update your generator to include &lt;strong&gt;both&lt;/strong&gt; the old and the new cluster.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Avoid Collisions:&lt;/strong&gt; Ensure your application names include the cluster name (e.g., &lt;code&gt;name: '{{cluster}}-{{app}}'&lt;/code&gt;) to keep them unique.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Cut-over:&lt;/strong&gt; * Verify the new cluster is &lt;code&gt;Healthy&lt;/code&gt;.

&lt;ul&gt;
&lt;li&gt;Switch your DNS/Traffic to the new cluster.&lt;/li&gt;
&lt;li&gt;Remove the old cluster from the generator. The &lt;code&gt;preserveResourcesOnDeletion&lt;/code&gt; flag will keep the old apps as orphans until you're ready to manually delete them.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  5. Scenario C: The "App of Apps" Hierarchy
&lt;/h2&gt;

&lt;p&gt;When a &lt;strong&gt;Root App&lt;/strong&gt; manages your &lt;strong&gt;ApplicationSet&lt;/strong&gt;, you have a three-layer deletion chain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Breaking the Chain
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Orphan the AppSet:&lt;/strong&gt; Delete the Root App using the &lt;code&gt;--cascade=false&lt;/code&gt; flag via the CLI:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;argocd app delete root-app &lt;span class="nt"&gt;--cascade&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Disable Root-Level Pruning:&lt;/strong&gt; Ensure the new Root App is deployed with &lt;code&gt;prune: false&lt;/code&gt; for the first sync to prevent it from cleaning up the existing AppSet if there's a minor naming mismatch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Re-parenting:&lt;/strong&gt; Once the new Root App "adopts" the AppSet, you can safely re-enable pruning.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Senior Level Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Finalizers:&lt;/strong&gt; Check for &lt;code&gt;resources-finalizer.argocd.argoproj.io&lt;/code&gt;. If it's there and you don't use the orphan flags, resources &lt;strong&gt;will&lt;/strong&gt; be deleted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignore Scaling Drift:&lt;/strong&gt; Use &lt;code&gt;ignoreDifferences&lt;/code&gt; for &lt;code&gt;replicas&lt;/code&gt; to prevent the migration from resetting your production scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync Strategy:&lt;/strong&gt; Always start with &lt;code&gt;Manual Sync&lt;/code&gt; during a migration. Only switch back to &lt;code&gt;Automated&lt;/code&gt; once you've verified the adoption.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary Runbook
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Disable Pruning&lt;/strong&gt; on the Root/Parent level.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Patch&lt;/strong&gt; &lt;code&gt;preserveResources&lt;/code&gt; on the AppSet.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Decouple&lt;/strong&gt; (use &lt;code&gt;cascade=false&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Deploy&lt;/strong&gt; the new definition &amp;amp; Verify via Dry-run.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>argocd</category>
      <category>sre</category>
      <category>devops</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Stop Rebuilding Frontends for Every Environment</title>
      <dc:creator>Yoni Gofman</dc:creator>
      <pubDate>Fri, 27 Mar 2026 14:57:31 +0000</pubDate>
      <link>https://forem.com/yonigofman/stop-rebuilding-frontends-for-every-environment-k8m</link>
      <guid>https://forem.com/yonigofman/stop-rebuilding-frontends-for-every-environment-k8m</guid>
      <description>&lt;p&gt;As a DevOps engineer, one frontend problem has always felt unnecessarily expensive to me:&lt;/p&gt;

&lt;p&gt;You build once for staging, then rebuild again for production because the frontend baked environment variables into the bundle.&lt;/p&gt;

&lt;p&gt;Same app. Same code. Different environment. Another build.&lt;/p&gt;

&lt;p&gt;That works, but it breaks one of the cleanest deployment ideas we have: build once, promote the same artifact everywhere.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;clientshell&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It lets you inject public runtime config into already-built frontend apps at container startup, without rebuilding for every environment.&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://yonigofman.github.io/clientshell/" rel="noopener noreferrer"&gt;https://yonigofman.github.io/clientshell/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Most frontend tooling treats environment variables as build-time values.&lt;/p&gt;

&lt;p&gt;That means if your app needs a different API URL, feature flag, analytics key, or public tenant setting in another environment, the artifact changes. So your pipeline ends up doing this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;build for dev&lt;/li&gt;
&lt;li&gt;build for staging&lt;/li&gt;
&lt;li&gt;build for prod&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From a DevOps point of view, that is friction.&lt;/p&gt;

&lt;p&gt;It makes deployments slower, introduces more room for drift, and weakens the "immutable artifact" model that works so well in modern delivery pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I wanted instead
&lt;/h2&gt;

&lt;p&gt;I wanted a flow like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build the frontend once&lt;/li&gt;
&lt;li&gt;Ship the same static bundle everywhere&lt;/li&gt;
&lt;li&gt;Inject environment-specific public config at runtime&lt;/li&gt;
&lt;li&gt;Keep the frontend DX typed and predictable&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is what &lt;strong&gt;clientshell&lt;/strong&gt; does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What clientshell is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;clientshell&lt;/code&gt; is a small toolchain for runtime public config in frontend apps.&lt;/p&gt;

&lt;p&gt;It has a few pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@clientshell/core&lt;/code&gt; for defining and reading typed public config&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@clientshell/zod&lt;/code&gt; if you want to define the schema with Zod&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@clientshell/vite&lt;/code&gt;, &lt;code&gt;@clientshell/webpack&lt;/code&gt;, and &lt;code&gt;@clientshell/rollup&lt;/code&gt; plugins&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@clientshell/cli&lt;/code&gt; for manifest generation and validation&lt;/li&gt;
&lt;li&gt;a fast Go-based injector for container startup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;define a schema in TypeScript&lt;/li&gt;
&lt;li&gt;generate a manifest during build&lt;/li&gt;
&lt;li&gt;inject values into &lt;code&gt;/env-config.js&lt;/code&gt; at runtime&lt;/li&gt;
&lt;li&gt;read them in the browser with a typed API&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this matters in DevOps
&lt;/h2&gt;

&lt;p&gt;This project came from a DevOps mindset more than a frontend one.&lt;/p&gt;

&lt;p&gt;The main benefit is not just convenience. It is deployment discipline.&lt;/p&gt;

&lt;p&gt;With runtime injection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the same image can go to staging and production&lt;/li&gt;
&lt;li&gt;environment differences move to runtime config, not build logic&lt;/li&gt;
&lt;li&gt;CI pipelines get simpler&lt;/li&gt;
&lt;li&gt;rollbacks are cleaner&lt;/li&gt;
&lt;li&gt;supply chain and provenance story improves because you publish fewer environment-specific artifacts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words: your frontend starts behaving more like a real deployable artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple example
&lt;/h2&gt;

&lt;p&gt;Define your public config shape once:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@clientshell/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clientEnvSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineSchema&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;API_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;ENABLE_BETA&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it in your app:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readEnvFromShape&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@clientshell/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;clientEnvSchema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./env.schema&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;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readEnvFromShape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clientEnvSchema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;API_URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At build time, clientshell creates a manifest.&lt;/p&gt;

&lt;p&gt;At runtime, the injector reads environment variables and serves the generated config to the browser.&lt;/p&gt;

&lt;p&gt;No rebuild required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use &lt;code&gt;window.__ENV__&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;You can. A lot of teams do.&lt;/p&gt;

&lt;p&gt;But that usually turns into custom glue code, ad hoc scripts, inconsistent schema handling, and no real validation story.&lt;/p&gt;

&lt;p&gt;What I wanted was something more structured:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;typed schema&lt;/li&gt;
&lt;li&gt;clear manifest format&lt;/li&gt;
&lt;li&gt;bundler support&lt;/li&gt;
&lt;li&gt;runtime injection&lt;/li&gt;
&lt;li&gt;container-friendly behavior&lt;/li&gt;
&lt;li&gt;CLI support for validation and generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So instead of one more shell-script-on-top-of-nginx setup, this became a small, focused toolchain.&lt;/p&gt;

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

&lt;p&gt;I think &lt;code&gt;clientshell&lt;/code&gt; is especially useful for teams that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deploy the same frontend artifact across multiple environments&lt;/li&gt;
&lt;li&gt;use Docker heavily&lt;/li&gt;
&lt;li&gt;want stronger separation between build and deploy phases&lt;/li&gt;
&lt;li&gt;care about typed public config&lt;/li&gt;
&lt;li&gt;are tired of environment-specific frontend rebuilds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are running Vite, Webpack, or Rollup-based apps and you want cleaner promotion pipelines, this is exactly the use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker example
&lt;/h2&gt;

&lt;p&gt;The runtime model works especially well with containers.&lt;/p&gt;

&lt;p&gt;A typical flow looks like this:&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;# 1. Build the app&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;node:20-alpine&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;pnpm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pnpm build

&lt;span class="c"&gt;# 2. Runtime image&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; clientshell&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/dist /app/dist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 9000:9000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;CLIENTSHELL_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;9000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;CLIENT_API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.example.com &lt;span class="se"&gt;\&lt;/span&gt;
  my-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the piece I like most.&lt;/p&gt;

&lt;p&gt;You stop coupling deployment environment to bundle generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on DX
&lt;/h2&gt;

&lt;p&gt;I did not want this to feel like a DevOps-only hack bolted onto frontend apps.&lt;/p&gt;

&lt;p&gt;So the project keeps frontend ergonomics in mind too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;schema is typed&lt;/li&gt;
&lt;li&gt;browser-side API is clean&lt;/li&gt;
&lt;li&gt;Vite integration is straightforward&lt;/li&gt;
&lt;li&gt;there is a Zod adapter if that is your preferred pattern&lt;/li&gt;
&lt;li&gt;local development still works with stubbed config&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the frontend team does not need to fight the delivery model just because the platform team wants cleaner deployments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;Honestly, because I got tired of treating frontend config as if it had to be frozen at build time.&lt;/p&gt;

&lt;p&gt;Backend and platform engineers have had better separation between build and runtime for a long time. Frontend delivery often lags behind there.&lt;/p&gt;

&lt;p&gt;I wanted something small, explicit, typed, and deployment-friendly.&lt;/p&gt;

&lt;p&gt;That became &lt;code&gt;clientshell&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  If this sounds familiar
&lt;/h2&gt;

&lt;p&gt;If you have ever thought:&lt;/p&gt;

&lt;p&gt;"Why am I rebuilding the exact same frontend just to change one public URL?"&lt;/p&gt;

&lt;p&gt;then this project is for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project
&lt;/h2&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/yonigofman/clientshell" rel="noopener noreferrer"&gt;https://github.com/yonigofman/clientshell&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://yonigofman.github.io/clientshell/" rel="noopener noreferrer"&gt;https://yonigofman.github.io/clientshell/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Packages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@clientshell/core" rel="noopener noreferrer"&gt;&lt;code&gt;@clientshell/core&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@clientshell/cli" rel="noopener noreferrer"&gt;&lt;code&gt;@clientshell/cli&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@clientshell/vite" rel="noopener noreferrer"&gt;&lt;code&gt;@clientshell/vite&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@clientshell/webpack" rel="noopener noreferrer"&gt;&lt;code&gt;@clientshell/webpack&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@clientshell/rollup" rel="noopener noreferrer"&gt;&lt;code&gt;@clientshell/rollup&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@clientshell/zod" rel="noopener noreferrer"&gt;&lt;code&gt;@clientshell/zod&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>frontend</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
