<?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: Mikhail Dorokhovich</title>
    <description>The latest articles on DEV Community by Mikhail Dorokhovich (@mikhail_dorokhovich_0c532).</description>
    <link>https://dev.to/mikhail_dorokhovich_0c532</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2184540%2F2a57f0ad-7d85-44d7-8b50-05d9dff396e4.png</url>
      <title>DEV Community: Mikhail Dorokhovich</title>
      <link>https://dev.to/mikhail_dorokhovich_0c532</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mikhail_dorokhovich_0c532"/>
    <language>en</language>
    <item>
      <title>The #1 beginner Kubernetes bug: Service with no endpoints (and why labels are the glue)</title>
      <dc:creator>Mikhail Dorokhovich</dc:creator>
      <pubDate>Sat, 15 Aug 2026 19:15:32 +0000</pubDate>
      <link>https://dev.to/mikhail_dorokhovich_0c532/the-1-beginner-kubernetes-bug-service-with-no-endpoints-and-why-labels-are-the-glue-43fo</link>
      <guid>https://dev.to/mikhail_dorokhovich_0c532/the-1-beginner-kubernetes-bug-service-with-no-endpoints-and-why-labels-are-the-glue-43fo</guid>
      <description>&lt;p&gt;Chapter 7 of a local-Kubernetes series: the three manifests you actually need — Namespace, Deployment, Service — and the "aha" that they're glued together by &lt;em&gt;labels and selectors&lt;/em&gt;, not hard references.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Namespace = a folder for your resources.&lt;/strong&gt; Don't dump apps in &lt;code&gt;default&lt;/code&gt;; a dedicated namespace makes cleanup, limits, and isolation easy. Same-namespace access uses the short name (&lt;code&gt;myapp&lt;/code&gt;); cross-namespace needs the FQDN (&lt;code&gt;myapp.myapp.svc.cluster.local&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The object hierarchy:&lt;/strong&gt; &lt;code&gt;Deployment → ReplicaSet → Pods&lt;/code&gt;. You describe only the Deployment; Kubernetes creates the rest. Pods are ephemeral (new IP on every recreate), so you never address them directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rule everyone trips on:&lt;/strong&gt; &lt;code&gt;spec.selector.matchLabels&lt;/code&gt; must match &lt;code&gt;spec.template.metadata.labels&lt;/code&gt;. Diverge and Kubernetes rejects the manifest at apply time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service (ClusterIP)&lt;/strong&gt; gives a group of Pods one stable virtual IP + DNS name, reachable &lt;em&gt;only inside&lt;/em&gt; the cluster; it tracks which Pods match its selector via EndpointSlices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;port&lt;/code&gt; vs &lt;code&gt;targetPort&lt;/code&gt;:&lt;/strong&gt; &lt;code&gt;port&lt;/code&gt; is what the Service listens on (we use 80); &lt;code&gt;targetPort&lt;/code&gt; is the Pod's port (8080). Defaults to &lt;code&gt;port&lt;/code&gt; if omitted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The single most common bug:&lt;/strong&gt; the Service's selector doesn't match the Pods' labels → &lt;code&gt;kubectl describe svc&lt;/code&gt; shows &lt;code&gt;Endpoints: &amp;lt;none&amp;gt;&lt;/code&gt; → the service exists but traffic goes nowhere. One command to diagnose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;apply&lt;/code&gt; (declarative, idempotent) vs &lt;code&gt;create&lt;/code&gt; (imperative, fails if it exists).&lt;/strong&gt; Always &lt;code&gt;apply&lt;/code&gt; for Git-tracked manifests. &lt;code&gt;kubectl diff -f&lt;/code&gt; before applying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep manifests in Git next to the code&lt;/strong&gt; (a flat &lt;code&gt;k8s/&lt;/code&gt; folder); reach for Kustomize base+overlays when environments diverge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full article: &lt;a href="https://dorokhovich.com/blog/local-k8s-kubernetes-manifests?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=local-k8s-kubernetes-manifests" rel="noopener noreferrer"&gt;https://dorokhovich.com/blog/local-k8s-kubernetes-manifests?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=local-k8s-kubernetes-manifests&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Local Kubernetes Dev — Part 7: Kubernetes manifests for your service</title>
      <dc:creator>Mikhail Dorokhovich</dc:creator>
      <pubDate>Thu, 13 Aug 2026 09:49:08 +0000</pubDate>
      <link>https://dev.to/mikhail_dorokhovich_0c532/local-kubernetes-dev-part-7-kubernetes-manifests-for-your-service-4lo5</link>
      <guid>https://dev.to/mikhail_dorokhovich_0c532/local-kubernetes-dev-part-7-kubernetes-manifests-for-your-service-4lo5</guid>
      <description>&lt;p&gt;"The Service exists, but traffic never reaches the pods, and Endpoints is empty" — the most common beginner bug in Kubernetes. And the cause is almost always the same one.&lt;/p&gt;

&lt;p&gt;Part seven of the series is the chapter where, instead of copy-pasting YAML, you finally understand HOW Kubernetes objects connect to each other. We write three minimal manifests for myapp — Namespace, Deployment, Service — and unpack what you really need to internalize: labels and selectors are the glue that binds Deployment and Service together. No hard references like "Service, here are the IDs of these pods" — it's all held together by matching labels.&lt;/p&gt;

&lt;p&gt;The key rule everyone trips on: a Deployment's &lt;code&gt;spec.selector.matchLabels&lt;/code&gt; MUST match its &lt;code&gt;spec.template.metadata.labels&lt;/code&gt;. And the big trap — the Service selector doesn't match the pod labels (a typo, or you forgot to update it): the Service just hangs, and &lt;code&gt;kubectl describe svc&lt;/code&gt; shows &lt;code&gt;Endpoints: &amp;lt;none&amp;gt;&lt;/code&gt;. The service seems to exist, but traffic goes nowhere. You can verify it instantly.&lt;/p&gt;

&lt;p&gt;Plus: how ClusterIP differs from a pod's direct address, the difference between &lt;code&gt;port&lt;/code&gt; and &lt;code&gt;targetPort&lt;/code&gt; (80 vs 8080), declarative &lt;code&gt;kubectl apply&lt;/code&gt; vs imperative &lt;code&gt;create&lt;/code&gt;, and where to keep your manifests (spoiler: in Git next to the code, in a &lt;code&gt;k8s/&lt;/code&gt; folder). &lt;a href="https://dorokhovich.com/blog/local-k8s-kubernetes-manifests?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=local-k8s-kubernetes-manifests" rel="noopener noreferrer"&gt;https://dorokhovich.com/blog/local-k8s-kubernetes-manifests?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=local-k8s-kubernetes-manifests&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Storytelling in Software Design: The Principle That Makes Architecture Docs Actually Stick</title>
      <dc:creator>Mikhail Dorokhovich</dc:creator>
      <pubDate>Wed, 12 Aug 2026 12:32:07 +0000</pubDate>
      <link>https://dev.to/mikhail_dorokhovich_0c532/storytelling-in-software-design-the-principle-that-makes-architecture-docs-actually-stick-4g3a</link>
      <guid>https://dev.to/mikhail_dorokhovich_0c532/storytelling-in-software-design-the-principle-that-makes-architecture-docs-actually-stick-4g3a</guid>
      <description>&lt;h2&gt;
  
  
  The problem in context
&lt;/h2&gt;

&lt;p&gt;Every engineering org I have worked with has the same artifact: a wiki page that opens with something like &lt;em&gt;'Three-tier architecture: presentation, business logic, data access.'&lt;/em&gt; It is accurate. It is correct. And it is completely inert. New hires read it, nod, and then spend their first month asking questions the page technically already answered.&lt;/p&gt;

&lt;p&gt;The instinct is to blame the writer or the tooling — not enough diagrams, wrong wiki, stale content. That instinct is wrong. The information was fine. What failed was the &lt;em&gt;form&lt;/em&gt;. We had hidden the meaning behind jargon and boxes-and-arrows and stripped out the one thing human cognition is actually built to retain: a throughline. A component inventory is a list of nouns. A system is a sequence of things happening to actors over time — and our brains file the second away far more reliably than the first.&lt;/p&gt;

&lt;p&gt;That gap between what we produce (inventories) and what people retain (narratives) is the whole problem. It shows up as slow onboarding, as design reviews that miss the point, and as decisions that evaporate the moment the person who made them leaves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The principle
&lt;/h2&gt;

&lt;p&gt;The principle here is that &lt;strong&gt;story is a design technique, not decoration.&lt;/strong&gt; A narrative — actors, an event, a resolution — is a way of structuring concepts so that intent is visible, and intent is the part that has to survive new people and the passage of time.&lt;/p&gt;

&lt;p&gt;Here is the same system, documented two ways:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Inventory form:&lt;/strong&gt; 'Three-tier architecture: presentation, business logic, data access.'&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Narrative form:&lt;/strong&gt; 'Clients (presentation) consult experts (domain) who consult the archive (data) and return a decision. Each request is a plotline that crosses actors and comes back with a resolution.'&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The narrative version is not fluffier — it is &lt;em&gt;more&lt;/em&gt; precise, because it exposes intent rather than just structure. A reader who absorbs the second version can reason about where a new feature belongs. A reader who memorizes the first can only recite it. That is the entire thesis in miniature: narrative scales because intent scales, and implementation details do not.&lt;/p&gt;

&lt;p&gt;The leverage point is that you do not need a new methodology to apply this. The mature design practices are &lt;em&gt;already&lt;/em&gt; narrative — most teams just run them mechanically and miss it. There is a detailed treatment of this reframing in &lt;a href="https://dorokhovich.com/blog/storytelling-software-design?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=success-story&amp;amp;utm_content=storytelling-software-design" rel="noopener noreferrer"&gt;a longer essay on software design as storytelling&lt;/a&gt; if you want the full argument; the short version is that five techniques you likely already own are stories wearing engineering clothes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Domain-Driven Design (DDD).&lt;/strong&gt; &lt;a href="https://martinfowler.com/bliki/BoundedContext.html" rel="noopener noreferrer"&gt;Bounded contexts&lt;/a&gt; are chapters; the ubiquitous language is the cast's shared dialogue. Ask 'who are the characters here and what words do they use?' and boundaries that looked arbitrary suddenly have reasons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavior-Driven Development (BDD).&lt;/strong&gt; Given / When / Then, &lt;a href="https://dannorth.net/blog/introducing-bdd/" rel="noopener noreferrer"&gt;the grammar Dan North coined for BDD&lt;/a&gt;, is a plot: a setup, an event, a resolution — readable by every stakeholder and executable as a spec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event Storming.&lt;/strong&gt; &lt;a href="https://www.eventstorming.com/" rel="noopener noreferrer"&gt;Alberto Brandolini's workshop format&lt;/a&gt; models events, commands, and policies on a wall until the arc of the system emerges in sticky notes. You watch the plot appear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain Storytelling.&lt;/strong&gt; &lt;a href="https://domainstorytelling.org/" rel="noopener noreferrer"&gt;The most literal technique&lt;/a&gt;, documented by Stefan Hofer and Henning Schwentner and &lt;a href="https://www.thoughtworks.com/radar/techniques/domain-storytelling" rel="noopener noreferrer"&gt;featured on the Thoughtworks Technology Radar&lt;/a&gt;: domain experts draw their workflow as numbered steps — actors, work objects, activities, in order. 'The &lt;em&gt;dispatcher&lt;/em&gt; assigns the &lt;em&gt;order&lt;/em&gt; to a &lt;em&gt;driver&lt;/em&gt;' hands you the language and the boundaries for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture Decision Records (ADRs).&lt;/strong&gt; &lt;a href="https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions" rel="noopener noreferrer"&gt;Michael Nygard's format&lt;/a&gt; is the memoir: context, decision, consequences — the &lt;em&gt;why&lt;/em&gt;, preserved for whoever inherits it. Pair it with &lt;a href="https://c4model.com/" rel="noopener noreferrer"&gt;Simon Brown's C4 model&lt;/a&gt; for the map, but keep the rule that every diagram earns a sentence of narrative or it is just decoration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;p&gt;Narrative design is not free, and treating it as an unqualified good is how it degrades into whimsy. The honest comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Inventory docs&lt;/th&gt;
&lt;th&gt;Narrative design&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Onboarding retention&lt;/td&gt;
&lt;td&gt;Read once, forgotten&lt;/td&gt;
&lt;td&gt;Retained and reasoned about&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authoring cost&lt;/td&gt;
&lt;td&gt;Low up front&lt;/td&gt;
&lt;td&gt;Higher up front, lower over time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Precision&lt;/td&gt;
&lt;td&gt;Exact but intent-free&lt;/td&gt;
&lt;td&gt;Exact &lt;em&gt;and&lt;/em&gt; intent-bearing when done well&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure mode&lt;/td&gt;
&lt;td&gt;Inert, ignored&lt;/td&gt;
&lt;td&gt;Vague metaphor if intent is hidden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-team alignment&lt;/td&gt;
&lt;td&gt;Repeated meetings&lt;/td&gt;
&lt;td&gt;Shared story spine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decision durability&lt;/td&gt;
&lt;td&gt;Lost when people leave&lt;/td&gt;
&lt;td&gt;Preserved in ADRs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business 'why'&lt;/td&gt;
&lt;td&gt;In one senior head&lt;/td&gt;
&lt;td&gt;Embedded in the artifact&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two costs that matter: narrative takes more effort to write well, and it fails badly when done poorly. A story that hides detail instead of exposing intent is worse than a dry doc — you have added metaphor without adding clarity. So the discipline is that &lt;em&gt;narrative must be more precise, never less.&lt;/em&gt; If your reframing makes the system harder to reason about, revert it.&lt;/p&gt;

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

&lt;p&gt;The adoption failure I see most often is boiling the ocean — a team decides to rewrite every document as a story and stalls under the weight. Stage it instead.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with the single most-ignored document&lt;/strong&gt;, usually the onboarding page, and rewrite it as a story with actors, a journey, and a resolution. Prove the principle on the artifact that hurts most before you touch anything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write your next architecture decision as an ADR the day you make it.&lt;/strong&gt; An ADR written at decision time preserves intent; one reconstructed six months later preserves a guess. This is the highest-leverage habit on the list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run one flow through Event Storming as a group.&lt;/strong&gt; Put a single user-facing flow on a wall — events, commands, actors — and narrate it end to end out loud: 'the customer does this, which triggers that, which billing decides on, and the resolution is this.' Within an hour the arc emerges, and so do the boundary disagreements the team has been silently carrying. Saying the story together is what makes the ubiquitous language stick — it arrives by conversation, not decree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce the vocabulary.&lt;/strong&gt; If the story's words and the code's names drift apart, the throughline is gone. Keep them in sync or the whole effort decays.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice what this is &lt;em&gt;not&lt;/em&gt;: no new framework, no tool purchase, no reorg. You are changing the form of what you already produce so the meaning survives contact with new people and with time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;The direction of travel is that narrative stops being how you &lt;em&gt;document&lt;/em&gt; a system and becomes how you &lt;em&gt;design&lt;/em&gt; it. Once a team pitches features as plotlines — 'here is the actor, here is what changes, here is the resolution' — reviews sharpen automatically, because a reviewer can ask 'whose story is this and where does it resolve?' and expose a design gap no checklist would catch.&lt;/p&gt;

&lt;p&gt;There is a second horizon worth watching: as more of the design surface gets handed to AI assistants, intent-bearing artifacts become the differentiator. A model can generate a component inventory trivially; what it cannot invent is &lt;em&gt;why&lt;/em&gt; your bounded contexts sit where they do. Teams that have captured that why as narrative and ADRs will hand their tools far better context than teams sitting on boxes-and-arrows. The organizations that treat storytelling software design as a first-class engineering practice — rather than a nicety for the onboarding wiki — are the ones whose intent will outlast both their people and their tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources &amp;amp; further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Stefan Hofer &amp;amp; Henning Schwentner — &lt;a href="https://domainstorytelling.org/" rel="noopener noreferrer"&gt;Domain Storytelling&lt;/a&gt;, and its &lt;a href="https://www.thoughtworks.com/radar/techniques/domain-storytelling" rel="noopener noreferrer"&gt;Thoughtworks Technology Radar entry&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Alberto Brandolini — &lt;a href="https://www.eventstorming.com/" rel="noopener noreferrer"&gt;EventStorming&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Martin Fowler — &lt;a href="https://martinfowler.com/bliki/BoundedContext.html" rel="noopener noreferrer"&gt;Bounded Context&lt;/a&gt; (Domain-Driven Design)&lt;/li&gt;
&lt;li&gt;Dan North — &lt;a href="https://dannorth.net/blog/introducing-bdd/" rel="noopener noreferrer"&gt;Introducing BDD&lt;/a&gt; (the origin of Given/When/Then)&lt;/li&gt;
&lt;li&gt;Michael Nygard — &lt;a href="https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions" rel="noopener noreferrer"&gt;Documenting Architecture Decisions&lt;/a&gt; (the original ADR format)&lt;/li&gt;
&lt;li&gt;Simon Brown — &lt;a href="https://c4model.com/" rel="noopener noreferrer"&gt;The C4 model for visualising software architecture&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A longer reference treatment of &lt;a href="https://dorokhovich.com/blog/storytelling-software-design?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=success-story&amp;amp;utm_content=storytelling-software-design" rel="noopener noreferrer"&gt;reframing software design as storytelling&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>A production Dockerfile for FastAPI: the two mistakes beginners always make</title>
      <dc:creator>Mikhail Dorokhovich</dc:creator>
      <pubDate>Tue, 11 Aug 2026 19:31:20 +0000</pubDate>
      <link>https://dev.to/mikhail_dorokhovich_0c532/devto-reddit-rdocker-dockerfile-best-practices-and-hardening-lists-are-highly-shareable-1dod</link>
      <guid>https://dev.to/mikhail_dorokhovich_0c532/devto-reddit-rdocker-dockerfile-best-practices-and-hardening-lists-are-highly-shareable-1dod</guid>
      <description>&lt;p&gt;Chapter 6 of a local-Kubernetes series. Not another "your first Dockerfile" — it leads with the two things beginners get wrong that have real consequences: running as root (a security liability) and cache-busting layer order (a productivity tax).&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Layer order is caching.&lt;/strong&gt; Put what rarely changes first. &lt;code&gt;COPY requirements.txt&lt;/code&gt; + &lt;code&gt;pip install&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; &lt;code&gt;COPY ./app&lt;/code&gt;, so a one-line code edit doesn't reinstall every dependency. The reverse (&lt;code&gt;COPY . .&lt;/code&gt; before install) is the most common anti-pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run as a non-root user.&lt;/strong&gt; &lt;code&gt;adduser --disabled-password --uid 10001 appuser&lt;/code&gt; then &lt;code&gt;USER appuser&lt;/code&gt;; reinforce with &lt;code&gt;securityContext.runAsNonRoot: true&lt;/code&gt; in the Pod. Least privilege, smaller attack surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-stage build:&lt;/strong&gt; install deps into a venv in a &lt;code&gt;build&lt;/code&gt; stage, &lt;code&gt;COPY --from=build /opt/venv /opt/venv&lt;/code&gt; into a clean runtime image — no compilers or pip caches in the final layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Base image trade-offs:&lt;/strong&gt; &lt;code&gt;slim&lt;/code&gt; (glibc, best wheel compatibility — the sane default), &lt;code&gt;alpine&lt;/code&gt; (musl → pip often compiles from source, slow/fragile, DNS quirks), &lt;code&gt;distroless&lt;/code&gt; (no shell/package manager, great secure runtime, exec-form only, pair with multi-stage).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;exec form of CMD is not optional:&lt;/strong&gt; &lt;code&gt;["fastapi","run",...]&lt;/code&gt; makes the app PID 1 and receives &lt;code&gt;SIGTERM&lt;/code&gt; directly. Shell form wraps it in &lt;code&gt;/bin/sh&lt;/code&gt;, SIGTERM never reaches the app, and graceful shutdown breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI specifics:&lt;/strong&gt; &lt;code&gt;fastapi run&lt;/code&gt; (prod-tuned uvicorn) not bare uvicorn; never &lt;code&gt;--reload&lt;/code&gt; in the cluster image; &lt;code&gt;PYTHONUNBUFFERED=1&lt;/code&gt; or logs never reach &lt;code&gt;kubectl logs&lt;/code&gt;; add &lt;code&gt;--proxy-headers&lt;/code&gt; behind Ingress.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HEALTHCHECK without curl:&lt;/strong&gt; &lt;code&gt;slim&lt;/code&gt;/distroless have no &lt;code&gt;curl&lt;/code&gt;, so probe with Python's &lt;code&gt;urllib&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The finale (and the trap):&lt;/strong&gt; &lt;code&gt;docker build&lt;/code&gt; does NOT make the image visible to k3d — its nodes run isolated containerd. Deliver it via &lt;code&gt;k3d image import myapp:dev -c dev&lt;/code&gt; (+ &lt;code&gt;imagePullPolicy: IfNotPresent&lt;/code&gt;) or push to the built-in registry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full article: &lt;a href="https://dorokhovich.com/blog/local-k8s-containerizing-your-service?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=local-k8s-containerizing-your-service" rel="noopener noreferrer"&gt;https://dorokhovich.com/blog/local-k8s-containerizing-your-service?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=local-k8s-containerizing-your-service&lt;/a&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>devops</category>
      <category>docker</category>
      <category>security</category>
    </item>
    <item>
      <title>A Raspberry Pi VNC Remote Desktop From Anywhere: The Principle of Multiplexing a Tunnel You Already Own</title>
      <dc:creator>Mikhail Dorokhovich</dc:creator>
      <pubDate>Tue, 11 Aug 2026 19:26:01 +0000</pubDate>
      <link>https://dev.to/mikhail_dorokhovich_0c532/a-raspberry-pi-vnc-remote-desktop-from-anywhere-the-principle-of-multiplexing-a-tunnel-you-already-2l39</link>
      <guid>https://dev.to/mikhail_dorokhovich_0c532/a-raspberry-pi-vnc-remote-desktop-from-anywhere-the-principle-of-multiplexing-a-tunnel-you-already-2l39</guid>
      <description>&lt;h2&gt;
  
  
  The problem in context
&lt;/h2&gt;

&lt;p&gt;SSH into a home Raspberry Pi gives you a black text console — perfect for editing configs, useless for anything with a mouse. Most of the time that is fine, but there is always a stubborn 10%: a graphical config tool with no CLI equivalent, or a camera app whose live preview you genuinely need to &lt;em&gt;see&lt;/em&gt;. For those, you need VNC — screen-sharing that streams the Pi's desktop to your laptop and sends clicks and keystrokes back.&lt;/p&gt;

&lt;p&gt;The naive path is to install a VNC server, forward port 5900 on the router, and connect. Two things kill that plan. First, a Pi behind carrier-grade NAT has no router port to forward — the same constraint that forces an outbound tunnel for SSH in the first place. Second, and more important, &lt;strong&gt;exposing raw VNC to the internet is a genuinely bad idea.&lt;/strong&gt; The protocol's own spec, &lt;a href="https://datatracker.ietf.org/doc/html/rfc6143" rel="noopener noreferrer"&gt;RFC 6143&lt;/a&gt;, concedes its password check is "cryptographically weak and is not intended for use on untrusted networks," and recommends tunneling it inside IPsec or SSH. So the real problem is not "how do I reach VNC" but "how do I get a graphical desktop remotely &lt;em&gt;without adding any new attack surface&lt;/em&gt;."&lt;/p&gt;

&lt;h2&gt;
  
  
  The principle
&lt;/h2&gt;

&lt;p&gt;The principle here is that &lt;strong&gt;an SSH tunnel is not limited to one port — the same encrypted connection that already carries SSH can carry a desktop too, so you multiplex a pipe you already own rather than opening a new one.&lt;/strong&gt; If a reverse SSH tunnel already forwards the Pi's port 22 out to a cloud bridge, VNC is just a second reverse forward on that same connection. Nothing new is exposed; the Pi still only ever dials outward; the traffic is wrapped in SSH the entire way.&lt;/p&gt;

&lt;p&gt;The mental model that makes it click is three ports and one path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Port 5900&lt;/strong&gt; on the Pi is where the VNC server already listens — per the &lt;a href="https://www.raspberrypi.com/documentation/computers/remote-access.html" rel="noopener noreferrer"&gt;official Raspberry Pi remote-access docs&lt;/a&gt;, a VNC server (wayvnc on current Raspberry Pi OS) is built in, so nothing gets installed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port 5901&lt;/strong&gt; on the bridge becomes the public front door that forwards into the tunnel.&lt;/li&gt;
&lt;li&gt;Everything hitting &lt;code&gt;EC2:5901&lt;/code&gt; gets redirected, encrypted, down to &lt;code&gt;RPi:5900&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VNC Viewer  --&amp;gt;  EC2:5901  --&amp;gt;  encrypted SSH tunnel  --&amp;gt;  RaspberryPi:5900  --&amp;gt;  desktop image back the same way
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is &lt;a href="https://dorokhovich.com/blog/rpi/vnc-ec2-setup?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=success-story&amp;amp;utm_content=rpi/vnc-ec2-setup" rel="noopener noreferrer"&gt;a detailed treatment of the full VNC-over-tunnel build&lt;/a&gt; with the port map and security-group walkthrough; the mechanical change is a single added flag. An existing systemd tunnel unit carrying only SSH looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="nt"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/usr/bin/ssh -i /home/pi/.ssh/id_tunnel ... -R 2222:localhost:22 tunnel@YOUR-EC2-IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding a second &lt;code&gt;-R&lt;/code&gt; — the same remote-forwarding mechanism from the &lt;a href="https://man.openbsd.org/ssh" rel="noopener noreferrer"&gt;ssh(1) man page&lt;/a&gt;, just another instance of it — carries the desktop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="nt"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/usr/bin/ssh -i /home/pi/.ssh/id_tunnel ... -R 2222:localhost:22 -R 5901:localhost:5900 tunnel@YOUR-EC2-IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After &lt;code&gt;systemctl daemon-reload&lt;/code&gt; and a restart, &lt;code&gt;ps aux | grep ssh | grep tunnel&lt;/code&gt; showing &lt;code&gt;-R 5901:localhost:5900&lt;/code&gt; in the running process is the checkpoint that the tunnel picked up the new forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;p&gt;Multiplexing an owned tunnel beats every VNC-in-the-cloud SaaS on the axes that matter for a homelab, but the choice carries real security decisions you have to make deliberately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Convenient / default&lt;/th&gt;
&lt;th&gt;Safer choice&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bridge port binding&lt;/td&gt;
&lt;td&gt;Loopback (SSH default)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GatewayPorts yes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A loopback-bound forward is reachable &lt;em&gt;from&lt;/em&gt; the bridge but never from your laptop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security-group source&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0.0.0.0/0&lt;/code&gt; on 5901&lt;/td&gt;
&lt;td&gt;Scope to your own IP&lt;/td&gt;
&lt;td&gt;Open means the whole internet can reach your VNC login prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VNC transport&lt;/td&gt;
&lt;td&gt;Raw VNC over the internet&lt;/td&gt;
&lt;td&gt;Wrapped in the SSH tunnel&lt;/td&gt;
&lt;td&gt;The protocol's own auth is weak by its spec's admission&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VNC server&lt;/td&gt;
&lt;td&gt;RealVNC free tier&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://tigervnc.org/" rel="noopener noreferrer"&gt;TigerVNC&lt;/a&gt; / wayvnc&lt;/td&gt;
&lt;td&gt;RealVNC increasingly pushes cloud-brokered connections, the opposite of a self-owned tunnel&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;GatewayPorts&lt;/code&gt; line is the one that eats an hour. Check the bridge with &lt;code&gt;sudo ss -tulpn | grep 5901&lt;/code&gt; and you will likely see it bound to &lt;code&gt;127.0.0.1&lt;/code&gt; — localhost only, reachable from the bridge but not from across the internet. This is SSH's safe default; the &lt;a href="https://man.openbsd.org/sshd_config" rel="noopener noreferrer"&gt;sshd_config(5) man page&lt;/a&gt; notes sshd binds remote forwardings to the loopback address unless told otherwise. One line, &lt;code&gt;GatewayPorts yes&lt;/code&gt;, plus &lt;code&gt;systemctl restart ssh&lt;/code&gt; and a tunnel restart, rebinds it to &lt;code&gt;0.0.0.0:5901&lt;/code&gt;. The rule worth internalizing: &lt;em&gt;a &lt;code&gt;127.0.0.1&lt;/code&gt;-bound forward is not broken, it is just missing &lt;code&gt;GatewayPorts&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The deeper trade-off is ownership versus convenience. Routing desktop pixels through a third-party VNC broker is easier, but the whole point of the owned tunnel is that no pixels ever leave infrastructure you control — the bridge is your box, the transport is plain SSH, and the only thing installed is a viewer on your laptop. That is worth defending, which is why the RealVNC-pushing-toward-cloud-brokering drift matters: if a direct address:port connection gets gated behind a subscription, TigerVNC or wayvnc speaks the same protocol over the same &lt;code&gt;-R 5901:localhost:5900&lt;/code&gt; forward, and the tunnel does not care which server sits on port 5900.&lt;/p&gt;

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

&lt;p&gt;This assumes the reverse SSH tunnel already exists — VNC rides on top of it, so build that foundation first if you are starting from scratch.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add the second &lt;code&gt;-R&lt;/code&gt; forward&lt;/strong&gt; to the existing systemd unit and confirm it in the running process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flip &lt;code&gt;GatewayPorts yes&lt;/code&gt; on the bridge&lt;/strong&gt;, restart sshd, then restart the tunnel on the Pi so the forward re-establishes against the updated server. Order matters — re-check ports on the bridge after each restart. A local &lt;code&gt;nc -zv localhost 5901&lt;/code&gt; on the bridge confirms the forward actually reaches the Pi before you touch the firewall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open exactly one firewall port&lt;/strong&gt;, scoped tight: a security-group inbound rule for TCP 5901 sourced to your own IP, not &lt;code&gt;0.0.0.0/0&lt;/code&gt;. If you need roaming access, keep the VNC password strong and layer SSH key auth in front.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Point a viewer at &lt;code&gt;EC2-IP:5901&lt;/code&gt;&lt;/strong&gt;, enter the VNC password, and the desktop fills the window — mouse and keyboard, live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the session pleasant, not just working.&lt;/strong&gt; Drop the viewer's color depth for a big responsiveness win over a home uplink; set a sane fixed resolution on the headless Pi so the remote window fits your laptop; and prefer wired Ethernet on the Pi, which removes a whole class of "why is this laggy" questions before you ask them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two operational choices keep it maintainable. Keep the VNC forward in the &lt;em&gt;same&lt;/em&gt; systemd unit as SSH — one process, one thing to monitor, one place to look when something is wrong, and removing VNC is deleting one flag and reloading. And understand the reconnection behavior: because VNC rides the same managed tunnel, a network drop restarts both, and the viewer simply reconnects once the forward is back, usually within the ten-second restart window. A frozen screen after a Wi-Fi blip is not a failure; it comes back on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;The immediate generalization is that &lt;em&gt;any&lt;/em&gt; service on the Pi can ride this same connection — a metrics endpoint, a database, an internal dashboard — each one a &lt;code&gt;-R&lt;/code&gt; forward on the tunnel you already trust, none of them exposed at home. Once you have internalized "one outbound connection, many services multiplexed back," the pattern scales from a single Pi to a small fleet without ever revisiting the router.&lt;/p&gt;

&lt;p&gt;The larger direction of travel is that this hand-built setup is a concrete instance of where secure remote access is heading industry-wide: identity- and key-gated access to individual services over an outbound-initiated, encrypted transport, rather than exposed inbound ports on a trusted network. That is the same premise underneath zero-trust access and the managed desktop-brokering services — the difference is only who owns the hops. Building it yourself, one &lt;code&gt;-R&lt;/code&gt; flag at a time, is the best way to understand what those products are actually doing, and to keep the option of owning every hop when the traffic is your own desktop. The tooling will keep getting easier; the principle of multiplexing an owned, outbound tunnel is the part worth keeping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources &amp;amp; further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://datatracker.ietf.org/doc/html/rfc6143" rel="noopener noreferrer"&gt;RFC 6143 — The Remote Framebuffer Protocol&lt;/a&gt; — VNC's own spec, on why its auth is weak and should be tunneled.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.raspberrypi.com/documentation/computers/remote-access.html" rel="noopener noreferrer"&gt;Raspberry Pi — Remote access documentation&lt;/a&gt; — enabling the built-in VNC server.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man.openbsd.org/ssh" rel="noopener noreferrer"&gt;OpenSSH &lt;code&gt;ssh(1)&lt;/code&gt; man page&lt;/a&gt; and &lt;a href="https://man.openbsd.org/sshd_config" rel="noopener noreferrer"&gt;&lt;code&gt;sshd_config(5)&lt;/code&gt; man page&lt;/a&gt; — the &lt;code&gt;-R&lt;/code&gt; forward and &lt;code&gt;GatewayPorts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tigervnc.org/" rel="noopener noreferrer"&gt;TigerVNC&lt;/a&gt; — an open-source VNC server/viewer that speaks the same protocol without a subscription.&lt;/li&gt;
&lt;li&gt;A longer reference treatment of &lt;a href="https://dorokhovich.com/blog/rpi/vnc-ec2-setup?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=success-story&amp;amp;utm_content=rpi/vnc-ec2-setup" rel="noopener noreferrer"&gt;VNC over a reverse SSH tunnel&lt;/a&gt; — the port map and AWS security-group walkthrough behind this framing.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hardware</category>
      <category>linux</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A Reverse SSH Tunnel for a Raspberry Pi: The Principle of Dialing Out Instead of Being Reached</title>
      <dc:creator>Mikhail Dorokhovich</dc:creator>
      <pubDate>Mon, 10 Aug 2026 08:12:29 +0000</pubDate>
      <link>https://dev.to/mikhail_dorokhovich_0c532/a-reverse-ssh-tunnel-for-a-raspberry-pi-the-principle-of-dialing-out-instead-of-being-reached-13p0</link>
      <guid>https://dev.to/mikhail_dorokhovich_0c532/a-reverse-ssh-tunnel-for-a-raspberry-pi-the-principle-of-dialing-out-instead-of-being-reached-13p0</guid>
      <description>&lt;h2&gt;
  
  
  The problem in context
&lt;/h2&gt;

&lt;p&gt;Consider a common homelab bind: a Raspberry Pi at home records a camera feed, runs a couple of cron jobs, and hosts a small dashboard. On the LAN it is fine. Then you travel, open a laptop, type the familiar &lt;code&gt;ssh pi@home&lt;/code&gt;, and get a timeout. The instinct is to log into the router and forward port 22 — except there is no public IP to forward &lt;em&gt;to&lt;/em&gt;. The ISP has quietly moved the connection behind CGNAT (carrier-grade NAT), the &lt;a href="https://datatracker.ietf.org/doc/html/rfc6598" rel="noopener noreferrer"&gt;shared address space reserved in RFC 6598&lt;/a&gt;, which means the "public" IP is shared with hundreds of subscribers and you control none of it.&lt;/p&gt;

&lt;p&gt;That is the real problem, and it is worth naming precisely because it explains why the usual fixes all fail: &lt;strong&gt;port forwarding is meaningless when the port lives on someone else's equipment.&lt;/strong&gt; DDNS points at an address you cannot open, UPnP does nothing, and VPN appliances want a static IP you do not have. Every one of those approaches assumes you can accept an inbound connection at home. Behind CGNAT, you cannot — and no amount of router configuration changes that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The principle
&lt;/h2&gt;

&lt;p&gt;The principle here is that &lt;strong&gt;NAT blocks inbound connections but happily allows outbound ones, so you stop waiting for the world to knock and have the Pi knock on a server you own — then keep that door propped open.&lt;/strong&gt; This is a reverse SSH tunnel, and it is the cleanest escape hatch behind CGNAT, a locked-down office network, or any router you would rather not touch.&lt;/p&gt;

&lt;p&gt;Concretely: rent a tiny always-on box in the cloud with a real public IP, have the Pi open an SSH connection &lt;em&gt;out&lt;/em&gt; to it, and use SSH remote port forwarding (&lt;code&gt;-R&lt;/code&gt;) so a port on the cloud box tunnels straight back through that connection to port 22 on the Pi. You connect to the cloud box, hop through the tunnel, and you are on the Pi — no router config, no exposed ports at home. The architecture is three parts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your laptop (anywhere)  --&amp;gt;  EC2 bridge (public IP, :2222)  --&amp;gt;  reverse tunnel  --&amp;gt;  Raspberry Pi (:22, behind CGNAT)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EC2 bridge:&lt;/strong&gt; Ubuntu on a &lt;code&gt;t2.micro&lt;/code&gt; (free-tier eligible); its only job is to be reachable and forward port 2222 back down the tunnel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raspberry Pi:&lt;/strong&gt; initiates and holds the tunnel open, restarting automatically if the network hiccups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tunnel:&lt;/strong&gt; an SSH remote port forward (&lt;code&gt;-R 2222:localhost:22&lt;/code&gt;) wrapped in a systemd service so it survives reboots and drops.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;-R&lt;/code&gt; flag is the whole trick — the &lt;a href="https://man.openbsd.org/ssh" rel="noopener noreferrer"&gt;ssh(1) man page&lt;/a&gt; defines it as forwarding connections to a given TCP port on the remote host back to the local side. There is &lt;a href="https://dorokhovich.com/blog/rpi/reverse-ssh?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=success-story&amp;amp;utm_content=rpi/reverse-ssh" rel="noopener noreferrer"&gt;a detailed treatment of the full reverse-SSH build&lt;/a&gt; with every security-group rule and connection method; the mechanics compress to a handful of config lines below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;p&gt;A reverse SSH tunnel is not the only way to reach a machine behind NAT, and it is worth being honest about where it wins and loses against the managed options:&lt;/p&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 costs&lt;/th&gt;
&lt;th&gt;What it buys&lt;/th&gt;
&lt;th&gt;Best when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reverse SSH tunnel&lt;/td&gt;
&lt;td&gt;You run and own a cloud bridge&lt;/td&gt;
&lt;td&gt;Depends on nothing but SSH + systemd; every hop is yours; no third-party control plane&lt;/td&gt;
&lt;td&gt;You want to own every hop and understand exactly what carries your packets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/" rel="noopener noreferrer"&gt;Cloudflare Tunnel&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Traffic rides a third-party control plane&lt;/td&gt;
&lt;td&gt;Outbound-only &lt;code&gt;cloudflared&lt;/code&gt;; firewall can block all inbound; least setup&lt;/td&gt;
&lt;td&gt;You want the shortest path and don't want to run infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://tailscale.com/blog/how-nat-traversal-works" rel="noopener noreferrer"&gt;Tailscale&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;A coordination service brokers peers&lt;/td&gt;
&lt;td&gt;Clever STUN/ICE NAT traversal builds direct peer-to-peer links&lt;/td&gt;
&lt;td&gt;You want a mesh across many devices with minimal fuss&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three are good; the choice is about what you are optimizing for. Cloudflare Tunnel is the easier button. The reverse SSH tunnel wins on ownership: it depends on nothing but tools already on the Pi, it does not route traffic through anyone else's control plane, and the cloud bridge is a box you can rebuild in minutes. The cost you accept in exchange is running that bridge and securing an internet-facing port yourself — which is a real cost, and the section below is mostly about paying it correctly.&lt;/p&gt;

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

&lt;p&gt;Build it in layers, proving each one before wrapping it in automation.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prepare the bridge.&lt;/strong&gt; Launch a &lt;code&gt;t2.micro&lt;/code&gt;, open port 22 to your own IP and 2222 to &lt;code&gt;0.0.0.0/0&lt;/code&gt;, and set three lines in &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;GatewayPorts&lt;/span&gt; &lt;span class="no"&gt;yes&lt;/span&gt;
&lt;span class="k"&gt;ClientAliveInterval&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
&lt;span class="k"&gt;ClientAliveCountMax&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GatewayPorts yes&lt;/code&gt; is the line most people miss. As the &lt;a href="https://man.openbsd.org/sshd_config" rel="noopener noreferrer"&gt;sshd_config(5) man page&lt;/a&gt; notes, sshd binds remote forwardings to loopback by default, so without it port 2222 stays invisible from the internet — the tunnel looks healthy but is unreachable from your laptop. The keepalives let the server reap dead tunnels instead of leaving zombie forwards holding the port.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a dedicated, unprivileged &lt;code&gt;tunnel&lt;/code&gt; user&lt;/strong&gt; on the bridge whose only reason to exist is holding this forward. Least privilege: if it is ever compromised, the blast radius is one useless shell.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prove a manual tunnel&lt;/strong&gt; from the Pi with a fresh ED25519 key whose public half is in the &lt;code&gt;tunnel&lt;/code&gt; user's &lt;code&gt;authorized_keys&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/id_tunnel &lt;span class="nt"&gt;-fN&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 2222:localhost:22 tunnel@YOUR-EC2-IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the bridge, &lt;code&gt;sudo ss -tulpn | grep 2222&lt;/code&gt; should show it LISTENing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Make it permanent with systemd&lt;/strong&gt;, because a manual tunnel dies the moment the Pi reboots or Wi-Fi blinks:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="nt"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;Reverse SSH Tunnel to EC2
&lt;span class="nt"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;network-online.target
&lt;span class="nt"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;network-online.target

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="nt"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;simple
&lt;span class="nt"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;pi
&lt;span class="nt"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/usr/bin/ssh -i /home/pi/.ssh/id_tunnel -o ServerAliveInterval=60 -o ExitOnForwardFailure=yes -o StrictHostKeyChecking=no -N -R 2222:localhost:22 tunnel@YOUR-EC2-IP
&lt;span class="nt"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;always
&lt;span class="nt"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;10

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="nt"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each option earns its place: &lt;code&gt;ExitOnForwardFailure=yes&lt;/code&gt; kills the process if the forward cannot bind, so systemd retries a &lt;em&gt;clean&lt;/em&gt; tunnel; &lt;code&gt;Restart=always&lt;/code&gt; — which the &lt;a href="https://man7.org/linux/man-pages/man5/systemd.service.5.html" rel="noopener noreferrer"&gt;systemd.service(5) man page&lt;/a&gt; defines as restarting regardless of exit status — with &lt;code&gt;RestartSec=10&lt;/code&gt; recovers within ten seconds; and &lt;code&gt;ServerAliveInterval=60&lt;/code&gt; stops idle NAT timeouts from silently severing the link. &lt;code&gt;StrictHostKeyChecking=no&lt;/code&gt; is required because a headless unit has no human to accept a fingerprint prompt.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Make it pleasant with a two-hop &lt;code&gt;ProxyCommand&lt;/code&gt;&lt;/strong&gt; in your laptop's &lt;code&gt;~/.ssh/config&lt;/code&gt; so &lt;code&gt;ssh rpi&lt;/code&gt; transparently connects through the bridge and onto the Pi:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; rpi
  &lt;span class="k"&gt;HostName&lt;/span&gt; localhost
  &lt;span class="k"&gt;Port&lt;/span&gt; &lt;span class="m"&gt;2222&lt;/span&gt;
  &lt;span class="k"&gt;User&lt;/span&gt; pi
  &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/id_rpi
  &lt;span class="k"&gt;ProxyCommand&lt;/span&gt; ssh -i ~/.ssh/rpi-tunnel-key.pem -W localhost:2222 ubuntu@YOUR-EC2-IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The non-negotiable part is the security posture. Opening 2222 to the world is reasonable &lt;em&gt;only&lt;/em&gt; because it is key-gated: disable password auth on the Pi, add fail2ban on the bridge, and treat that as the same-day work, not a follow-up. The gotchas that cost real time all trace back to the config above — a forgotten &lt;code&gt;GatewayPorts yes&lt;/code&gt; (tunnel connects but stays localhost-bound), a missing &lt;code&gt;StrictHostKeyChecking=no&lt;/code&gt; (headless service hangs on the fingerprint prompt), and zombie forwards after a hard drop (cleared by the server keepalives plus &lt;code&gt;ExitOnForwardFailure&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Two operating habits keep this honest. Verify monthly — &lt;code&gt;systemctl is-active&lt;/code&gt; on the Pi, &lt;code&gt;ss -tulpn | grep 2222&lt;/code&gt; on the bridge, and &lt;code&gt;ssh rpi 'uptime'&lt;/code&gt; end to end — because a tunnel you never check fails silently the day you need it, and &lt;code&gt;journalctl -u&lt;/code&gt; shows the reason when it does. And treat the bridge as cattle, not a pet: its entire config is a few &lt;code&gt;sshd_config&lt;/code&gt; lines and one user, so rebuilding takes minutes, and nothing irreplaceable lives on it, which means nothing irreplaceable is exposed on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;The immediate extension is that the same tunnel carries more than SSH: add a second &lt;code&gt;-R 5901:localhost:5900&lt;/code&gt; forward and the headless Pi becomes a full remote desktop over VNC, reusing everything above. Once you have internalized "the device dials out and multiplexes services back," the pattern generalizes to any port you want to reach — a dashboard, a database, a metrics endpoint — without ever exposing them at home.&lt;/p&gt;

&lt;p&gt;The larger direction of travel is that this is a hand-rolled instance of a principle the whole industry has converged on: &lt;strong&gt;outbound-only connectivity as the default security posture.&lt;/strong&gt; Cloudflare Tunnel, Tailscale, and the broader zero-trust and SASE movement all rest on the same idea that a device should reach out to a control plane rather than expose inbound ports, and that identity and keys, not network location, gate access. Building the reverse tunnel by hand is the best way to understand what those managed services are actually doing for you — and to decide, per deployment, whether owning every hop is worth running the bridge yourself. The mental model is the durable part; the tooling that implements it will keep getting easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources &amp;amp; further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://man.openbsd.org/ssh" rel="noopener noreferrer"&gt;OpenSSH &lt;code&gt;ssh(1)&lt;/code&gt; man page&lt;/a&gt; — the &lt;code&gt;-R&lt;/code&gt; remote port forwarding flag.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man.openbsd.org/sshd_config" rel="noopener noreferrer"&gt;OpenSSH &lt;code&gt;sshd_config(5)&lt;/code&gt; man page&lt;/a&gt; — the &lt;code&gt;GatewayPorts&lt;/code&gt; option.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man7.org/linux/man-pages/man5/systemd.service.5.html" rel="noopener noreferrer"&gt;&lt;code&gt;systemd.service(5)&lt;/code&gt; man page&lt;/a&gt; — &lt;code&gt;Restart=&lt;/code&gt; and &lt;code&gt;RestartSec=&lt;/code&gt; semantics.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datatracker.ietf.org/doc/html/rfc6598" rel="noopener noreferrer"&gt;RFC 6598 — IANA-Reserved IPv4 Prefix for Shared Address Space&lt;/a&gt; — the standard behind CGNAT.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/" rel="noopener noreferrer"&gt;Cloudflare Tunnel docs&lt;/a&gt; and &lt;a href="https://tailscale.com/blog/how-nat-traversal-works" rel="noopener noreferrer"&gt;Tailscale's "How NAT traversal works"&lt;/a&gt; — the managed alternatives.&lt;/li&gt;
&lt;li&gt;A longer reference treatment of &lt;a href="https://dorokhovich.com/blog/rpi/reverse-ssh?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=success-story&amp;amp;utm_content=rpi/reverse-ssh" rel="noopener noreferrer"&gt;this reverse SSH tunnel build&lt;/a&gt; — the security-group rules, port reference, and troubleshooting matrix behind this framing.&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Release Orchestration for Microservices: The Principle of the Control Tower, Not the Light Switch</title>
      <dc:creator>Mikhail Dorokhovich</dc:creator>
      <pubDate>Sun, 09 Aug 2026 08:50:42 +0000</pubDate>
      <link>https://dev.to/mikhail_dorokhovich_0c532/release-orchestration-for-microservices-the-principle-of-the-control-tower-not-the-light-switch-b5f</link>
      <guid>https://dev.to/mikhail_dorokhovich_0c532/release-orchestration-for-microservices-the-principle-of-the-control-tower-not-the-light-switch-b5f</guid>
      <description>&lt;h2&gt;
  
  
  The problem in context
&lt;/h2&gt;

&lt;p&gt;Picture the release a lot of microservices shops actually run. An engineer merges to main, CI builds the images, someone clicks deploy, and thirty seconds later two dozen services are rolling simultaneously with no coordination between them. Most nights it works. The night it doesn't, a service ships a change its neighbor isn't ready for, the neighbor starts throwing 500s, the service &lt;em&gt;behind&lt;/em&gt; it times out waiting, and within four minutes the checkout path is down. Users no longer forgive minute-long outages, and this kind lasts a lot longer than a minute.&lt;/p&gt;

&lt;p&gt;The post-mortem is always the same shape: no staged rollout, no automated health gate, no clean way to undo, and zero visibility for anyone outside the on-call channel. That is the core problem &lt;strong&gt;release orchestration&lt;/strong&gt; addresses: a fleet of 60 interdependent services being shipped with the coordination model of a single monolith. The reflex is to blame the change, or the engineer, or CI. But CI did its job — it built and delivered the code. What was missing was the layer that decides &lt;em&gt;whether the whole system is ready for that code to go live&lt;/em&gt;, and in what order.&lt;/p&gt;

&lt;h2&gt;
  
  
  The principle
&lt;/h2&gt;

&lt;p&gt;The principle here is that &lt;strong&gt;a release is air-traffic control, not a light switch.&lt;/strong&gt; Planes can technically take off on their own; without a control tower you get chaos in the sky. Each microservice &lt;em&gt;can&lt;/em&gt; update itself — but without a control layer deciding order, timing, and approval, one backward-incompatible change becomes a company-wide outage.&lt;/p&gt;

&lt;p&gt;That analogy is also the cleanest way to see the difference between CI/CD and orchestration. CI/CD is the assembly line that builds and delivers code. Orchestration is the control tower deciding &lt;em&gt;which&lt;/em&gt; system updates first, &lt;em&gt;how&lt;/em&gt; changes synchronize, and &lt;em&gt;who&lt;/em&gt; signs off. Most teams that suffer domino outages have a working assembly line and no tower. There is &lt;a href="https://dorokhovich.com/blog/release-orchestration/overview?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=success-story&amp;amp;utm_content=release-orchestration/overview" rel="noopener noreferrer"&gt;a detailed treatment of release orchestration for microservices&lt;/a&gt; that walks the full model; the compressed principle is that orchestration is a control layer over the pipeline, not a fancier pipeline.&lt;/p&gt;

&lt;p&gt;The control tower is built from three techniques, best adopted in order of pain relieved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Feature flags&lt;/strong&gt; decouple deploy from release. Pete Hodgson's &lt;a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer"&gt;feature toggles guide&lt;/a&gt; names this a &lt;em&gt;release toggle&lt;/em&gt;: ship the code, keep the behavior dark. A risky change sits dormant in production, gets flipped on for internal users then everyone, and flips &lt;em&gt;off&lt;/em&gt; in seconds if something smells wrong — no redeploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canary releases&lt;/strong&gt; cap blast radius. Instead of 100% at once, a new version goes to a slice of traffic — what Danilo Sato's &lt;a href="https://martinfowler.com/bliki/CanaryRelease.html" rel="noopener noreferrer"&gt;canary release definition&lt;/a&gt; frames as rolling out to a small subset before the whole fleet. On Kubernetes with Argo Rollouts, the config is almost embarrassingly small, mirroring the &lt;code&gt;setWeight&lt;/code&gt;/&lt;code&gt;pause&lt;/code&gt; model in the &lt;a href="https://argo-rollouts.readthedocs.io/en/stable/features/canary/" rel="noopener noreferrer"&gt;Argo Rollouts canary reference&lt;/a&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Rollout&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;example-app&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;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;canary&lt;/span&gt;&lt;span class="pi"&gt;:&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;setWeight&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pause&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;10m&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;setWeight&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pause&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;10m&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;setWeight&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten percent, pause and watch, fifty, pause and watch, then full. The &lt;code&gt;pause&lt;/code&gt; windows are where automated health checks live — if error rate or latency crosses a threshold during a pause, the rollout aborts and holds at the last safe weight instead of marching to 100%.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blue-green&lt;/strong&gt; handles the changes you can't canary — schema-coupled services, mostly — by running two identical environments, the pattern Martin Fowler &lt;a href="https://martinfowler.com/bliki/BlueGreenDeployment.html" rel="noopener noreferrer"&gt;described in 2010&lt;/a&gt;. Users stay on blue while green bakes; when green is verified, traffic flips instantly and blue stays warm as a fallback.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;p&gt;None of the three is universal; each buys a different guarantee at a different cost. The honest comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technique&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Rollback&lt;/th&gt;
&lt;th&gt;The cost to respect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Feature flags&lt;/td&gt;
&lt;td&gt;Decoupling deploy from release&lt;/td&gt;
&lt;td&gt;Flip off in seconds, no redeploy&lt;/td&gt;
&lt;td&gt;Stale flags become hidden branches — debt with a fuse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canary&lt;/td&gt;
&lt;td&gt;Everyday releases where partial traffic makes sense&lt;/td&gt;
&lt;td&gt;Auto-abort at last safe weight&lt;/td&gt;
&lt;td&gt;Only as smart as the metrics behind the pause steps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blue-green&lt;/td&gt;
&lt;td&gt;Schema-coupled changes you can't slice&lt;/td&gt;
&lt;td&gt;Instant flip back to blue&lt;/td&gt;
&lt;td&gt;Roughly double resources; DB/session state is hard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two constraints do the most damage when ignored. A canary is only as smart as its metrics: if the &lt;code&gt;pause&lt;/code&gt; steps aren't backed by real health signals — error rate, latency, saturation — you have added slow-motion to a bad deploy, so wire the checks before you trust the automation. And config is production code: the worst incidents tend to come from YAML and flag flips, not application code, so deployment config needs the same review gate. Tooling choice is a smaller trade-off than teams expect — &lt;a href="https://argo-rollouts.readthedocs.io/en/stable/features/canary/" rel="noopener noreferrer"&gt;Argo Rollouts&lt;/a&gt; if you are already all-in on Kubernetes, &lt;a href="https://docs.flagger.app/" rel="noopener noreferrer"&gt;Flagger&lt;/a&gt; for a lighter automatic metric-driven loop, Spinnaker for multi-cloud sprawl. The real cost is not tools; it is the team time to design the process.&lt;/p&gt;

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

&lt;p&gt;Do not boil the ocean. The staged path that works is three steps.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assess.&lt;/strong&gt; Answer honestly: how long does a release take, how many manual steps, how often do you roll back, does the business have any visibility? Writing those answers down is uncomfortable and clarifying, and it gives you the baseline to prove improvement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start small — process before tech.&lt;/strong&gt; Introduce release checklists, code review on &lt;em&gt;config&lt;/em&gt; changes, and a written rollback plan for every release; then flags for new functionality, monitoring on key metrics, and automated health checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale up.&lt;/strong&gt; Only then template the canary config across services.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Budget for the culture lag: the tools install in a day, but getting everyone to write a rollback plan per release takes a quarter. It is worth it. And when you need to justify the work, frame it in leadership's units, not yours — nobody buys "canary rollouts," they buy fewer outages and faster shipping. Calculate the cost of one hour of downtime, multiply by historical incident frequency, and set that against the near-zero cost of free tooling; the rollback plan alone tends to pay for the initiative on paper. The counterintuitive headline is that decoupling deploy from release and capping blast radius does not slow teams down — it lets them ship &lt;em&gt;more&lt;/em&gt;, because each ship is cheap to undo. Competitors shipping 3–5x more often aren't smarter; they have just made each release cheap to reverse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;The direction of travel is toward the control tower making more of its own decisions. AIOps — machine learning that predicts release risk and flags suspicious metrics automatically — is the natural next layer, feeding richer signals into the same pause-and-abort gates that a human tunes today. But it is an accelerant on top of good orchestration, not a substitute for it: you cannot ML your way out of not having a control tower in the first place.&lt;/p&gt;

&lt;p&gt;The deeper forward-looking point is that every one of these techniques produces the structured signal that smarter automation will need — labeled rollouts, health-gated pauses, explicit rollback plans, config under review. Teams that build the tower now are not just avoiding tonight's domino outage; they are assembling the legible, well-instrumented substrate that AI-assisted release agents will reason over next. The control layer is the thing that stays valuable as the intelligence sitting on top of it improves — which is exactly why it is worth building before the automation arrives, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources &amp;amp; further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Danilo Sato, &lt;a href="https://martinfowler.com/bliki/CanaryRelease.html" rel="noopener noreferrer"&gt;Canary Release&lt;/a&gt; — martinfowler.com.&lt;/li&gt;
&lt;li&gt;Martin Fowler, &lt;a href="https://martinfowler.com/bliki/BlueGreenDeployment.html" rel="noopener noreferrer"&gt;Blue Green Deployment&lt;/a&gt; — martinfowler.com.&lt;/li&gt;
&lt;li&gt;Pete Hodgson, &lt;a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer"&gt;Feature Toggles (aka Feature Flags)&lt;/a&gt; — martinfowler.com.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://argo-rollouts.readthedocs.io/en/stable/features/canary/" rel="noopener noreferrer"&gt;Argo Rollouts — Canary Deployment Strategy&lt;/a&gt; — official docs.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.flagger.app/" rel="noopener noreferrer"&gt;Flagger — progressive delivery for Kubernetes&lt;/a&gt; — official docs.&lt;/li&gt;
&lt;li&gt;A longer reference treatment of &lt;a href="https://dorokhovich.com/blog/release-orchestration/overview?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=success-story&amp;amp;utm_content=release-orchestration/overview" rel="noopener noreferrer"&gt;release orchestration for microservices&lt;/a&gt; — the tool comparison and the assess/start-small/scale roadmap behind this framing.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>deployment</category>
      <category>devops</category>
      <category>microservices</category>
    </item>
    <item>
      <title>Local Kubernetes Dev — Part 6: Containerizing your service — writing a Dockerfile</title>
      <dc:creator>Mikhail Dorokhovich</dc:creator>
      <pubDate>Sun, 09 Aug 2026 08:50:40 +0000</pubDate>
      <link>https://dev.to/mikhail_dorokhovich_0c532/local-kubernetes-dev-part-6-containerizing-your-service-writing-a-dockerfile-2k9p</link>
      <guid>https://dev.to/mikhail_dorokhovich_0c532/local-kubernetes-dev-part-6-containerizing-your-service-writing-a-dockerfile-2k9p</guid>
      <description>&lt;p&gt;Two mistakes beginners make in Dockerfiles constantly — and both have real consequences: running as root (a security hole) and the wrong layer order (a tax on build speed).&lt;/p&gt;

&lt;p&gt;Part six of the series is a production-ready Dockerfile for the FastAPI service myapp. Not "your first Dockerfile," but a breakdown of what separates a solid image from a bloated, insecure one. Inside: why &lt;code&gt;COPY requirements.txt&lt;/code&gt; BEFORE &lt;code&gt;COPY ./app&lt;/code&gt; — and pip install gets cached instead of reinstalling everything on every code change; a multi-stage build that carries only the finished venv into the final image, without compilers and pip caches; base image choice (slim vs alpine with its musl and compile-from-source vs distroless with no shell); and the security backbone of the chapter — an unprivileged user (&lt;code&gt;adduser --uid 10001&lt;/code&gt; + &lt;code&gt;USER appuser&lt;/code&gt;) plus &lt;code&gt;runAsNonRoot&lt;/code&gt; in the manifest.&lt;/p&gt;

&lt;p&gt;Plus the important details that break prod silently: the exec form of CMD (otherwise SIGTERM never arrives and graceful shutdown breaks), &lt;code&gt;fastapi run&lt;/code&gt; instead of bare uvicorn, &lt;code&gt;PYTHONUNBUFFERED=1&lt;/code&gt; (otherwise you see no logs in kubectl logs), HEALTHCHECK via Python (there's no curl in slim).&lt;/p&gt;

&lt;p&gt;And the finale — the main trap: &lt;code&gt;docker build&lt;/code&gt; does NOT make the image visible to k3d. Two ways to deliver it: &lt;code&gt;k3d image import&lt;/code&gt; and the built-in registry. &lt;a href="https://dorokhovich.com/blog/local-k8s-containerizing-your-service?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=local-k8s-containerizing-your-service" rel="noopener noreferrer"&gt;https://dorokhovich.com/blog/local-k8s-containerizing-your-service?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=local-k8s-containerizing-your-service&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>python</category>
      <category>security</category>
    </item>
    <item>
      <title>A GitOps Adoption Roadmap: The Principle of Walking the Arc Instead of Leaping It</title>
      <dc:creator>Mikhail Dorokhovich</dc:creator>
      <pubDate>Sat, 08 Aug 2026 17:16:57 +0000</pubDate>
      <link>https://dev.to/mikhail_dorokhovich_0c532/a-gitops-adoption-roadmap-the-principle-of-walking-the-arc-instead-of-leaping-it-2408</link>
      <guid>https://dev.to/mikhail_dorokhovich_0c532/a-gitops-adoption-roadmap-the-principle-of-walking-the-arc-instead-of-leaping-it-2408</guid>
      <description>&lt;h2&gt;
  
  
  The problem in context
&lt;/h2&gt;

&lt;p&gt;Anyone in the industry long enough remembers the sleepless release night: the coffee, the collective prayer that a deploy would go smoothly, the one guru who alone understood the process, no documentation, and rollbacks so frequent they stopped feeling like exceptions. The canonical horror story — a telecom billing release that ran 14 hours, involved twelve people, crashed three times, and needed another six hours to recover — is only an exaggeration by degree. Plenty of teams still gather on Friday evenings for six-to-twelve-hour manual releases.&lt;/p&gt;

&lt;p&gt;The reflex, once the pain is acute enough, is to buy the shiniest platform and leap straight to it. That reflex is exactly what makes transformations stall. The problem is not that a team lacks GitOps; it is that manual-deploy shops try to adopt GitOps as a single jump, skipping the rungs — script automation, CI/CD, declarative delivery — that GitOps quietly assumes already exist. Framed that way, escaping 3 AM deploys stops being a unique curse and becomes a solved problem with a well-worn path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The principle
&lt;/h2&gt;

&lt;p&gt;The principle here is that &lt;strong&gt;a GitOps adoption roadmap is a layered arc, not a purchase: each layer assumes the one below it, so you walk scripts → CI/CD → GitOps → progressive delivery rather than leaping.&lt;/strong&gt; The industry traveled this arc for a reason, and repeating its order is a good sign you are not skipping steps.&lt;/p&gt;

&lt;p&gt;The mental model is four phases, each earning the next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Assess.&lt;/strong&gt; Measure before you automate. A blunt maturity checklist, answered honestly, tells you where the pain is &lt;em&gt;and&lt;/em&gt; gives you the baseline you will later use to prove progress:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Maturity assessment checklist&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; Deployment &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;target: &amp;lt; 30 minutes&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; Release frequency &lt;span class="o"&gt;(&lt;/span&gt;target: weekly or more often&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; Manual steps count &lt;span class="o"&gt;(&lt;/span&gt;target: 0&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; Mean &lt;span class="nb"&gt;time &lt;/span&gt;to restore &lt;span class="o"&gt;(&lt;/span&gt;target: &amp;lt; 1 hour&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; Change success rate &lt;span class="o"&gt;(&lt;/span&gt;target: &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 95%&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; Automated &lt;span class="nb"&gt;test &lt;/span&gt;coverage &lt;span class="o"&gt;(&lt;/span&gt;target: &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 80%&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; Monitoring and alerting &lt;span class="k"&gt;in &lt;/span&gt;place
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; Rollback and disaster recovery procedures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those targets line up with the throughput and stability signals &lt;a href="https://dora.dev/guides/dora-metrics/" rel="noopener noreferrer"&gt;DORA formalizes as its core delivery metrics&lt;/a&gt;. Assess organizational readiness too — executive sponsorship, willingness to change, DevOps expertise, budget — because skipping any of those is how transformations stall.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quick wins.&lt;/strong&gt; Chase visible pain reduction: standardize release checklists and runbooks, containerize apps, put infrastructure in code, stand up basic CI, add health checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale.&lt;/strong&gt; Make Git the single source of truth with &lt;a href="https://opengitops.dev/" rel="noopener noreferrer"&gt;GitOps&lt;/a&gt; and a reconciling controller like &lt;a href="https://argo-cd.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;Argo CD&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimize.&lt;/strong&gt; Layer on progressive delivery — feature flags, canary, observability-driven automatic rollback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is &lt;a href="https://dorokhovich.com/blog/release-orchestration/learning-path?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=success-story&amp;amp;utm_content=release-orchestration/learning-path" rel="noopener noreferrer"&gt;a detailed treatment of the full manual-to-GitOps learning path&lt;/a&gt; with the tools comparison and timeline behind each phase; the compressed principle is that quick wins are how you &lt;em&gt;earn permission&lt;/em&gt; for the disruptive changes. Shave one service's deploy from hours to under an hour with a basic pipeline and health checks, and the skeptics stop arguing whether automation is worth it — the proof is in front of them. Momentum is a currency, and quick wins are how you mint it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;p&gt;The tooling arc recapitulates the industry's evolution, and each rung trades setup effort for tighter integration and stronger guarantees. The honest comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;What it buys&lt;/th&gt;
&lt;th&gt;The cost / when it fits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Script automation (&lt;a href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html" rel="noopener noreferrer"&gt;Ansible&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Turns tribal knowledge into documentation-as-code; agentless, readable playbooks&lt;/td&gt;
&lt;td&gt;Still push-based and imperative; fine as the first rung, not the destination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD (&lt;a href="https://docs.github.com/en/actions/about-github-actions/understanding-github-actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Build/test/deploy folded into version control; low barrier&lt;/td&gt;
&lt;td&gt;Some setup effort; pipeline logic can sprawl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitOps (Argo CD)&lt;/td&gt;
&lt;td&gt;Git as source of truth; versioned, reviewable, self-healing, no drift&lt;/td&gt;
&lt;td&gt;Requires declarative discipline and Kubernetes maturity underneath&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Progressive delivery (&lt;a href="https://argo-rollouts.readthedocs.io/en/stable/features/canary/" rel="noopener noreferrer"&gt;Argo Rollouts&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Analysis-gated canaries; automatic rollback on metric breach&lt;/td&gt;
&lt;td&gt;Needs trustworthy metrics and the three rungs below it already solid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trade-off that matters most is &lt;em&gt;ordering discipline&lt;/em&gt;: each rung assumes the one below. GitOps on top of a cluster nobody can describe declaratively just relocates the chaos into YAML; progressive delivery without trustworthy metrics automates a decision you cannot yet make. A representative Ansible playbook and an Argo CD Application make the two ends concrete:&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 web application&lt;/span&gt;
  &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;webservers&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
  &lt;span class="na"&gt;tasks&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;Update application files&lt;/span&gt;
      &lt;span class="na"&gt;copy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/builds/myapp-v2.0/&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/opt/myapp/&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;Check application health&lt;/span&gt;
      &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;//localhost&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;8080/health&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;status_code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;200&lt;/span&gt; &lt;span class="pi"&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 yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;myapp&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;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/company/myapp-config&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;k8s&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;automated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;prune&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;selfHeal&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;The deeper trade-off is where you spend effort. The tempting failure modes are all one-dimensional: changing everything at once (chaos and resistance), fixing only tooling while ignoring process and culture (minimal impact), and treating security or metrics as an afterthought. The costliest of these is cultural — the technology is the easy part; getting people to trust automation over heroics, and to run blameless post-mortems instead of assigning blame, is the real work.&lt;/p&gt;

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

&lt;p&gt;Start with the audit, not the tooling, and walk the arc one measured phase at a time.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run the maturity audit and baseline the metrics.&lt;/strong&gt; You cannot prove an improvement you never measured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find the single biggest pain point and fix it with a quick win.&lt;/strong&gt; Prove it with the metric you baselined. Bank the visible, uncontroversial success before touching anything invasive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Introduce GitOps once CI and containers are stable.&lt;/strong&gt; With Argo CD, every change becomes a reviewable pull request, every state is versioned and instantly revertible, and the cluster stops drifting from what Git says — the four OpenGitOps properties (declarative, versioned, pulled, continuously reconciled) doing the work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add progressive delivery last.&lt;/strong&gt; Encode a canary that pauses, runs an automated analysis against an error-rate query, and advances only if the metric stays healthy. That is the moment releases become genuinely low-drama: the system, not a stressed human at 3 AM, decides whether to proceed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invest in the team as much as the tools.&lt;/strong&gt; Training reliably returns more than buying yet another platform, and it is what converts a blame-and-burnout culture into a blameless, continuous-learning one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Perfect is the enemy of progress — a small improvement today beats the perfect plan still being refined next quarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;The direction of travel is toward &lt;strong&gt;fully closed-loop delivery&lt;/strong&gt;: Git as the declared intent, controllers reconciling reality to it, and progressive-delivery analysis promoting or reverting releases without a human in the path. Once the arc is walked and the metrics are trustworthy, the natural next step is handing more of the promote/rollback judgement to the system — and, increasingly, to AI-assisted operations that reason over the same signals to forecast capacity, flag anomalies on release, and draft the changes that reconcile drift.&lt;/p&gt;

&lt;p&gt;None of that can be bolted onto Friday-night deploys later; it compounds on the layers beneath it. The teams walking the roadmap now are not just escaping the sleepless nights — they are building the legible, declarative, well-instrumented substrate that the next generation of autonomous delivery tooling will need in order to be trusted at all. That is the real reason to walk the arc rather than leap it: each rung you lay down is what makes the next one, human or machine, safe to stand on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources &amp;amp; further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html" rel="noopener noreferrer"&gt;Ansible playbooks&lt;/a&gt; and &lt;a href="https://docs.github.com/en/actions/about-github-actions/understanding-github-actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt; — the script-automation and CI/CD rungs of the arc.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://argo-cd.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;Argo CD docs&lt;/a&gt; and &lt;a href="https://opengitops.dev/" rel="noopener noreferrer"&gt;OpenGitOps principles&lt;/a&gt; — declarative, Git-as-source-of-truth delivery.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://argo-rollouts.readthedocs.io/en/stable/features/canary/" rel="noopener noreferrer"&gt;Argo Rollouts — Canary strategy&lt;/a&gt; — automated, analysis-gated progressive delivery.&lt;/li&gt;
&lt;li&gt;DORA, &lt;a href="https://dora.dev/guides/dora-metrics/" rel="noopener noreferrer"&gt;DORA metrics&lt;/a&gt; — the baseline metrics that prove each phase actually helped.&lt;/li&gt;
&lt;li&gt;A longer reference treatment of &lt;a href="https://dorokhovich.com/blog/release-orchestration/learning-path?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=success-story&amp;amp;utm_content=release-orchestration/learning-path" rel="noopener noreferrer"&gt;the full manual-to-GitOps learning path&lt;/a&gt; — the four-phase roadmap, tools comparison, and timeline/budget detail.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>automation</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>Trunk-Based Development vs GitFlow: The Principle Is to Match Your Branches to Your Cadence</title>
      <dc:creator>Mikhail Dorokhovich</dc:creator>
      <pubDate>Fri, 07 Aug 2026 08:53:57 +0000</pubDate>
      <link>https://dev.to/mikhail_dorokhovich_0c532/trunk-based-development-vs-gitflow-the-principle-is-to-match-your-branches-to-your-cadence-4ihi</link>
      <guid>https://dev.to/mikhail_dorokhovich_0c532/trunk-based-development-vs-gitflow-the-principle-is-to-match-your-branches-to-your-cadence-4ihi</guid>
      <description>&lt;h2&gt;
  
  
  The problem in context
&lt;/h2&gt;

&lt;p&gt;A lot of repositories are museums of good intentions. Feature branches live for weeks, sometimes months, drifting further from the mainline every day until merging one means a full day of conflict archaeology. &lt;code&gt;main&lt;/code&gt; has no protection, so a direct push can — and eventually does — destabilize a release. Hotfixes go out and never get back-merged, so the same bug reappears two releases later. And because unrelated features get mixed into a single release branch, no release is ever truly reproducible.&lt;/p&gt;

&lt;p&gt;The reflex is to blame Git, or to reach for whatever branching model a louder team swears by. But the underlying issue in the perennial &lt;strong&gt;trunk-based development vs GitFlow&lt;/strong&gt; debate is subtler: a branch in Git is just a lightweight pointer to a commit, so creating, merging, and deleting branches is cheap — which means long-lived branches are a &lt;em&gt;choice&lt;/em&gt;, not a necessity. The teams drowning in conflicts are almost always shipping often while branching as if they shipped quarterly. That mismatch, not the tool and not the model, is the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The principle
&lt;/h2&gt;

&lt;p&gt;The principle here is that &lt;strong&gt;branch lifetime should track shipping frequency: the more often you ship, the shorter your branches must live and the stricter your CI gates into the mainline must be.&lt;/strong&gt; Pick the model to match the cadence, rather than inheriting one by accident.&lt;/p&gt;

&lt;p&gt;That reframe dissolves the "which model is correct" argument, because branching models have a clear arc and each fits a different cadence. From 2008–2012 it was &lt;a href="https://nvie.com/posts/a-successful-git-branching-model/" rel="noopener noreferrer"&gt;GitFlow&lt;/a&gt; with many long-lived branches; 2013–2018 simplified to GitHub Flow; from 2018 onward the industry moved to &lt;a href="https://trunkbaseddevelopment.com/" rel="noopener noreferrer"&gt;trunk-based development&lt;/a&gt; with short branches and feature flags. GitFlow is not wrong — its own author now notes it suits versioned software with multiple supported releases rather than continuously deployed web apps. If releases are infrequent and you run many parallel supported versions, its explicit release and hotfix branches are genuinely clearer. If you ship frequently, the answer is trunk-based plus feature flags plus strict CI. There is &lt;a href="https://dorokhovich.com/blog/release-orchestration/git-branching?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=success-story&amp;amp;utm_content=release-orchestration/git-branching" rel="noopener noreferrer"&gt;a detailed treatment of choosing and running a branching model&lt;/a&gt; that walks the full decision; the compressed rule is: cadence dictates model.&lt;/p&gt;

&lt;p&gt;The second, deeper principle is to know what branches are actually &lt;em&gt;for&lt;/em&gt;, because every merge-hell symptom is one of their benefits inverted. Branches buy exactly four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Risk isolation&lt;/strong&gt; — unfinished work does not break the stable line. (Long-lived branches invert this by drifting.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallelism&lt;/strong&gt; — features, releases, and hotfixes proceed at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality control&lt;/strong&gt; — PRs, mandatory reviews, green-CI gates. (An unprotected &lt;code&gt;main&lt;/code&gt; inverts this.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traceability&lt;/strong&gt; — commits, tags, and release notes document exactly what shipped. (Mixed release branches invert this.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reframing the goal as "preserve those four properties" is what makes the specific rules feel principled rather than arbitrary. It also fixes the vocabulary: &lt;code&gt;main&lt;/code&gt; is the always-deployable, protected production source of truth; &lt;code&gt;develop&lt;/code&gt; is an optional integration buffer for sprint cadences; &lt;code&gt;feature/*&lt;/code&gt; is short-lived and one-PR-per-goal; &lt;code&gt;release/*&lt;/code&gt; is stabilization only (fixes and version/CHANGELOG bumps, never new features); &lt;code&gt;hotfix/*&lt;/code&gt; branches off &lt;code&gt;main&lt;/code&gt; and integrates &lt;em&gt;back&lt;/em&gt; into both &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;develop&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;p&gt;The choice is not binary good-vs-bad; it is a fit question. The honest comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;GitFlow (long-lived branches)&lt;/th&gt;
&lt;th&gt;Trunk-based (short branches + flags)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Best-fit cadence&lt;/td&gt;
&lt;td&gt;Infrequent, versioned releases; multiple supported versions&lt;/td&gt;
&lt;td&gt;Frequent / continuous deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge cost&lt;/td&gt;
&lt;td&gt;Grows with branch age — conflict archaeology&lt;/td&gt;
&lt;td&gt;Small and frequent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release reproducibility&lt;/td&gt;
&lt;td&gt;Clear via explicit release branches&lt;/td&gt;
&lt;td&gt;Clear via trunk + tags + flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incomplete work&lt;/td&gt;
&lt;td&gt;Hidden in a long branch&lt;/td&gt;
&lt;td&gt;Merged behind a feature flag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI strictness required&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;High — the mainline gate is load-bearing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main failure mode&lt;/td&gt;
&lt;td&gt;Parallel branches drift out of sync&lt;/td&gt;
&lt;td&gt;Undisciplined flags accumulate as debt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trade-off worth stating plainly: trunk-based buys small merges and fast flow, but only if you pay for it with strict CI and &lt;a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer"&gt;feature-flag discipline&lt;/a&gt; — flags are what make merging incomplete work safe, and a flag with no owner or expiry is future debt. GitFlow buys clean parallel-version management, but the bill arrives as merge cost that scales with branch age. Choosing trunk-based while keeping weeks-long branches gives you the costs of both and the benefits of neither.&lt;/p&gt;

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

&lt;p&gt;Do not adopt a whole new model overnight — that is the failure mode, and it invites the resistance you fear. Sequence by leverage.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Protect &lt;code&gt;main&lt;/code&gt; first.&lt;/strong&gt; Require reviews and green CI; forbid direct pushes. This is the single highest-payoff change: the moment direct pushes are impossible, "unstable release" incidents essentially stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shorten feature branches.&lt;/strong&gt; Aim for hours to a couple of days. Add a lightweight branch-age report to CI that flags any feature branch older than a few days, so drift becomes visible &lt;em&gt;before&lt;/em&gt; it becomes painful. Making the invisible cost visible changes behaviour more than any policy memo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adopt one release lifecycle and walk every release through it.&lt;/strong&gt; Preparation branches off a clean mainline:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch develop &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git pull &lt;span class="nt"&gt;--ff-only&lt;/span&gt;
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; release/1.8.0
git commit &lt;span class="nt"&gt;-am&lt;/span&gt; &lt;span class="s2"&gt;"chore(release): bump to 1.8.0 &amp;amp; update CHANGELOG"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The release merges into &lt;code&gt;main&lt;/code&gt; with a no-fast-forward merge and an annotated tag, then — the step teams skip — back-integrates into &lt;code&gt;develop&lt;/code&gt; so nothing is lost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch main &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git pull &lt;span class="nt"&gt;--ff-only&lt;/span&gt;
git merge &lt;span class="nt"&gt;--no-ff&lt;/span&gt; release/1.8.0 &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"release: 1.8.0"&lt;/span&gt;
git tag &lt;span class="nt"&gt;-a&lt;/span&gt; v1.8.0 &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Release 1.8.0"&lt;/span&gt;
git push origin main &lt;span class="nt"&gt;--tags&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hotfixes follow the same discipline in miniature: branch off &lt;code&gt;main&lt;/code&gt;, fix, merge back into &lt;em&gt;both&lt;/em&gt; &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;develop&lt;/code&gt;. Skipping that final back-merge is the exact bug that resurrects old defects. This release/hotfix structure is the part worth keeping from &lt;a href="https://nvie.com/posts/a-successful-git-branching-model/" rel="noopener noreferrer"&gt;Vincent Driessen's original model&lt;/a&gt; even as you shorten everything else toward trunk.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Make the good path the quick path.&lt;/strong&gt; Standardize a handful of commands and aliases so the right thing is also the easy thing:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch &lt;span class="nt"&gt;-c&lt;/span&gt; feature/login
git fetch &lt;span class="nt"&gt;--prune&lt;/span&gt;
git pull &lt;span class="nt"&gt;--ff-only&lt;/span&gt;
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; release/2.0.0 develop
git switch main &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git merge &lt;span class="nt"&gt;--no-ff&lt;/span&gt; release/2.0.0 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git tag &lt;span class="nt"&gt;-a&lt;/span&gt; v2.0.0 &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Release 2.0.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drill one rule hardest: never &lt;code&gt;rebase&lt;/code&gt; a published branch — rewriting shared history breaks teammates; for public branches prefer &lt;code&gt;merge --no-ff&lt;/code&gt;. Pair that with &lt;a href="https://www.conventionalcommits.org/en/v1.0.0/" rel="noopener noreferrer"&gt;Conventional Commits&lt;/a&gt; and auto-generated changelogs and semantic releases come almost for free.&lt;/p&gt;

&lt;p&gt;The cultural surprise is that resistance rarely materializes once aliases and protected &lt;code&gt;main&lt;/code&gt; are in place. Nobody enjoys conflict archaeology, so a workflow that quietly removes it sells itself after the first clean release. You do not mandate discipline so much as remove the friction that was rewarding the bad habits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;The direction of travel is trunk-based plus feature flags, &lt;strong&gt;branch protection expressed as policy-as-code&lt;/strong&gt;, and automated releases that compress the cycle to hours. Flags are the piece that makes short branches safe — you merge incomplete work behind a flag instead of hiding it in a long-lived branch — and combined with canary or blue-green delivery, branching stops being a source of risk and becomes bookkeeping.&lt;/p&gt;

&lt;p&gt;The further horizon is that as more of the commit-to-release path gets handled by automation and AI-assisted tooling — bots that open, review, and land small changes; agents that assemble release notes from conventional commits; policy engines that gate merges on live quality signals — the value of a short, linear, well-tagged history compounds. Automated systems reason far more reliably over a clean trunk than over a thicket of drifting branches. The teams that match their branching to their cadence today are the ones whose history will be legible enough for the next generation of tooling to safely act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources &amp;amp; further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Vincent Driessen, &lt;a href="https://nvie.com/posts/a-successful-git-branching-model/" rel="noopener noreferrer"&gt;A successful Git branching model&lt;/a&gt; — the original GitFlow, plus the author's later note on when it no longer fits.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://trunkbaseddevelopment.com/" rel="noopener noreferrer"&gt;Trunk-Based Development&lt;/a&gt; — the short-lived-branch model and why it scales with shipping frequency.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.conventionalcommits.org/en/v1.0.0/" rel="noopener noreferrer"&gt;Conventional Commits&lt;/a&gt; — the commit convention that powers auto-generated changelogs and semantic releases.&lt;/li&gt;
&lt;li&gt;Pete Hodgson, &lt;a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer"&gt;Feature Toggles&lt;/a&gt; — how flags make short branches and trunk-based work safe.&lt;/li&gt;
&lt;li&gt;A longer reference treatment of &lt;a href="https://dorokhovich.com/blog/release-orchestration/git-branching?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=success-story&amp;amp;utm_content=release-orchestration/git-branching" rel="noopener noreferrer"&gt;choosing and running a Git branching model&lt;/a&gt; — every command and the GitFlow-vs-trunk decision for a given cadence.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>git</category>
      <category>softwaredevelopment</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>The Release Automation Business Case: The Principle for Getting Delivery Work Funded</title>
      <dc:creator>Mikhail Dorokhovich</dc:creator>
      <pubDate>Thu, 06 Aug 2026 09:29:18 +0000</pubDate>
      <link>https://dev.to/mikhail_dorokhovich_0c532/the-release-automation-business-case-the-principle-for-getting-delivery-work-funded-30la</link>
      <guid>https://dev.to/mikhail_dorokhovich_0c532/the-release-automation-business-case-the-principle-for-getting-delivery-work-funded-30la</guid>
      <description>&lt;h2&gt;
  
  
  The problem in context
&lt;/h2&gt;

&lt;p&gt;Engineering asks for investment in delivery automation and gets a polite no — repeatedly, and usually for the same reason. The pitch walks into the room talking about GitOps, pipelines, and Kubernetes, and watches the executives' eyes glaze over. It is answering a question nobody in that room asked. To leadership, release management reads as an IT cost center, a necessary evil, not a lever on the business. So the failed &lt;strong&gt;release automation business case&lt;/strong&gt; is almost never a failure of the underlying work; it is a failure of translation.&lt;/p&gt;

&lt;p&gt;This matters because the gap is asymmetric. The engineers know the automation is valuable and cannot understand the no; the executives are not being obtuse — they simply have no line of sight from "pipeline" to anything on their own scorecard. Until someone builds that line of sight, the money stays where the budget-holder can already see the return.&lt;/p&gt;

&lt;h2&gt;
  
  
  The principle
&lt;/h2&gt;

&lt;p&gt;The principle here is that &lt;strong&gt;a delivery investment has to be argued in the units the audience already uses to rank the world, and for a budget-holder those units are revenue, risk, and operating cost — in that order.&lt;/strong&gt; Modern release orchestration is not a technical nicety; the speed and reliability of shipping software directly determine a company's ability to capture markets and retain customers. That is not a slogan — it is the central finding of &lt;a href="https://dora.dev/research/" rel="noopener noreferrer"&gt;DORA's multi-year research program&lt;/a&gt;, which has repeatedly shown that software delivery performance predicts &lt;em&gt;organizational&lt;/em&gt; performance. That sentence is something a CFO can act on. "We need GitOps" is not.&lt;/p&gt;

&lt;p&gt;The mental model that makes the translation mechanical is that every delivery metric is a business outcome wearing an engineering costume. The signals &lt;a href="https://dora.dev/guides/dora-metrics/" rel="noopener noreferrer"&gt;DORA tracks as the Four Keys&lt;/a&gt;, which Google Cloud packaged into the &lt;a href="https://cloud.google.com/blog/products/devops-sre/using-the-four-keys-to-measure-your-devops-performance" rel="noopener noreferrer"&gt;open-source Four Keys project&lt;/a&gt;, restate cleanly into money and risk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lead time → revenue.&lt;/strong&gt; Getting products in front of customers sooner; the headline framing is up to a +23% revenue effect from faster delivery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change failure rate → risk.&lt;/strong&gt; Dramatically fewer operational incidents — on the order of an 85% reduction — which is avoided downtime cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time to restore → risk and cost.&lt;/strong&gt; Recovery in minutes not days; and automation-driven utilization cuts infrastructure OpEx by roughly 40%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lead the conversation with &lt;em&gt;their&lt;/em&gt; ranking — revenue first, risk second, cost third — because that is the order the person holding the budget already thinks in. There is &lt;a href="https://dorokhovich.com/blog/release-orchestration/evolution?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=success-story&amp;amp;utm_content=release-orchestration/evolution" rel="noopener noreferrer"&gt;a detailed treatment of the evolution and business case for release automation&lt;/a&gt; with the full executive summary and metrics table; the compressed principle is: translate before you present, and order by their priorities, not yours.&lt;/p&gt;

&lt;p&gt;The single most effective artifact is a plain table mapping each metric from current state to target to business effect — abstractions do not get funded, concrete deltas do:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Business effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Release frequency&lt;/td&gt;
&lt;td&gt;Quarterly&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;Ship value continuously&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change lead time&lt;/td&gt;
&lt;td&gt;2–6 months&lt;/td&gt;
&lt;td&gt;Under 1 day&lt;/td&gt;
&lt;td&gt;~98% faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to restore&lt;/td&gt;
&lt;td&gt;1–7 days&lt;/td&gt;
&lt;td&gt;Under 1 hour&lt;/td&gt;
&lt;td&gt;~95% faster recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed releases&lt;/td&gt;
&lt;td&gt;15–30%&lt;/td&gt;
&lt;td&gt;0–5%&lt;/td&gt;
&lt;td&gt;~83% fewer bad releases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each row is a business outcome in disguise. "Lead time from months to a day" is really "we can respond to a competitor or a regulation in a day." Pair it with one real company's shape — a fintech with $50B in assets that cut time-to-market from 8 months to 6 weeks and booked +$15M in first-year revenue — so the percentages have a concrete silhouette behind them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;p&gt;The business case is itself a design with trade-offs, and getting them wrong is how a strong underlying investment still gets a no:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Choice in the pitch&lt;/th&gt;
&lt;th&gt;Weaker option&lt;/th&gt;
&lt;th&gt;Stronger option&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;Framing&lt;/td&gt;
&lt;td&gt;Technology capabilities&lt;/td&gt;
&lt;td&gt;Business outcomes&lt;/td&gt;
&lt;td&gt;The budget-holder scores outcomes, not capabilities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evidence&lt;/td&gt;
&lt;td&gt;Architecture diagrams&lt;/td&gt;
&lt;td&gt;Before/after deltas + one real case&lt;/td&gt;
&lt;td&gt;Concrete deltas get funded; abstractions do not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Is this a fad?" objection&lt;/td&gt;
&lt;td&gt;Left unanswered&lt;/td&gt;
&lt;td&gt;A 25-year industry arc&lt;/td&gt;
&lt;td&gt;Makes the direction feel inevitable, not speculative&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payback&lt;/td&gt;
&lt;td&gt;Unmentioned&lt;/td&gt;
&lt;td&gt;12–24 month window, quantified&lt;/td&gt;
&lt;td&gt;Reframes "can we afford it?" as "can we afford not to?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollout risk&lt;/td&gt;
&lt;td&gt;Big-bang transformation&lt;/td&gt;
&lt;td&gt;Instrumented pilot on one service&lt;/td&gt;
&lt;td&gt;A measurable small win is an easy yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest cost of doing this well is that translation takes real work and some numbers are necessarily estimates — overclaim and you lose credibility on the first missed target. So the discipline is to bring ranges, name the assumptions, and let one real case carry the specificity the estimates cannot.&lt;/p&gt;

&lt;p&gt;The "is this a fad?" objection deserves its own move, because it sits under every budget no. Defuse it with the arc the industry has already traveled: the heroic-scripts era of 2000–2005 (6–12 hour releases, 30–50% rollbacks, downtime at $100K–$1M+ per hour), the first automation wave of 2005–2012, the CI/CD revolution of 2012–2020 — the inflection &lt;a href="https://martinfowler.com/bliki/ContinuousDelivery.html" rel="noopener noreferrer"&gt;Martin Fowler frames as Continuous Delivery&lt;/a&gt;, keeping software deployable at any time — and the 2020s move to GitOps, IaC, and business-metric-driven rollout. Presented this way, the ask stops sounding like a gamble on new technology and starts sounding like catching up to where elite teams already operate. No leadership team wants to be running 2005-style releases in 2026.&lt;/p&gt;

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

&lt;p&gt;If you are stuck getting funding for delivery automation, stop refining the architecture slide and build the case in this order.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reorder by their priorities.&lt;/strong&gt; Open with revenue and risk, close with cost. The same content in the wrong order still reads as an IT expense.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the before/after table&lt;/strong&gt; with your real current-state numbers — you need the baseline anyway to prove the improvement later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bring one real company's numbers&lt;/strong&gt; so the abstractions have a shape, and present the 25-year arc so the direction feels inevitable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anchor to a payback window.&lt;/strong&gt; Be honest that this is an ongoing capability, not a one-time purchase, and set the expectation at 12–24 months depending on scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Propose an instrumented pilot on a single non-critical service&lt;/strong&gt;, measured with the same three metrics, before the full rollout. A funded pilot with a measurable result is a far easier decision than a big-bang transformation — and the delta it produces makes the second, larger ask almost automatic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice the throughline: the technology was never the hard part. The hard part is translating lead time, change failure rate, and MTTR into revenue, risk, and OpEx, and ordering them the way the budget-holder ranks the world.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;The most durable version of this argument positions the automation as a &lt;strong&gt;foundation rather than a feature.&lt;/strong&gt; The same pipeline discipline that speeds releases today is the substrate that AI-assisted operations, predictive capacity forecasting, and zero-touch pipelines will run on tomorrow — none of which can be bolted onto a manual release process later. That reframes the question one final time, from "can we afford this project?" to "can we afford to still be building on 2015 foundations when our competitors are running on 2025 ones?"&lt;/p&gt;

&lt;p&gt;The forward-looking case is that delivery capability is compounding, not linear: each layer of automation lowers the cost of the next, and the teams funding it now are buying the option to adopt whatever the next wave of AI-driven operations turns out to require. The business case you make for release automation today is, increasingly, the business case for being able to adopt anything at all at the speed the market will demand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources &amp;amp; further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dora.dev/research/" rel="noopener noreferrer"&gt;DORA research program&lt;/a&gt; — the longest-running study linking software delivery performance to organizational and business outcomes.&lt;/li&gt;
&lt;li&gt;DORA, &lt;a href="https://dora.dev/guides/dora-metrics/" rel="noopener noreferrer"&gt;DORA metrics (the Four Keys)&lt;/a&gt; and Google Cloud, &lt;a href="https://cloud.google.com/blog/products/devops-sre/using-the-four-keys-to-measure-your-devops-performance" rel="noopener noreferrer"&gt;Using the Four Keys to measure your DevOps performance&lt;/a&gt; — the throughput and stability signals behind the ROI story.&lt;/li&gt;
&lt;li&gt;Martin Fowler, &lt;a href="https://martinfowler.com/bliki/ContinuousDelivery.html" rel="noopener noreferrer"&gt;Continuous Delivery&lt;/a&gt; — the capability the investment actually buys.&lt;/li&gt;
&lt;li&gt;A longer reference treatment of &lt;a href="https://dorokhovich.com/blog/release-orchestration/evolution?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=success-story&amp;amp;utm_content=release-orchestration/evolution" rel="noopener noreferrer"&gt;the evolution and business case for release automation&lt;/a&gt; — the executive summary, metrics table, and ROI framing behind this argument.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>leadership</category>
      <category>management</category>
    </item>
    <item>
      <title>Blue-Green vs Canary Deployment: The Principle Is to Stop Choosing and Start Combining</title>
      <dc:creator>Mikhail Dorokhovich</dc:creator>
      <pubDate>Wed, 05 Aug 2026 10:05:14 +0000</pubDate>
      <link>https://dev.to/mikhail_dorokhovich_0c532/blue-green-vs-canary-deployment-the-principle-is-to-stop-choosing-and-start-combining-2ph1</link>
      <guid>https://dev.to/mikhail_dorokhovich_0c532/blue-green-vs-canary-deployment-the-principle-is-to-stop-choosing-and-start-combining-2ph1</guid>
      <description>&lt;h2&gt;
  
  
  The problem in context
&lt;/h2&gt;

&lt;p&gt;The releases that hurt most are the ones where a team has to &lt;em&gt;decide, live,&lt;/em&gt; whether things are bad enough to roll back. Error rate looks a little high — is that the new version or normal noise? Nobody agreed on a threshold in advance, so the argument happens in the incident channel while users suffer. That single failure mode — rollback as a live debate — is what a deployment strategy exists to prevent, and it is why the perennial "blue-green vs canary deployment" argument is usually the wrong frame.&lt;/p&gt;

&lt;p&gt;Treated as a ranking problem, it produces a stalemate: blue-green people cite instant rollback, canary people cite limited blast radius, and the team picks one and inherits the other's weaknesses. The reframe is that these are not rivals to rank. They are tools that answer three different questions — &lt;em&gt;how do we roll out, how do we roll back, how do we limit blast radius&lt;/em&gt; — and a real delivery system needs a different answer at different blast radii.&lt;/p&gt;

&lt;h2&gt;
  
  
  The principle
&lt;/h2&gt;

&lt;p&gt;The principle here is that &lt;strong&gt;deployment strategy is blast-radius management, and different releases have different blast radii.&lt;/strong&gt; Once you accept that, the design stops being "pick the one right strategy" and becomes "assemble a layered system where each tool covers the radius it is best at." A combination almost always wins: trunk-based development plus feature flags plus canary for small daily releases, and blue-green for the big drops where instant, explainable rollback is the whole point. &lt;a href="https://dorokhovich.com/blog/release-orchestration/deployment-strategies?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=success-story&amp;amp;utm_content=release-orchestration/deployment-strategies" rel="noopener noreferrer"&gt;A detailed treatment of choosing and combining strategies&lt;/a&gt; walks the full model; the compressed version is four tools with four jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blue-green&lt;/strong&gt; keeps two identical production environments — the pattern &lt;a href="https://martinfowler.com/bliki/BlueGreenDeployment.html" rel="noopener noreferrer"&gt;Martin Fowler documented in 2010&lt;/a&gt;. Blue serves users; green is prepared calmly for the next release; when you are confident you flip traffic in one router change, and if something is wrong you flip back just as fast. Its virtue is that rollback is instant and trivial to explain to a business stakeholder — a fintech spotting a EUR-payments bug and flipping back in about two minutes, with a tiny fraction of operations affected, is the kind of story that sells it to leadership.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Canary&lt;/strong&gt; rolls a change out to a small subset first — what &lt;a href="https://martinfowler.com/bliki/CanaryRelease.html" rel="noopener noreferrer"&gt;Danilo Sato describes on Fowler's site&lt;/a&gt; — starting at 1–5% of traffic, watching errors, latency, resource use, and one business metric, then ramping 5 → 10 → 25 → 50 → 100% and comparing at each step. Its virtue is a naturally small blast radius: a bad version is seen by few before the system reacts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rolling updates&lt;/strong&gt; are the quiet workhorse for stateless services — the &lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/" rel="noopener noreferrer"&gt;default strategy in the Kubernetes docs&lt;/a&gt;, where &lt;code&gt;maxSurge&lt;/code&gt; and &lt;code&gt;maxUnavailable&lt;/code&gt; govern how aggressively pods are replaced while the service stays available throughout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feature flags&lt;/strong&gt; are the one that changes cadence most, because they separate two actions people wrongly treat as one: &lt;em&gt;deploying code&lt;/em&gt; and &lt;em&gt;releasing a feature&lt;/em&gt;. Ship dark, enable for staff, then a percentage, then everyone, and kill the switch without redeploying — the technique &lt;a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer"&gt;Pete Hodgson documents as feature toggles&lt;/a&gt;, which pairs naturally with &lt;a href="https://trunkbaseddevelopment.com/" rel="noopener noreferrer"&gt;trunk-based development&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The unifying move that ends the 2 AM arguments is writing &lt;strong&gt;stop-criteria before the release&lt;/strong&gt;, as executable rules rather than live judgement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IF error_rate_canary &amp;gt; error_rate_baseline * 1.5 THEN rollback
IF latency_p99_canary &amp;gt; latency_p99_baseline * 1.3 THEN rollback
IF conversion_rate_canary &amp;lt; baseline * 0.95 THEN rollback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With those in place, rollback stops being a debate and becomes a reflex — the system pulls the new version before a human opens the dashboard. Netflix's canonical case is exactly this: a gradual ramp where SmartTV performance degraded at one step and triggered an automatic rollback in roughly two minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;p&gt;No strategy is free, and the honest way to reason is per blast radius:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Rollback&lt;/th&gt;
&lt;th&gt;Real cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Blue-green&lt;/td&gt;
&lt;td&gt;Big, risky drops (payments rewrite, framework upgrade)&lt;/td&gt;
&lt;td&gt;Instant single flip&lt;/td&gt;
&lt;td&gt;~2× resources during release; DB and session state are hard; watch DNS TTL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canary&lt;/td&gt;
&lt;td&gt;Everyday releases&lt;/td&gt;
&lt;td&gt;Automatic on stop-criteria&lt;/td&gt;
&lt;td&gt;Needs percentage routing; slow to reach confidence on low traffic; pin users by ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rolling update&lt;/td&gt;
&lt;td&gt;Stateless services on Kubernetes&lt;/td&gt;
&lt;td&gt;Gradual, batch by batch&lt;/td&gt;
&lt;td&gt;Old and new pods coexist — API contracts must stay compatible across versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feature flags&lt;/td&gt;
&lt;td&gt;Decoupling deploy from release&lt;/td&gt;
&lt;td&gt;Kill switch, no redeploy&lt;/td&gt;
&lt;td&gt;Forgotten flags rot; every flag needs an owner and an expiry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two constraints deserve emphasis because they are where teams get burned. First, the rolling-update coexistence problem: because old and new pods serve traffic simultaneously, forward-compatible database and API changes are not optional — deployment strategy and schema evolution are two halves of one discipline. Second, a flag with no owner is technical debt with a fuse on it; the discipline that makes flags safe is an owner and an expiry date on every one, with dead code removed once the feature is universal.&lt;/p&gt;

&lt;p&gt;And one distinction worth nailing down, because conflating them wastes time: &lt;strong&gt;canary is not A/B testing.&lt;/strong&gt; Canary asks "is the new version not &lt;em&gt;worse&lt;/em&gt;?" and optimizes for safety. A/B testing asks "which version is &lt;em&gt;better&lt;/em&gt;?" and optimizes for a product decision — split traffic, pin each user to a variant, and wait a week or two to smooth out weekday and seasonality effects before running the stats. Use canary to protect releases and A/B tests to choose product directions; using one for the other's job produces noisy, untrustworthy conclusions.&lt;/p&gt;

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

&lt;p&gt;The mistake is adopting all four at once. Sequence by where the pain is loudest.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write the stop-criteria before the next release and make them executable.&lt;/strong&gt; This is the single highest-leverage move; it converts rollback from a live argument into an automatic reaction and costs almost nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add feature flags to decouple deploy from release.&lt;/strong&gt; Ship dark, enable gradually, keep a kill switch. Put an owner and expiry on every flag from day one so the debt never accumulates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make rolling updates safe on Kubernetes&lt;/strong&gt; with small batches, health probes, and a pause between batches:
&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;my-app&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;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&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;RollingUpdate&lt;/span&gt;
    &lt;span class="na"&gt;rollingUpdate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;maxUnavailable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;maxSurge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&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;containers&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;my-app&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;my-app:v2&lt;/span&gt;
        &lt;span class="na"&gt;readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/health/ready&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;8080&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/health&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;8080&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Batches too large increase risk and make rollback painful; skipping the pause means you notice problems too late.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reserve blue-green for the big drops&lt;/strong&gt; where instant, explainable rollback justifies double the resources. Solve the session problem with a shared session store and graceful connection draining, and watch DNS TTL, which can delay a switch you thought was instant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate the canary gates&lt;/strong&gt; rather than hand-rolling them. &lt;a href="https://argo-rollouts.readthedocs.io/en/stable/features/canary/" rel="noopener noreferrer"&gt;Argo Rollouts encodes exactly these gates&lt;/a&gt; — &lt;code&gt;setWeight&lt;/code&gt; and &lt;code&gt;pause&lt;/code&gt; steps plus automated analysis that advances the traffic weight only while metrics stay healthy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Put together, a normal day looks like this: a change merges to trunk, deploys dark behind a flag, and enables as a canary to 5% while the automated gates watch; if they stay green it ramps to 100% over an hour, and if not the flag flips off while the code stays deployed but inert. A large, risky drop reaches for blue-green instead. Each tool covers a different blast radius, and together they mean no release requires heroics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;The direction of travel is toward &lt;strong&gt;progressive delivery as a fully automated control loop&lt;/strong&gt; — the stop-criteria you write by hand today become the analysis templates your rollout controller evaluates on its own, promoting or reverting without a human in the path. Tools like Argo Rollouts and Flagger are early forms of this; the interesting frontier is richer signals feeding the gate, including model-based anomaly detection that can catch a regression no static threshold would.&lt;/p&gt;

&lt;p&gt;The deeper point is that all of this rests on the same foundation: a legible definition of "healthy" and forward-compatible changes underneath the traffic shifting. Teams that have captured what "not worse" means as executable criteria are the ones who will safely hand more of the promote/rollback decision to automation — and eventually to AI-assisted release agents that reason over the same signals. The layered strategy is not just what ends the 2 AM rollbacks today; it is the substrate the automated release systems of the next few years will need in order to be trusted at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources &amp;amp; further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Martin Fowler, &lt;a href="https://martinfowler.com/bliki/BlueGreenDeployment.html" rel="noopener noreferrer"&gt;BlueGreenDeployment&lt;/a&gt; and Danilo Sato, &lt;a href="https://martinfowler.com/bliki/CanaryRelease.html" rel="noopener noreferrer"&gt;CanaryRelease&lt;/a&gt; — the canonical write-ups of both patterns.&lt;/li&gt;
&lt;li&gt;Kubernetes docs, &lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/" rel="noopener noreferrer"&gt;Deployments&lt;/a&gt; — the RollingUpdate strategy with &lt;code&gt;maxSurge&lt;/code&gt;/&lt;code&gt;maxUnavailable&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://argo-rollouts.readthedocs.io/en/stable/features/canary/" rel="noopener noreferrer"&gt;Argo Rollouts — Canary strategy&lt;/a&gt; — automated canary analysis and traffic-weight gates.&lt;/li&gt;
&lt;li&gt;Pete Hodgson, &lt;a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer"&gt;Feature Toggles&lt;/a&gt; and &lt;a href="https://trunkbaseddevelopment.com/" rel="noopener noreferrer"&gt;Trunk-Based Development&lt;/a&gt; — decoupling deploy from release.&lt;/li&gt;
&lt;li&gt;A longer reference treatment of &lt;a href="https://dorokhovich.com/blog/release-orchestration/deployment-strategies?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=success-story&amp;amp;utm_content=release-orchestration/deployment-strategies" rel="noopener noreferrer"&gt;choosing and combining deployment strategies&lt;/a&gt; — the sequences and stop-criteria behind this framing.&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
  </channel>
</rss>
