<?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: Sri Balaji</title>
    <description>The latest articles on DEV Community by Sri Balaji (@sri2614).</description>
    <link>https://dev.to/sri2614</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%2F467906%2Fee67cb27-7e15-4293-b895-091047dc03fc.jpeg</url>
      <title>DEV Community: Sri Balaji</title>
      <link>https://dev.to/sri2614</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sri2614"/>
    <language>en</language>
    <item>
      <title>Observability: Metrics, Logs &amp; Traces (The Three Pillars)</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Wed, 12 Aug 2026 11:40:47 +0000</pubDate>
      <link>https://dev.to/sri2614/observability-metrics-logs-traces-the-three-pillars-2gh3</link>
      <guid>https://dev.to/sri2614/observability-metrics-logs-traces-the-three-pillars-2gh3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Walk away able to instrument a service the right way: wire up metrics, logs, and traces, watch the &lt;strong&gt;four golden signals&lt;/strong&gt;, and set SLIs and SLOs so you can answer not just whether a system broke but why.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The 3am question monitoring can't answer&lt;/li&gt;
&lt;li&gt;Monitoring vs observability, the one distinction that matters&lt;/li&gt;
&lt;li&gt;How telemetry actually flows&lt;/li&gt;
&lt;li&gt;The three pillars, side by side&lt;/li&gt;
&lt;li&gt;What to actually measure: the four golden signals&lt;/li&gt;
&lt;li&gt;SLI, SLO, SLA, measuring 'good enough' on purpose&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 3am question monitoring can't answer
&lt;/h2&gt;

&lt;p&gt;It's 3am. Your dashboard is red: error rate is up, latency is climbing. Monitoring did its job, it told you &lt;em&gt;something&lt;/em&gt; is wrong. Then comes the question monitoring cannot answer: &lt;strong&gt;why?&lt;/strong&gt; Which service? Which dependency? Which one customer's request set off the cascade? You start SSHing into boxes and grepping logs by hand, and the incident drags on for an hour that should have been five minutes.&lt;/p&gt;

&lt;p&gt;That gap, between &lt;em&gt;knowing something broke&lt;/em&gt; and &lt;em&gt;understanding why&lt;/em&gt;, is exactly the gap &lt;strong&gt;observability&lt;/strong&gt; fills. It's one of the clearest dividing lines between an engineer who can keep a system alive and one who can only watch it die. This article builds the mental model from zero: what observability actually means, the three pillars it rests on, what to measure, and how to set targets you can defend.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Anyone who has deployed something and then wondered what it's doing in production. No prior monitoring experience needed. We use vendor-neutral terms (the same ideas apply whether you run Prometheus + Grafana, Datadog, or the OpenTelemetry stack).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Monitoring vs observability, the one distinction that matters
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Monitoring answers questions you already knew to ask. Observability lets you ask new questions of your system without shipping new code to answer them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Monitoring is dashboards and alerts for &lt;strong&gt;known failure modes&lt;/strong&gt;: CPU over 80%, disk almost full, error rate above threshold. You decided in advance what to watch. That's necessary, but production fails in ways nobody predicted. Observability is the property that, when something &lt;em&gt;unexpected&lt;/em&gt; happens, you can explore the system's actual behaviour and figure it out from the data you're already emitting.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🚨 A check-engine light&lt;/td&gt;
&lt;td&gt;Monitoring (a known signal fired)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔧 A mechanic's full diagnostic port&lt;/td&gt;
&lt;td&gt;Observability (ask anything, after the fact)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📋 A fixed pre-flight checklist&lt;/td&gt;
&lt;td&gt;Alerts on known thresholds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🎥 The flight recorder (black box)&lt;/td&gt;
&lt;td&gt;Logs + traces you can replay&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Same car, two different relationships with it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The practical upshot: you build observability so that the &lt;em&gt;next&lt;/em&gt; incident, the one you can't imagine yet, is debuggable with data you're already collecting. You don't get a second chance to instrument an outage that already happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  How telemetry actually flows
&lt;/h2&gt;

&lt;p&gt;Before the three pillars, see the shape of the whole system. Your application emits three kinds of telemetry. They travel through a collector, land in backends suited to each data type, and surface as dashboards and alerts. Follow the flow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fYXBwKCJZb3VyIEFwcDxici8-aW5zdHJ1bWVudGVkIikKICBjbGFzcyBuX2FwcCBjb21wdXRlOwogIG5fY29sbGVjdG9yKCJDb2xsZWN0b3I8YnIvPk9UZWwgLyBhZ2VudCIpCiAgY2xhc3Mgbl9jb2xsZWN0b3IgcXVldWU7CiAgbl9tZXRyaWNzKCJNZXRyaWNzIFN0b3JlPGJyLz5Qcm9tZXRoZXVzIikKICBjbGFzcyBuX21ldHJpY3Mgb2JzZXJ2YWJpbGl0eTsKICBuX2xvZ3MoIkxvZyBTdG9yZTxici8-TG9raSAvIEVMSyIpCiAgY2xhc3Mgbl9sb2dzIG9ic2VydmFiaWxpdHk7CiAgbl90cmFjZXMoIlRyYWNlIFN0b3JlPGJyLz5UZW1wbyAvIEphZWdlciIpCiAgY2xhc3Mgbl90cmFjZXMgb2JzZXJ2YWJpbGl0eTsKICBuX2Rhc2goIkRhc2hib2FyZHM8YnIvPkdyYWZhbmEiKQogIGNsYXNzIG5fZGFzaCBvYnNlcnZhYmlsaXR5OwogIG5fYWxlcnQoIkFsZXJ0aW5nPGJyLz5wYWdlcyBhIGh1bWFuIikKICBjbGFzcyBuX2FsZXJ0IGV4dGVybmFsOwogIG5fYXBwIC0tPnwiZW1pdCJ8IG5fY29sbGVjdG9yCiAgbl9jb2xsZWN0b3IgLS0-fCJtZXRyaWNzInwgbl9tZXRyaWNzCiAgbl9jb2xsZWN0b3IgLS0-fCJsb2dzInwgbl9sb2dzCiAgbl9jb2xsZWN0b3IgLS0-fCJ0cmFjZXMifCBuX3RyYWNlcwogIG5fbWV0cmljcyAtLT4gbl9kYXNoCiAgbl9sb2dzIC0uLT4gbl9kYXNoCiAgbl90cmFjZXMgLS4tPiBuX2Rhc2gKICBuX21ldHJpY3MgLS0-fCJ0aHJlc2hvbGRzInwgbl9hbGVydA%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fYXBwKCJZb3VyIEFwcDxici8-aW5zdHJ1bWVudGVkIikKICBjbGFzcyBuX2FwcCBjb21wdXRlOwogIG5fY29sbGVjdG9yKCJDb2xsZWN0b3I8YnIvPk9UZWwgLyBhZ2VudCIpCiAgY2xhc3Mgbl9jb2xsZWN0b3IgcXVldWU7CiAgbl9tZXRyaWNzKCJNZXRyaWNzIFN0b3JlPGJyLz5Qcm9tZXRoZXVzIikKICBjbGFzcyBuX21ldHJpY3Mgb2JzZXJ2YWJpbGl0eTsKICBuX2xvZ3MoIkxvZyBTdG9yZTxici8-TG9raSAvIEVMSyIpCiAgY2xhc3Mgbl9sb2dzIG9ic2VydmFiaWxpdHk7CiAgbl90cmFjZXMoIlRyYWNlIFN0b3JlPGJyLz5UZW1wbyAvIEphZWdlciIpCiAgY2xhc3Mgbl90cmFjZXMgb2JzZXJ2YWJpbGl0eTsKICBuX2Rhc2goIkRhc2hib2FyZHM8YnIvPkdyYWZhbmEiKQogIGNsYXNzIG5fZGFzaCBvYnNlcnZhYmlsaXR5OwogIG5fYWxlcnQoIkFsZXJ0aW5nPGJyLz5wYWdlcyBhIGh1bWFuIikKICBjbGFzcyBuX2FsZXJ0IGV4dGVybmFsOwogIG5fYXBwIC0tPnwiZW1pdCJ8IG5fY29sbGVjdG9yCiAgbl9jb2xsZWN0b3IgLS0-fCJtZXRyaWNzInwgbl9tZXRyaWNzCiAgbl9jb2xsZWN0b3IgLS0-fCJsb2dzInwgbl9sb2dzCiAgbl9jb2xsZWN0b3IgLS0-fCJ0cmFjZXMifCBuX3RyYWNlcwogIG5fbWV0cmljcyAtLT4gbl9kYXNoCiAgbl9sb2dzIC0uLT4gbl9kYXNoCiAgbl90cmFjZXMgLS4tPiBuX2Rhc2gKICBuX21ldHJpY3MgLS0-fCJ0aHJlc2hvbGRzInwgbl9hbGVydA%3FbgColor%3D0d1017%26type%3Dpng" alt="Telemetry pipeline. The app emits metrics, logs, and traces. A collector (e.g. the OpenTelemetry Collector) fans them ou" width="843" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Telemetry pipeline. The app emits metrics, logs, and traces. A collector (e.g. the OpenTelemetry Collector) fans them out to purpose-built backends. Dashboards and alerting sit on top. Alerts page a human; dashboards are where that human investigates.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Notice the division of labour: &lt;strong&gt;metrics&lt;/strong&gt; drive alerts because they're cheap to aggregate, &lt;strong&gt;logs and traces&lt;/strong&gt; are where you investigate once an alert fires. A mature setup links them, an alert on a metric jumps you to the relevant traces, which link to the exact log lines. That stitched-together path is what turns a one-hour incident into a five-minute one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three pillars, side by side
&lt;/h2&gt;

&lt;p&gt;Metrics, logs, and traces are not competing choices, they answer different questions, and you need all three. The fastest way to internalise them is to ask what each one is &lt;em&gt;good at&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Answers&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Metrics&lt;/td&gt;
&lt;td&gt;Numbers aggregated over time&lt;/td&gt;
&lt;td&gt;Is something wrong, and how much?&lt;/td&gt;
&lt;td&gt;p99 latency = 1.4s; error rate = 3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs&lt;/td&gt;
&lt;td&gt;Timestamped event records&lt;/td&gt;
&lt;td&gt;What exactly happened in this event?&lt;/td&gt;
&lt;td&gt;"payment failed: card declined, user 8842"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traces&lt;/td&gt;
&lt;td&gt;One request's path across services&lt;/td&gt;
&lt;td&gt;Where in the request did time/errors go?&lt;/td&gt;
&lt;td&gt;checkout: gateway 12ms → auth 8ms → db 1300ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Each pillar answers a different question. Reach for the one that matches what you're trying to learn.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Metrics, cheap, aggregated, alert-friendly
&lt;/h3&gt;

&lt;p&gt;A metric is a number measured over time: request count, latency, CPU, queue depth. Because they're pre-aggregated, metrics are cheap to store and fast to query, which makes them ideal for &lt;strong&gt;dashboards and alerts&lt;/strong&gt;. Their weakness is that aggregation throws away detail, a metric tells you 3% of requests failed, never &lt;em&gt;which&lt;/em&gt; ones or &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logs, high detail, the per-event truth
&lt;/h3&gt;

&lt;p&gt;A log line is a record of one event. Logs carry the detail metrics lose. The single biggest upgrade you can make is &lt;strong&gt;structured logging&lt;/strong&gt;, emit JSON, not free text, so you can filter and aggregate. &lt;code&gt;{"level":"error","user":8842,"reason":"card_declined"}&lt;/code&gt; is queryable; &lt;code&gt;payment failed for user&lt;/code&gt; is a needle in a haystack.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;structured-log.json&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-05T03:11:42Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"checkout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"trace_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3d4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8842&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"payment_failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"card_declined"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"latency_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1340&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Traces, following one request through everything
&lt;/h3&gt;

&lt;p&gt;In a system with many services, one user action triggers a chain of internal calls. A &lt;strong&gt;trace&lt;/strong&gt; records that whole journey as a tree of &lt;strong&gt;spans&lt;/strong&gt;, each span is one operation, with timing. Traces are how you answer "the checkout is slow, &lt;em&gt;which&lt;/em&gt; of the eight services it touches is the culprit?" The &lt;code&gt;trace_id&lt;/code&gt; in that log line above is what ties the pillars together: from a slow trace you jump straight to its logs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; The cardinality trap: never put high-cardinality values (user IDs, request IDs, emails) in metric &lt;em&gt;labels&lt;/em&gt;, it explodes your metrics store and bankrupts you. Those belong in logs and traces. Metric labels should be low-cardinality: status code, endpoint, region. This single rule prevents most observability cost blowups.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What to actually measure: the four golden signals
&lt;/h2&gt;

&lt;p&gt;You could measure a thousand things. Google's SRE book distilled what matters for any user-facing service down to four. If you only instrument these, you'll catch the overwhelming majority of real problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt;, how long requests take. Track p50, p95, p99, and crucially split &lt;em&gt;successful&lt;/em&gt; from &lt;em&gt;failed&lt;/em&gt; requests (a fast failure can hide a slow success).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic&lt;/strong&gt;, how much demand the system is under: requests per second, transactions per minute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors&lt;/strong&gt;, the rate of failed requests. Include both explicit failures (HTTP 500s) and implicit ones (wrong answers, policy violations).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saturation&lt;/strong&gt;, how "full" the system is: CPU, memory, queue depth, connection pool usage. The leading indicator of impending failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;USE and RED, two cousins worth knowing:&lt;/strong&gt; For resources (CPUs, disks), the USE method tracks Utilisation, Saturation, and Errors. For request-driven services, the RED method tracks Rate, Errors, and Duration. Both are subsets of the golden signals, pick the framing that fits what you're measuring.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  SLI, SLO, SLA, measuring 'good enough' on purpose
&lt;/h2&gt;

&lt;p&gt;Metrics tell you what's happening. &lt;strong&gt;SLOs&lt;/strong&gt; tell you whether what's happening is acceptable, and they turn "is the site okay?" from a gut feeling into a number. The three terms get mixed up constantly, so here they are in plain language:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SLI&lt;/td&gt;
&lt;td&gt;A measured indicator of service health&lt;/td&gt;
&lt;td&gt;% of requests served in under 300ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLO&lt;/td&gt;
&lt;td&gt;Your internal target for that SLI&lt;/td&gt;
&lt;td&gt;99.9% of requests under 300ms, monthly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLA&lt;/td&gt;
&lt;td&gt;A contractual promise to customers&lt;/td&gt;
&lt;td&gt;99.9% uptime or you get a credit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;An SLI is the measurement, an SLO is your internal target, an SLA is the external promise (with consequences).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The genius idea that falls out of SLOs is the &lt;strong&gt;error budget&lt;/strong&gt;. A 99.9% monthly SLO means you're &lt;em&gt;allowed&lt;/em&gt; to be bad 0.1% of the time, roughly 43 minutes a month. That budget is a tool: if you have budget left, ship features fast. If you've burned it, freeze risky changes and spend on reliability. It ends the eternal feature-vs-stability argument with math instead of opinions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Don't aim for 100%:&lt;/strong&gt; Each extra '9' of availability costs exponentially more and buys diminishing value. 99.999% ("five nines") is ~5 minutes of downtime a &lt;em&gt;year&lt;/em&gt;, heroic and rarely worth it. Set the SLO at the level your users actually need, then stop. Chasing 100% is how teams burn out without users noticing the difference.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Alerting on causes instead of symptoms.&lt;/strong&gt; An alert on "CPU &amp;gt; 80%" pages you for something users may never feel. Alert on what users experience, error rate, latency SLO burn, and use causes for investigation, not paging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging unstructured text.&lt;/strong&gt; &lt;code&gt;log.info("user " + id + " failed")&lt;/code&gt; can't be filtered or aggregated. Emit structured JSON with consistent field names from day one; retrofitting it across a codebase is miserable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-cardinality metric labels.&lt;/strong&gt; Putting user IDs or request IDs in metric labels explodes storage and cost. Keep labels low-cardinality; push the detail to logs and traces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No trace IDs linking the pillars.&lt;/strong&gt; Without a shared &lt;code&gt;trace_id&lt;/code&gt; propagated across services and into logs, your three pillars are three islands and every investigation starts from scratch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert fatigue.&lt;/strong&gt; Hundreds of noisy, non-actionable alerts train people to ignore the pager. Every alert should be actionable and tied to user impact, or it shouldn't page. (More on this in the on-call article below.)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in 6 lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt; tells you &lt;em&gt;that&lt;/em&gt; something broke; &lt;strong&gt;observability&lt;/strong&gt; lets you find out &lt;em&gt;why&lt;/em&gt;, including failures you never predicted.&lt;/li&gt;
&lt;li&gt;The three pillars: &lt;strong&gt;metrics&lt;/strong&gt; (is it wrong, how much), &lt;strong&gt;logs&lt;/strong&gt; (what exactly happened), &lt;strong&gt;traces&lt;/strong&gt; (where the time/errors went).&lt;/li&gt;
&lt;li&gt;Alert on &lt;strong&gt;metrics&lt;/strong&gt;; investigate with &lt;strong&gt;logs and traces&lt;/strong&gt;, and link them with a shared &lt;code&gt;trace_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Instrument the &lt;strong&gt;four golden signals&lt;/strong&gt;: latency, traffic, errors, saturation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SLIs&lt;/strong&gt; measure, &lt;strong&gt;SLOs&lt;/strong&gt; set internal targets, &lt;strong&gt;SLAs&lt;/strong&gt; are external promises; the &lt;strong&gt;error budget&lt;/strong&gt; turns reliability into a decision.&lt;/li&gt;
&lt;li&gt;Don't chase 100%, keep metric labels low-cardinality and every alert actionable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Observability is the sense organ of a reliable system. Once you can see clearly, the next step is building systems that fail gracefully, and running the humans who respond when they do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make systems survive the failures you can now &lt;em&gt;see&lt;/em&gt;: &lt;a href="https://thesimplifiedtech.com/blog/reliability-resilience-design-for-failure" rel="noopener noreferrer"&gt;Reliability &amp;amp; Resilience: Designing for Failure&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;See observability in the context of running real clusters: &lt;a href="https://thesimplifiedtech.com/blog/kubernetes-in-production" rel="noopener noreferrer"&gt;Kubernetes in Production&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Turn signals into a humane response process: &lt;a href="https://thesimplifiedtech.com/blog/incident-management-and-oncall" rel="noopener noreferrer"&gt;Incident Management &amp;amp; On-Call&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Get hands-on inspecting a live system: the &lt;a href="https://thesimplifiedtech.com/labs/kubectl" rel="noopener noreferrer"&gt;kubectl Lab&lt;/a&gt; lets you query pod health and logs the way you would during an incident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instrument one service with the four golden signals this week. The next time it misbehaves, you'll be reading data instead of guessing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/observability-three-pillars" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>azure</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>Platform Engineering &amp; Internal Developer Platforms</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Tue, 11 Aug 2026 11:38:39 +0000</pubDate>
      <link>https://dev.to/sri2614/platform-engineering-internal-developer-platforms-cah</link>
      <guid>https://dev.to/sri2614/platform-engineering-internal-developer-platforms-cah</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Platform engineering corrects DevOps overload by building paved roads developers self-serve safely. Learn the platform-as-product mindset, what an &lt;strong&gt;IDP&lt;/strong&gt; provides, and the DevEx metrics that prove it's working.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why every developer became a part-time SRE&lt;/li&gt;
&lt;li&gt;The one-sentence definition&lt;/li&gt;
&lt;li&gt;Platform-as-product: the mindset shift&lt;/li&gt;
&lt;li&gt;What an IDP actually provides&lt;/li&gt;
&lt;li&gt;Building a paved road, step by step&lt;/li&gt;
&lt;li&gt;DevEx metrics: proving it works&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why every developer became a part-time SRE
&lt;/h2&gt;

&lt;p&gt;DevOps had a great idea, tear down the wall between dev and ops, let teams own what they ship, and then took it too literally. "You build it, you run it" quietly became "you build it, and also learn Kubernetes, Terraform, IAM, Helm, observability, and on-call." A backend engineer who wants to deploy a service now spends a week stitching together CI, a Dockerfile, a Helm chart, secrets, alerts, and a dozen YAML files, most of it copy-pasted from a teammate who copy-pasted it from someone who left.&lt;/p&gt;

&lt;p&gt;That's cognitive overload, and it's slow, inconsistent, and unsafe. &lt;strong&gt;Platform engineering&lt;/strong&gt; is the correction. Instead of every team reinventing the same infrastructure badly, a dedicated platform team builds &lt;strong&gt;paved roads&lt;/strong&gt;, self-service tooling that makes the right way the easy way. Developers get back to building product; the platform handles the undifferentiated heavy lifting. This article covers the mindset, what an Internal Developer Platform provides, and how you know it's working.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Senior engineers, tech leads, and anyone feeling the 'too much YAML, too little product' squeeze, whether you might build a platform or just want to understand why your org needs one. No platform background assumed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The one-sentence definition
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Platform engineering is building an internal product, a self-service platform, that lets developers ship and run software quickly and safely, without needing to be infrastructure experts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two phrases matter. &lt;strong&gt;Self-service&lt;/strong&gt;: developers get what they need on demand, without filing a ticket and waiting on the ops team. &lt;strong&gt;Paved road&lt;/strong&gt; (or golden path): the recommended, supported, pre-hardened way to do a common thing, so the secure, observable, production-ready option is also the path of least resistance.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🛣️ Paved highways with signs and guardrails&lt;/td&gt;
&lt;td&gt;Golden paths, supported, safe defaults&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🚗 Drive yourself, anytime, no chauffeur needed&lt;/td&gt;
&lt;td&gt;Self-service, no ticket, no waiting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🚧 Guardrails keep you from the cliff&lt;/td&gt;
&lt;td&gt;Policy / security baked into the path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🗺️ Off-road if you must, at your own risk&lt;/td&gt;
&lt;td&gt;Escape hatches for edge cases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;An IDP is a well-run highway system, not a pile of car parts.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform-as-product: the mindset shift
&lt;/h2&gt;

&lt;p&gt;The defining idea of platform engineering is treating the platform like a &lt;strong&gt;product&lt;/strong&gt;, with your developers as the customers. That sounds like jargon until you see what it changes. A ticket-driven ops team measures success by tickets closed. A platform-as-product team measures success by whether developers &lt;em&gt;choose&lt;/em&gt; to use the platform and ship faster because of it. If they route around it, the platform has failed, you can't mandate adoption of a bad product.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Traditional Ops&lt;/th&gt;
&lt;th&gt;Platform-as-Product&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;Tickets &amp;amp; requests&lt;/td&gt;
&lt;td&gt;Self-service APIs / portal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Success metric&lt;/td&gt;
&lt;td&gt;Tickets closed, uptime&lt;/td&gt;
&lt;td&gt;Developer adoption &amp;amp; speed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Developers are&lt;/td&gt;
&lt;td&gt;Requesters&lt;/td&gt;
&lt;td&gt;Customers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roadmap driven by&lt;/td&gt;
&lt;td&gt;Whoever shouts loudest&lt;/td&gt;
&lt;td&gt;User research &amp;amp; DevEx data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adoption&lt;/td&gt;
&lt;td&gt;Mandated&lt;/td&gt;
&lt;td&gt;Earned (it's the easy path)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Same people, sometimes, but a fundamentally different operating model.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; The litmus test: if you had to compete for your developers' adoption against 'just do it themselves,' would you win? If the honest answer is 'no, we'd lose without a mandate,' you're running ops with a new name, not platform engineering.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What an IDP actually provides
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;Internal Developer Platform&lt;/strong&gt; is the product the platform team builds. It bundles the scattered infrastructure concerns into a coherent self-service experience. Concretely, a good IDP gives a developer these things on demand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Service templates (scaffolding).&lt;/strong&gt; Run one command, get a new service with CI, a Dockerfile, health checks, and observability already wired in, not a blank repo and a wiki page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD as a paved road.&lt;/strong&gt; A standard, supported pipeline you opt into, not a bespoke one you hand-build and maintain per team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environments on demand.&lt;/strong&gt; Spin up a preview or staging environment for a branch without knowing how the cluster works underneath.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability baked in.&lt;/strong&gt; Logs, metrics, traces, and sensible default dashboards/alerts from day one, not something each team bolts on later (or doesn't).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails by default.&lt;/strong&gt; Security policy, resource limits, and compliance enforced by the path itself, so doing the right thing requires no extra effort.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Crucially, an IDP is an &lt;em&gt;abstraction&lt;/em&gt;, not a cage. The developer says "I want a new service" and gets a production-ready one; they don't need to know it's a Helm chart deployed via GitOps onto an autoscaled cluster. But the escape hatch must exist, when an edge case needs raw access, the platform should let an expert drop down a level rather than block them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a paved road, step by step
&lt;/h2&gt;

&lt;p&gt;You don't build an IDP by buying a tool and declaring victory. You build it like any product, find the worst recurring pain, pave that one path, earn trust, repeat. A pragmatic sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find the most painful repeated journey&lt;/strong&gt;: Talk to developers. It's almost always 'spin up and deploy a new service', measure how long that takes today, end to end. That number is your baseline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pave that one path&lt;/strong&gt;: Build a golden-path template that produces a deployable service with CI, observability, and security defaults already wired in. Solve one journey completely, not ten partially.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make it the easy choice&lt;/strong&gt;: The paved road must be faster and less work than rolling your own. If self-service is harder than a ticket, no one adopts it, and you can't mandate your way out of that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a thin self-service surface&lt;/strong&gt;: A CLI, a portal, or a Git-based interface so developers trigger it themselves. The interface is the product; keep it boringly simple.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure, then widen the road&lt;/strong&gt;: Track adoption and the journey time you baselined. Once one path is loved, pave the next painful one. Grow the platform by demand, not by mandate.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Don't build the everything-platform first:&lt;/strong&gt; The classic failure is a platform team disappearing for a year to build a grand unified platform nobody asked for, then forcing adoption. Pave one path, ship it, get real users, iterate. A loved tool for one journey beats an ignored cathedral for all of them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  DevEx metrics: proving it works
&lt;/h2&gt;

&lt;p&gt;Because a platform is a product, you measure it like one, by the developer experience and delivery outcomes it produces, not by lines of YAML you abstracted away. The DORA metrics are the industry-standard delivery signal, and they map directly onto what a good platform improves.&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;What it measures&lt;/th&gt;
&lt;th&gt;Platform should...&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deployment frequency&lt;/td&gt;
&lt;td&gt;How often you ship&lt;/td&gt;
&lt;td&gt;Increase it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lead time for changes&lt;/td&gt;
&lt;td&gt;Commit → production&lt;/td&gt;
&lt;td&gt;Shorten it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change failure rate&lt;/td&gt;
&lt;td&gt;% of deploys causing issues&lt;/td&gt;
&lt;td&gt;Lower it (guardrails)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to restore&lt;/td&gt;
&lt;td&gt;How fast you recover&lt;/td&gt;
&lt;td&gt;Shorten it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to first deploy&lt;/td&gt;
&lt;td&gt;New dev → first prod ship&lt;/td&gt;
&lt;td&gt;Collapse it (days → hours)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;If the platform is working, these move in the right direction. If they don't, the platform isn't the easy path.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That last one, &lt;strong&gt;time to first deploy&lt;/strong&gt; for a brand-new service or a brand-new hire, is the single most honest measure of an IDP. If a new engineer can ship a production service on day one because the paved road did the heavy lifting, your platform is real. If it still takes them a week of YAML and Slack questions, it isn't, no matter how much tooling exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Building the platform nobody asked for.&lt;/strong&gt; Pave the paths developers actually walk daily, found by talking to them, not the architecture you find elegant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mandating adoption instead of earning it.&lt;/strong&gt; A platform you have to force is a product that failed. Make the paved road the easiest option and adoption follows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No escape hatch.&lt;/strong&gt; An abstraction with no way down becomes a cage; the first hard edge case sends teams routing around the whole platform. Always allow dropping a level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating it as a one-time project.&lt;/strong&gt; A platform is a living product with a roadmap, support, and versioning, not a tool you ship once and abandon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping the metrics.&lt;/strong&gt; Without DORA / time-to-first-deploy data you're guessing whether the platform helps. Baseline before, measure after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebranding ops as 'platform' with no model change.&lt;/strong&gt; Same tickets, same gatekeeping, new title. The shift is self-service and product thinking, not a new org chart.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Platform engineering in six lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's the correction to DevOps overload: stop making every developer a part-time SRE.&lt;/li&gt;
&lt;li&gt;Build paved roads, the secure, observable, production-ready way is also the easiest way.&lt;/li&gt;
&lt;li&gt;Treat the platform as a product; developers are customers whose adoption you earn, not mandate.&lt;/li&gt;
&lt;li&gt;An IDP provides templates, CI/CD, on-demand environments, and observability, self-service, with guardrails.&lt;/li&gt;
&lt;li&gt;Always leave an escape hatch: abstraction, not a cage.&lt;/li&gt;
&lt;li&gt;Measure with DORA + time-to-first-deploy. If they don't improve, it's ops with a new name.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Platform engineering is where DevOps philosophy, GitOps delivery, and a real career track meet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/what-devops-actually-is" rel="noopener noreferrer"&gt;What DevOps Actually Is&lt;/a&gt;, the philosophy platform engineering extends and corrects.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/gitops-argocd-flux" rel="noopener noreferrer"&gt;GitOps: Declarative Delivery with ArgoCD &amp;amp; Flux&lt;/a&gt;, the delivery model most IDPs are built on under the hood.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/career-paths/devops-engineer" rel="noopener noreferrer"&gt;The DevOps Engineer career path&lt;/a&gt;, the skills that lead into platform work, level by level.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/platform-engineering-idp" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>docker</category>
      <category>platformengineering</category>
    </item>
    <item>
      <title>Securing the Software Supply Chain (SLSA, SBOM, Signing)</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Mon, 10 Aug 2026 11:42:40 +0000</pubDate>
      <link>https://dev.to/sri2614/securing-the-software-supply-chain-slsa-sbom-signing-499e</link>
      <guid>https://dev.to/sri2614/securing-the-software-supply-chain-slsa-sbom-signing-499e</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Most of your production code is dependencies you didn't write, so the supply chain is the attack surface. Shift security left with SBOMs, signing and provenance via Sigstore, the &lt;strong&gt;SLSA&lt;/strong&gt; levels, and least-privilege CI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You didn't write most of your production code&lt;/li&gt;
&lt;li&gt;Shift left: security in the pipeline, not after it&lt;/li&gt;
&lt;li&gt;SBOM: an ingredients label for your software&lt;/li&gt;
&lt;li&gt;Signing and provenance: prove it's really yours&lt;/li&gt;
&lt;li&gt;SLSA: a maturity ladder for your build&lt;/li&gt;
&lt;li&gt;Least-privilege CI and dependency hygiene&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  You didn't write most of your production code
&lt;/h2&gt;

&lt;p&gt;Open your &lt;code&gt;package.json&lt;/code&gt; or &lt;code&gt;go.mod&lt;/code&gt; and count the direct dependencies. Now count the &lt;em&gt;transitive&lt;/em&gt; ones, the dependencies of your dependencies. It's usually hundreds, often over a thousand. Add your base container image, your build tools, your CI runner, and the plugins in your pipeline. The uncomfortable truth: &lt;strong&gt;most of the code running in your production environment was written by strangers&lt;/strong&gt;, pulled in automatically, and trusted by default.&lt;/p&gt;

&lt;p&gt;That's the software supply chain, and it's now the favoured attack surface. SolarWinds, Codecov, the &lt;code&gt;event-stream&lt;/code&gt; and &lt;code&gt;xz&lt;/code&gt; backdoors, none of these broke in through your code. They came in through something you trusted. Securing the supply chain means treating every input to your build as untrusted until proven otherwise: knowing exactly what's in your artifacts (SBOMs), proving they're unmodified (signing and provenance), and hardening the build itself (SLSA). This article covers all three.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Engineers who own a CI/CD pipeline and want to make it defensible. No security background needed, we define SBOM, provenance, SLSA, and signing from scratch. Familiarity with a pipeline (build → test → publish) is assumed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Shift left: security in the pipeline, not after it
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Shifting left means moving security checks as early in the pipeline as possible, catching a vulnerable dependency at pull-request time, not in a pen-test six months after it shipped.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The later you find a problem, the more it costs. A vulnerable library caught by a scanner on the PR is a one-line bump. The same library found in production after an incident is an outage, a disclosure, and a postmortem. So the supply-chain discipline is to push every check as far upstream as it'll go, into the build, the PR, the merge.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📋 Know every ingredient in every dish&lt;/td&gt;
&lt;td&gt;SBOM, inventory of what's in your artifact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🚚 Verify the supplier sealed the delivery&lt;/td&gt;
&lt;td&gt;Signing, prove the artifact is unmodified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📹 Cameras on the whole prep process&lt;/td&gt;
&lt;td&gt;Provenance, how the artifact was built&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⭐ A health-inspection grade on the door&lt;/td&gt;
&lt;td&gt;SLSA level, how trustworthy the build is&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Securing a restaurant kitchen.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  SBOM: an ingredients label for your software
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Software Bill of Materials&lt;/strong&gt; is a complete, machine-readable list of every component in an artifact, every library, version, and license, transitive ones included. You can't secure what you can't see, and when the next &lt;code&gt;log4shell&lt;/code&gt; drops, the difference between "are we affected?" taking five minutes versus five days is whether you have SBOMs. Generate one for every build and store it as an artifact.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sbom.sh&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate an SBOM for a built container image (Syft)&lt;/span&gt;
syft registry.example.com/api:v2 &lt;span class="nt"&gt;-o&lt;/span&gt; spdx-json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; sbom.spdx.json

&lt;span class="c"&gt;# Later: instantly answer 'are we exposed to CVE-2025-XXXX?'&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"log4j"&lt;/span&gt; sbom.spdx.json

&lt;span class="c"&gt;# Or scan the SBOM itself against the vuln database (Grype)&lt;/span&gt;
grype sbom:sbom.spdx.json &lt;span class="nt"&gt;--fail-on&lt;/span&gt; high
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; SPDX and CycloneDX are the two standard SBOM formats, pick either; both are widely supported. The win isn't the format, it's the habit: an SBOM per build, stored and queryable, so 'what's affected?' is a grep and not an archaeology project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Signing and provenance: prove it's really yours
&lt;/h2&gt;

&lt;p&gt;An SBOM tells you what's &lt;em&gt;inside&lt;/em&gt; an artifact. Signing tells you the artifact is &lt;strong&gt;the one you built and hasn't been tampered with&lt;/strong&gt; since. Without a signature, an attacker who compromises your registry can swap your image for theirs and your cluster will happily run it. &lt;strong&gt;Sigstore&lt;/strong&gt; (via the &lt;code&gt;cosign&lt;/code&gt; tool) made signing practical: keyless signing tied to your CI's OIDC identity, with the signature published to a public transparency log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provenance&lt;/strong&gt; goes one step further: a signed statement of &lt;em&gt;how&lt;/em&gt; the artifact was built, which source commit, which builder, which steps. Together, signature + provenance let a deployer verify "this image came from our repo, built by our CI, from this exact commit" before it ever runs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.github/workflows/release.yml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# A scan + sign + provenance stage in CI&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;      &lt;span class="c1"&gt;# OIDC identity for keyless signing&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build image&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build -t $IMAGE .&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;Scan before publishing&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grype $IMAGE --fail-on high&lt;/span&gt;   &lt;span class="c1"&gt;# block on high/critical CVEs&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate SBOM&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;syft $IMAGE -o spdx-json &amp;gt; sbom.spdx.json&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;Push image&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker push $IMAGE&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;Sign (keyless, via OIDC)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cosign sign --yes $IMAGE&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;Attach SBOM as a signed attestation&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cosign attest --yes --predicate sbom.spdx.json \&lt;/span&gt;
               &lt;span class="s"&gt;--type spdxjson $IMAGE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;verify-before-deploy.sh&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Deployers verify the signature and the source repo BEFORE running it&lt;/span&gt;
cosign verify &lt;span class="nv"&gt;$IMAGE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--certificate-identity-regexp&lt;/span&gt; &lt;span class="s1"&gt;'https://github.com/acme/.*'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--certificate-oidc-issuer&lt;/span&gt; &lt;span class="s1"&gt;'https://token.actions.githubusercontent.com'&lt;/span&gt;

&lt;span class="c"&gt;# In a cluster, enforce this automatically with an admission policy&lt;/span&gt;
&lt;span class="c"&gt;# (e.g. Sigstore policy-controller / Kyverno) so unsigned images are rejected.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Signing without verification is theatre:&lt;/strong&gt; Signing your images does nothing if nothing checks the signature. The value only materialises when an admission controller (policy-controller, Kyverno, or a Connaisseur-style gate) rejects unsigned or wrongly-signed images at deploy time. Sign in CI, then enforce verification in the cluster, both halves, or neither.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  SLSA: a maturity ladder for your build
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;SLSA&lt;/strong&gt; (Supply-chain Levels for Software Artifacts, said "salsa") is a framework that grades how trustworthy your &lt;em&gt;build process&lt;/em&gt; is. It's a ladder, each level is a concrete, achievable step, not an all-or-nothing certification. You don't need the top rung; you need to know which rung you're on and climb one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;What it requires&lt;/th&gt;
&lt;th&gt;What it stops&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;L1&lt;/td&gt;
&lt;td&gt;Build is scripted; provenance exists&lt;/td&gt;
&lt;td&gt;Nothing built by hand / undocumented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L2&lt;/td&gt;
&lt;td&gt;Hosted build service; signed provenance&lt;/td&gt;
&lt;td&gt;Tampering with provenance after the fact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L3&lt;/td&gt;
&lt;td&gt;Hardened, isolated builds; non-falsifiable provenance&lt;/td&gt;
&lt;td&gt;A compromised build job forging its own provenance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Climb one level at a time. Most teams should target L2 → L3 as a realistic, high-value goal.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The jump from L1 to L2 is mostly "use a real CI service and sign the provenance", achievable in an afternoon. L3 (isolated, ephemeral, hardened build runners) is where you stop a compromised build step from lying about what it produced. Pick a target, measure where you are, and treat the gap as a backlog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Least-privilege CI and dependency hygiene
&lt;/h2&gt;

&lt;p&gt;Your pipeline is itself a high-value target, it has registry credentials, signing identity, and often cluster access. Treat it like production. And the dependencies it pulls deserve the same suspicion as external input, because that's exactly what they are.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pin dependencies by hash, not just version.&lt;/strong&gt; A version tag can be re-pointed; a hash can't. Use lockfiles with integrity hashes and pin GitHub Actions to a commit SHA, never a moving tag like &lt;code&gt;@v4&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope CI tokens to the minimum.&lt;/strong&gt; Per-job, least-privilege, short-lived. A workflow that builds shouldn't hold deploy credentials. Prefer OIDC over long-lived secrets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scan dependencies and images on every PR&lt;/strong&gt;, and fail the build on high/critical CVEs. Make the safe path the default path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a private proxy/registry&lt;/strong&gt; for dependencies so a deleted or hijacked upstream package can't break or poison your builds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review what you add.&lt;/strong&gt; A new dependency is new code from a stranger running with your privileges. Weigh whether you need it at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;The moving-tag trap:&lt;/strong&gt; Pinning a GitHub Action to @v4 means you run whatever the maintainer (or whoever compromises their account) points v4 at, automatically, on your next build, with your secrets. Pin to a full commit SHA and update deliberately. This single change closes one of the most common CI attack paths.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No SBOM, so every new CVE is an archaeology project.&lt;/strong&gt; When the next log4shell drops, 'are we affected?' should be a grep, not a week of spelunking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signing images but never verifying them.&lt;/strong&gt; A signature nothing checks is decoration. Enforce verification at deploy time with an admission policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pinning Actions and base images to moving tags.&lt;/strong&gt; @v4 and :latest mean you silently run whatever someone repoints them to. Pin by SHA / digest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over-privileged CI.&lt;/strong&gt; A single broad token turns a compromised build into a compromised registry, signing key, and cluster. Scope tokens per job; prefer OIDC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scanning only in production / on a schedule.&lt;/strong&gt; By then it's an incident. Scan on the PR and fail the build on high-severity findings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating SLSA as pass/fail.&lt;/strong&gt; It's a ladder. Not knowing your level is the real failure, measure, then climb one rung.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Supply-chain security in six lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Most of your production code is other people's, the supply chain is the attack surface.&lt;/li&gt;
&lt;li&gt;Shift left: catch vulnerable dependencies on the PR, not in a post-incident pen-test.&lt;/li&gt;
&lt;li&gt;SBOM = an ingredients label per build, so 'are we affected?' is a grep.&lt;/li&gt;
&lt;li&gt;Sign artifacts (cosign/Sigstore) AND enforce verification at deploy, both halves or neither.&lt;/li&gt;
&lt;li&gt;Provenance proves how an artifact was built; SLSA grades how trustworthy that build is.&lt;/li&gt;
&lt;li&gt;Pin by hash, scope CI tokens tightly, scan every PR, your pipeline is production.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Supply-chain security lives inside your pipeline and is one expression of a broader 'trust nothing by default' mindset:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/cicd-fundamentals-what-a-pipeline-does" rel="noopener noreferrer"&gt;CI/CD Fundamentals: What a Pipeline Actually Does&lt;/a&gt;, the pipeline these controls plug into.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/zero-trust-networking-beginners" rel="noopener noreferrer"&gt;Zero-Trust Networking for Beginners&lt;/a&gt;, the same 'verify everything' principle, applied to the network.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/labs/cicd" rel="noopener noreferrer"&gt;Hands-on CI/CD lab&lt;/a&gt;, build a pipeline you can then harden with scanning and signing.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/securing-the-software-supply-chain" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>docker</category>
      <category>security</category>
    </item>
    <item>
      <title>Kubernetes in Production: Beyond the Tutorial</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Sun, 09 Aug 2026 11:22:11 +0000</pubDate>
      <link>https://dev.to/sri2614/kubernetes-in-production-beyond-the-tutorial-3edd</link>
      <guid>https://dev.to/sri2614/kubernetes-in-production-beyond-the-tutorial-3edd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Production Kubernetes is everything the tutorial skipped: resource requests and limits, liveness and readiness probes, autoscaling, disruption budgets, and namespaces with RBAC. Learn the guardrails that keep clusters standing at 3am.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The gap between 'it runs' and 'it survives'&lt;/li&gt;
&lt;li&gt;The first guardrail: requests and limits&lt;/li&gt;
&lt;li&gt;The second guardrail: liveness and readiness probes&lt;/li&gt;
&lt;li&gt;A production-grade Deployment&lt;/li&gt;
&lt;li&gt;Autoscaling and disruption budgets&lt;/li&gt;
&lt;li&gt;Namespaces and RBAC: blast-radius control&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The gap between 'it runs' and 'it survives'
&lt;/h2&gt;

&lt;p&gt;Every Kubernetes tutorial ends the same way: &lt;code&gt;kubectl apply&lt;/code&gt;, a pod goes Running, you &lt;code&gt;curl&lt;/code&gt; it, victory. Then you put it in production and discover everything the tutorial left out. One pod with a memory leak takes down its neighbours. A deploy goes out while a node is draining and you serve 502s. Traffic triples and nothing scales. A bad config gets applied cluster-wide because everything shares one namespace with god-mode access.&lt;/p&gt;

&lt;p&gt;None of these are exotic. They're the &lt;strong&gt;default failure modes&lt;/strong&gt; of a cluster run the way tutorials teach it. Production Kubernetes is mostly a handful of guardrails, resource requests and limits, health probes, autoscaling, disruption budgets, namespaces and RBAC, that the tutorial skipped because they're boring until the night they save you. This article is those guardrails, explained and shown in real YAML.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Engineers who can deploy to Kubernetes but haven't yet operated it under real traffic, real incidents, and real teammates. You should know what a Pod, Deployment, and Service are; we cover the production layer on top.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The first guardrail: requests and limits
&lt;/h2&gt;

&lt;p&gt;A pod with no resource requests is a pod the scheduler is guessing about. &lt;strong&gt;Requests&lt;/strong&gt; are what the pod is guaranteed, the scheduler uses them to decide which node has room. &lt;strong&gt;Limits&lt;/strong&gt; are the ceiling, exceed CPU and you get throttled; exceed memory and you get &lt;strong&gt;OOMKilled&lt;/strong&gt;. Without them, one greedy pod can starve every other pod on its node, and the scheduler can pack a node past the point of stability.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Request&lt;/th&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Means&lt;/td&gt;
&lt;td&gt;Guaranteed minimum&lt;/td&gt;
&lt;td&gt;Hard ceiling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Used by&lt;/td&gt;
&lt;td&gt;The scheduler (placement)&lt;/td&gt;
&lt;td&gt;The kubelet (enforcement)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hit CPU ceiling&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;Throttled (slowed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hit memory ceiling&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;OOMKilled (restarted)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set too low&lt;/td&gt;
&lt;td&gt;Pod evicted under pressure&lt;/td&gt;
&lt;td&gt;Killed under normal load&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Requests are for scheduling and guarantees; limits are for protecting the neighbours.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Memory limits are not like CPU limits:&lt;/strong&gt; CPU is compressible, over the limit, you're just throttled and slow. Memory is not, over the limit, your pod is killed outright. Set memory requests and limits equal for critical workloads so the scheduler reserves exactly what the pod can use, and you never get surprise OOMKills under contention.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The second guardrail: liveness and readiness probes
&lt;/h2&gt;

&lt;p&gt;These two probes sound similar and do opposite things, confusing them causes some of the nastiest production incidents. &lt;strong&gt;Readiness&lt;/strong&gt; answers "should this pod receive traffic right now?" If it fails, the pod is pulled from the Service's load-balancing pool but left running. &lt;strong&gt;Liveness&lt;/strong&gt; answers "is this pod broken beyond recovery?" If it fails, the pod is &lt;strong&gt;killed and restarted&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🪑 Still in onboarding, don't send them customers yet&lt;/td&gt;
&lt;td&gt;Readiness failing: no traffic, still running&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✅ Onboarded and ready, start routing work&lt;/td&gt;
&lt;td&gt;Readiness passing: added to the pool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🚑 Collapsed at their desk, call an ambulance&lt;/td&gt;
&lt;td&gt;Liveness failing: killed and restarted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;A new hire on their first day.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;The classic outage: liveness that's too aggressive:&lt;/strong&gt; Point liveness at a heavy endpoint, or set the timeout too tight, and a brief slowdown makes the probe fail, so Kubernetes kills the pod. Under load, every pod slows, every probe fails, and the cluster restarts your entire fleet mid-traffic-spike. Keep liveness cheap and forgiving; use readiness for the strict checks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A production-grade Deployment
&lt;/h2&gt;

&lt;p&gt;Here's a Deployment with all the guardrails wired in. Compare it to the three-line tutorial version, every extra block earns its place.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;deployment.yaml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;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;api&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments&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;3&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;maxSurge&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;maxUnavailable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;          &lt;span class="c1"&gt;# add before removing: zero-downtime&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;api&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;registry.example.com/api:v2&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
          &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;250m"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;256Mi"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;500m"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;256Mi"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;# mem = request&lt;/span&gt;
          &lt;span class="na"&gt;readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                    &lt;span class="c1"&gt;# gate traffic&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;/readyz&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;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
            &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
          &lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                     &lt;span class="c1"&gt;# restart if wedged&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;/healthz&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;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;
            &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
            &lt;span class="na"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
            &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;              &lt;span class="c1"&gt;# forgiving, not trigger-happy&lt;/span&gt;
          &lt;span class="na"&gt;securityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;runAsNonRoot&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;allowPrivilegeEscalation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
            &lt;span class="na"&gt;readOnlyRootFilesystem&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;Note the deliberate asymmetry between the probes: readiness checks often (every 5s, starts at 5s) so traffic is gated tightly; liveness checks rarely (every 20s, starts at 15s, tolerates 3 failures) so a transient hiccup never triggers a needless restart. That single difference prevents the most common self-inflicted Kubernetes outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Autoscaling and disruption budgets
&lt;/h2&gt;

&lt;p&gt;Three replicas is a guess. The &lt;strong&gt;Horizontal Pod Autoscaler&lt;/strong&gt; adjusts replica count to match load, scale out when CPU climbs, back in when it's quiet, so you neither fall over at peak nor pay for idle capacity at 3am.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;hpa.yaml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;autoscaling/v2&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;HorizontalPodAutoscaler&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;api&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments&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;scaleTargetRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
  &lt;span class="na"&gt;minReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;maxReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
  &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&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;Resource&lt;/span&gt;
      &lt;span class="na"&gt;resource&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;cpu&lt;/span&gt;
        &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Utilization&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;averageUtilization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;65&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HPA needs your &lt;strong&gt;requests&lt;/strong&gt; set correctly, &lt;code&gt;averageUtilization: 65&lt;/code&gt; means 65% &lt;em&gt;of the request&lt;/em&gt;. This is why the guardrails compound: get requests wrong and your autoscaler does the wrong thing. The other half of staying up during change is the &lt;strong&gt;PodDisruptionBudget&lt;/strong&gt;, which protects you during &lt;em&gt;voluntary&lt;/em&gt; disruptions like node drains and cluster upgrades.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pdb.yaml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;policy/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;PodDisruptionBudget&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;api&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments&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;minAvailable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;            &lt;span class="c1"&gt;# never let a drain take us below 2 pods&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;api&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Without a PDB, a routine node upgrade can evict all your pods at once, the cluster is just doing maintenance and has no idea your three pods all sat on the same node. minAvailable: 2 forces the drain to wait until replacements are ready. This is the cheapest reliability win in all of Kubernetes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Namespaces and RBAC: blast-radius control
&lt;/h2&gt;

&lt;p&gt;Running everything in &lt;code&gt;default&lt;/code&gt; with shared credentials is how a single mistake becomes a cluster-wide outage. &lt;strong&gt;Namespaces&lt;/strong&gt; partition the cluster into isolated environments, separate quotas, separate network policies, separate RBAC. &lt;strong&gt;RBAC&lt;/strong&gt; then grants each human and service account the &lt;em&gt;least&lt;/em&gt; access it needs. The payments team's CI should be able to deploy to the payments namespace and nothing else.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rbac.yaml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# A role scoped to ONE namespace, allowing only what's needed&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rbac.authorization.k8s.io/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;Role&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;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments&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;deployer&lt;/span&gt;
&lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;apiGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apps"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deployments"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;verbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;list"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;update"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;patch"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rbac.authorization.k8s.io/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;RoleBinding&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;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments&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;payments-ci&lt;/span&gt;
&lt;span class="na"&gt;subjects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&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;ServiceAccount&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;ci&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments&lt;/span&gt;
&lt;span class="na"&gt;roleRef&lt;/span&gt;&lt;span class="pi"&gt;:&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;Role&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;deployer&lt;/span&gt;
  &lt;span class="na"&gt;apiGroup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rbac.authorization.k8s.io&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;cluster-admin is not a starting point:&lt;/strong&gt; It's tempting to bind everything to cluster-admin to make errors go away. Don't. A leaked cluster-admin token is the entire cluster. Start from zero permissions and add exactly what each workload needs, a Role (namespace-scoped) beats a ClusterRole every time you can get away with it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No resource requests.&lt;/strong&gt; The scheduler packs nodes blindly and one leaky pod starves the rest. Always set requests; set memory request = limit for critical pods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Liveness probe doing real work.&lt;/strong&gt; A liveness check that hits the database or a slow endpoint turns a brief slowdown into a fleet-wide restart storm. Keep liveness trivially cheap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;maxUnavailable left at the default during rollouts.&lt;/strong&gt; The 25% default can drop a quarter of your capacity mid-deploy. Set it to 0 with a positive maxSurge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No PodDisruptionBudget.&lt;/strong&gt; A routine node drain evicts all your replicas at once. A one-line PDB prevents the most surprising self-inflicted outage there is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HPA with wrong or missing requests.&lt;/strong&gt; Utilization targets are a percentage of the request, wrong requests mean the autoscaler scales on a lie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything in default with broad RBAC.&lt;/strong&gt; No isolation means no blast-radius control. Namespace per team/env, least-privilege RBAC, no casual cluster-admin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring &lt;code&gt;kubectl describe&lt;/code&gt; and events.&lt;/strong&gt; When a pod won't start, the answer is almost always in the events, ImagePullBackOff, OOMKilled, FailedScheduling. Read them first, guess never.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Production Kubernetes in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requests are for scheduling; limits protect the neighbours. Memory over limit = OOMKilled.&lt;/li&gt;
&lt;li&gt;Readiness gates traffic (no kill); liveness restarts a wedged pod (kill). Don't swap them.&lt;/li&gt;
&lt;li&gt;Keep liveness cheap and forgiving, aggressive liveness causes restart storms under load.&lt;/li&gt;
&lt;li&gt;maxUnavailable: 0 + maxSurge gives true zero-downtime rollouts.&lt;/li&gt;
&lt;li&gt;HPA scales on a percentage of the request, so correct requests come first.&lt;/li&gt;
&lt;li&gt;A PodDisruptionBudget is the cheapest reliability win, it survives node drains.&lt;/li&gt;
&lt;li&gt;Namespaces + least-privilege RBAC keep one mistake from becoming a cluster outage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Production Kubernetes connects to how you decided to use containers, how you deliver to the cluster, and how you observe it once it's live:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/docker-vs-kubernetes-when-to-use-each" rel="noopener noreferrer"&gt;Docker vs Kubernetes: When to Use Each&lt;/a&gt;, make sure Kubernetes is actually the right tool first.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/gitops-argocd-flux" rel="noopener noreferrer"&gt;GitOps: Declarative Delivery with ArgoCD &amp;amp; Flux&lt;/a&gt;, deliver these manifests safely and auditably.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/observability-three-pillars" rel="noopener noreferrer"&gt;The Three Pillars of Observability&lt;/a&gt;, you can't operate what you can't see.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/labs/kubectl" rel="noopener noreferrer"&gt;Hands-on kubectl lab&lt;/a&gt;, practise probes, scaling, and reading events on a live cluster.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/kubernetes-in-production" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>docker</category>
      <category>production</category>
    </item>
    <item>
      <title>GitOps: Declarative Delivery with ArgoCD &amp; Flux</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Sat, 08 Aug 2026 11:21:22 +0000</pubDate>
      <link>https://dev.to/sri2614/gitops-declarative-delivery-with-argocd-flux-3121</link>
      <guid>https://dev.to/sri2614/gitops-declarative-delivery-with-argocd-flux-3121</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; &lt;strong&gt;GitOps&lt;/strong&gt; makes Git the single source of truth and lets a controller reconcile reality to match it, so rollback is a git revert, every change is audited, and drift heals itself. Includes ArgoCD vs Flux and the trade-offs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Who pushed that change to production?&lt;/li&gt;
&lt;li&gt;The one-sentence definition&lt;/li&gt;
&lt;li&gt;The reconciliation loop&lt;/li&gt;
&lt;li&gt;Pull vs push: why the direction matters&lt;/li&gt;
&lt;li&gt;ArgoCD vs Flux&lt;/li&gt;
&lt;li&gt;A real ArgoCD Application&lt;/li&gt;
&lt;li&gt;What GitOps actually buys you&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who pushed that change to production?
&lt;/h2&gt;

&lt;p&gt;Someone ran a &lt;code&gt;kubectl apply&lt;/code&gt; at 11pm to fix an incident. Nobody wrote it down. Three weeks later the cluster behaves differently than the YAML in your repo says it should, and no one can explain why. This is &lt;strong&gt;configuration drift&lt;/strong&gt;, and it's the slow rot at the heart of imperative deployment, every &lt;code&gt;kubectl edit&lt;/code&gt;, every manual scale, every quick fix moves the live system away from any written record of it.&lt;/p&gt;

&lt;p&gt;GitOps fixes this by inverting the flow. Instead of &lt;em&gt;pushing&lt;/em&gt; changes to the cluster, you commit the desired state to Git and let a controller &lt;em&gt;pull&lt;/em&gt; it, continuously reconciling the live cluster to match. Git becomes the single source of truth, your deploy history becomes your git log, and rollback becomes &lt;code&gt;git revert&lt;/code&gt;. This article builds that model and shows it with a real ArgoCD &lt;code&gt;Application&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Engineers running things on Kubernetes who deploy with scripts or CI that runs kubectl, and want auditable, self-healing delivery. Comfort with Git and basic k8s manifests is assumed; we explain the GitOps parts from scratch.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The one-sentence definition
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;GitOps is operating your infrastructure by declaring its desired state in Git and letting an automated controller continuously make reality match that declaration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two words carry the weight. &lt;strong&gt;Declarative&lt;/strong&gt;: you describe the end state you want (10 replicas of v2), not the steps to get there. &lt;strong&gt;Reconciliation&lt;/strong&gt;: a controller constantly compares desired (Git) to actual (cluster) and closes the gap, forever. It's a thermostat, not a light switch.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🌡️ You set the target temperature&lt;/td&gt;
&lt;td&gt;Commit desired state to Git&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔁 The thermostat reads the room, constantly&lt;/td&gt;
&lt;td&gt;Controller diffs Git vs cluster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔥 It heats or cools to close the gap&lt;/td&gt;
&lt;td&gt;Reconcile: apply changes to match Git&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🪟 Someone opens a window (drift)&lt;/td&gt;
&lt;td&gt;Manual kubectl edit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;♨️ It corrects automatically&lt;/td&gt;
&lt;td&gt;Self-heal back to Git's state&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;GitOps is a thermostat for your cluster.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The reconciliation loop
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fZGV2KCJEZXZlbG9wZXJzPGJyLz5PcGVuIGEgUFIiKQogIGNsYXNzIG5fZGV2IGNsaWVudDsKICBuX2dpdCgiQ29uZmlnIFJlcG88YnIvPkRlc2lyZWQgc3RhdGUgKEdpdCkiKQogIGNsYXNzIG5fZ2l0IGV4dGVybmFsOwogIG5fY3RsKCJHaXRPcHMgQ29udHJvbGxlcjxici8-QXJnb0NEIC8gRmx1eCIpCiAgY2xhc3Mgbl9jdGwgY29tcHV0ZTsKICBuX2NsdXN0ZXIoIkt1YmVybmV0ZXM8YnIvPkFjdHVhbCBzdGF0ZSIpCiAgY2xhc3Mgbl9jbHVzdGVyIGRhdGE7CiAgbl9yZWdpc3RyeSgiSW1hZ2UgUmVnaXN0cnk8YnIvPk5ldyBpbWFnZSB0YWdzIikKICBjbGFzcyBuX3JlZ2lzdHJ5IGV4dGVybmFsOwogIG5fZGV2IC0tPnwibWVyZ2UgUFIifCBuX2dpdAogIG5fY3RsIC0uLT58IndhdGNoIGRlc2lyZWQifCBuX2dpdAogIG5fY3RsIC0uLT58IndhdGNoIGFjdHVhbCJ8IG5fY2x1c3RlcgogIG5fY3RsIC0tPnwiYXBwbHkgLyBoZWFsInwgbl9jbHVzdGVyCiAgbl9yZWdpc3RyeSAtLi0-fCJidW1wIHRhZyAoQ0kpInwgbl9naXQ%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fZGV2KCJEZXZlbG9wZXJzPGJyLz5PcGVuIGEgUFIiKQogIGNsYXNzIG5fZGV2IGNsaWVudDsKICBuX2dpdCgiQ29uZmlnIFJlcG88YnIvPkRlc2lyZWQgc3RhdGUgKEdpdCkiKQogIGNsYXNzIG5fZ2l0IGV4dGVybmFsOwogIG5fY3RsKCJHaXRPcHMgQ29udHJvbGxlcjxici8-QXJnb0NEIC8gRmx1eCIpCiAgY2xhc3Mgbl9jdGwgY29tcHV0ZTsKICBuX2NsdXN0ZXIoIkt1YmVybmV0ZXM8YnIvPkFjdHVhbCBzdGF0ZSIpCiAgY2xhc3Mgbl9jbHVzdGVyIGRhdGE7CiAgbl9yZWdpc3RyeSgiSW1hZ2UgUmVnaXN0cnk8YnIvPk5ldyBpbWFnZSB0YWdzIikKICBjbGFzcyBuX3JlZ2lzdHJ5IGV4dGVybmFsOwogIG5fZGV2IC0tPnwibWVyZ2UgUFIifCBuX2dpdAogIG5fY3RsIC0uLT58IndhdGNoIGRlc2lyZWQifCBuX2dpdAogIG5fY3RsIC0uLT58IndhdGNoIGFjdHVhbCJ8IG5fY2x1c3RlcgogIG5fY3RsIC0tPnwiYXBwbHkgLyBoZWFsInwgbl9jbHVzdGVyCiAgbl9yZWdpc3RyeSAtLi0-fCJidW1wIHRhZyAoQ0kpInwgbl9naXQ%3FbgColor%3D0d1017%26type%3Dpng" alt="The GitOps loop. Developers never touch the cluster directly, they open a pull request against the config repo. The cont" width="463" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The GitOps loop. Developers never touch the cluster directly, they open a pull request against the config repo. The controller (ArgoCD or Flux) lives inside the cluster, watches Git for the desired state and the cluster for the actual state, and reconciles any difference. The dashed arrows are the continuous watch; the solid arrow is the apply that closes drift.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A change starts as a pull request&lt;/strong&gt;: You don't run kubectl. You edit a manifest in the config repo and open a PR, the same review, approval, and audit trail as any code change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The merge is the deploy&lt;/strong&gt;: When the PR merges to the main branch, Git's desired state has changed. That's the only action a human takes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The controller notices the diff&lt;/strong&gt;: Living inside the cluster, the controller polls or is webhook-notified that Git now differs from the running cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It reconciles&lt;/strong&gt;: The controller applies whatever is needed to make the cluster match Git, creating, updating, or deleting resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It keeps watching, forever&lt;/strong&gt;: If anyone hand-edits the cluster, the controller sees the drift and pulls it back to Git's truth (self-heal). The repo always wins.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Pull vs push: why the direction matters
&lt;/h2&gt;

&lt;p&gt;Traditional CI/CD &lt;strong&gt;pushes&lt;/strong&gt;: your pipeline holds cluster credentials and runs &lt;code&gt;kubectl apply&lt;/code&gt; from the outside. GitOps &lt;strong&gt;pulls&lt;/strong&gt;: a controller inside the cluster reaches out to Git and applies changes itself. That flip sounds small but it changes your security posture and your guarantees.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Push (CI runs kubectl)&lt;/th&gt;
&lt;th&gt;Pull (GitOps controller)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who applies changes&lt;/td&gt;
&lt;td&gt;External CI pipeline&lt;/td&gt;
&lt;td&gt;Controller inside the cluster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster credentials live&lt;/td&gt;
&lt;td&gt;In CI (broad blast radius)&lt;/td&gt;
&lt;td&gt;Inside the cluster only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drift detection&lt;/td&gt;
&lt;td&gt;None, fire and forget&lt;/td&gt;
&lt;td&gt;Continuous; self-heals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source of truth&lt;/td&gt;
&lt;td&gt;Whatever last ran&lt;/td&gt;
&lt;td&gt;Git, always&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback&lt;/td&gt;
&lt;td&gt;Re-run an old pipeline&lt;/td&gt;
&lt;td&gt;git revert&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The pull model keeps cluster credentials inside the cluster and makes drift detectable.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; The credentials point is underrated. With push, every CI runner needs cluster-admin-ish access, so a compromised pipeline is a compromised cluster. With pull, the cluster credentials never leave the cluster, CI only ever needs write access to a Git repo.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  ArgoCD vs Flux
&lt;/h2&gt;

&lt;p&gt;The two dominant controllers do the same job with different personalities. ArgoCD ships a polished web UI and an explicit &lt;code&gt;Application&lt;/code&gt; resource, great for teams who want to &lt;em&gt;see&lt;/em&gt; sync status and drift. Flux is leaner, more composable, and Git-native to a fault, great for teams who want everything, including Flux's own config, to live in YAML with no dashboard.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;ArgoCD&lt;/th&gt;
&lt;th&gt;Flux&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;Rich web dashboard&lt;/td&gt;
&lt;td&gt;CLI-first, minimal UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Core unit&lt;/td&gt;
&lt;td&gt;Application CRD&lt;/td&gt;
&lt;td&gt;Kustomization + sources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-tenancy&lt;/td&gt;
&lt;td&gt;Projects, RBAC, SSO built in&lt;/td&gt;
&lt;td&gt;Via namespaces + RBAC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image automation&lt;/td&gt;
&lt;td&gt;Add-on&lt;/td&gt;
&lt;td&gt;Built-in image updater&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feels like&lt;/td&gt;
&lt;td&gt;A deploy console&lt;/td&gt;
&lt;td&gt;A set of Unix tools&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Both are CNCF graduated and production-proven. Pick on UI preference and how composable you want it.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A real ArgoCD Application
&lt;/h2&gt;

&lt;p&gt;Here's the resource that wires it all together. This &lt;code&gt;Application&lt;/code&gt; tells ArgoCD: watch this repo path, keep this namespace in sync with it, and heal any drift automatically. Once this exists, you never deploy this app by hand again, you change the repo.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;application.yaml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;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;web&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&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;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&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/acme/cluster-config.git&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;          &lt;span class="c1"&gt;# the branch that is 'production'&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;apps/web/overlays/prod&lt;/span&gt;  &lt;span class="c1"&gt;# where this app's manifests live&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://kubernetes.default.svc&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;
  &lt;span class="na"&gt;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="c1"&gt;# delete resources removed from Git&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;span class="c1"&gt;# revert manual cluster edits back to Git&lt;/span&gt;
    &lt;span class="na"&gt;syncOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CreateNamespace=true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read &lt;code&gt;syncPolicy&lt;/code&gt; carefully, it's where GitOps gets its teeth. &lt;code&gt;prune: true&lt;/code&gt; means deleting a manifest from Git deletes it from the cluster (Git is &lt;em&gt;complete&lt;/em&gt; truth, not just additive). &lt;code&gt;selfHeal: true&lt;/code&gt; means a hand-edit to the live cluster gets reverted on the next reconcile. Together they guarantee the cluster can never silently diverge from the repo.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;verify.sh&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# See what Argo thinks: is the app in sync with Git?&lt;/span&gt;
argocd app get web

&lt;span class="c"&gt;# Watch a reconcile happen live after you merge a PR&lt;/span&gt;
argocd app &lt;span class="nb"&gt;sync &lt;/span&gt;web &lt;span class="nt"&gt;--watch&lt;/span&gt;

&lt;span class="c"&gt;# Roll back to any previous Git state, this is your 'undo'&lt;/span&gt;
argocd app &lt;span class="nb"&gt;history &lt;/span&gt;web
argocd app rollback web &amp;lt;revision&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Turn on selfHeal deliberately:&lt;/strong&gt; selfHeal is wonderful until an incident, when someone urgently scales a deployment by hand and the controller helpfully reverts it 30 seconds later. The fix isn't to disable selfHeal; it's culture: in a GitOps world, the emergency fix is also a fast PR. Make merging trivial so nobody is tempted to fight the controller.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What GitOps actually buys you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audit for free.&lt;/strong&gt; Every change to production is a git commit, author, timestamp, diff, review, all in your history. No separate change log to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback = git revert.&lt;/strong&gt; Bad release? Revert the commit; the controller reconciles the cluster back. No special tooling, no remembering which pipeline run was good.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-healing.&lt;/strong&gt; Drift from manual edits or partial failures is detected and corrected automatically. The cluster trends toward the repo, always.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disaster recovery is reproducible.&lt;/strong&gt; Lost the cluster? Point a fresh controller at the same repo and it rebuilds the declared state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Least-privilege CI.&lt;/strong&gt; Your pipeline only needs to write to Git, not to the cluster, shrinking the blast radius of a compromised runner.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Putting app code and config in the same repo.&lt;/strong&gt; Mixing them means every code commit churns the deploy controller. Keep a separate config repo (or at least a separate path) as the source of truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Committing plaintext secrets to Git.&lt;/strong&gt; Git is now your source of truth, including your secrets if you're careless. Use Sealed Secrets, SOPS, or an external secrets operator. Never raw base64.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting prune, then wondering why deleted manifests linger.&lt;/strong&gt; Without prune, removing a file from Git leaves the resource orphaned in the cluster. Git stops being complete truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fighting selfHeal during incidents.&lt;/strong&gt; Hand-editing a self-healing app starts a tug-of-war with the controller. Make emergency PRs fast instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No environment promotion strategy.&lt;/strong&gt; Pointing dev and prod at the same branch means every merge hits production. Use separate paths, overlays, or branches per environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating the controller as fire-and-forget.&lt;/strong&gt; GitOps controllers need monitoring too, a wedged ArgoCD silently stops reconciling, and drift creeps back in unnoticed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GitOps in six lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Declare desired state in Git; a controller continuously reconciles the cluster to match.&lt;/li&gt;
&lt;li&gt;Pull beats push: cluster credentials stay in the cluster, and drift self-heals.&lt;/li&gt;
&lt;li&gt;Every production change is a reviewed, audited git commit, for free.&lt;/li&gt;
&lt;li&gt;Rollback is git revert; disaster recovery is point-a-controller-at-the-repo.&lt;/li&gt;
&lt;li&gt;prune + selfHeal make Git the complete, authoritative truth, use them on purpose.&lt;/li&gt;
&lt;li&gt;Keep config separate from app code, and never commit raw secrets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;GitOps sits on top of Kubernetes and Git workflows, strengthen both, then go deep on the controller:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/kubernetes-in-production" rel="noopener noreferrer"&gt;Kubernetes in Production: Beyond the Tutorial&lt;/a&gt;, the manifests your GitOps repo will hold.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/git-workflows-production-teams" rel="noopener noreferrer"&gt;Git Workflows for Production Teams&lt;/a&gt;, because in GitOps, your branch strategy &lt;em&gt;is&lt;/em&gt; your deploy strategy.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/labs/kubectl" rel="noopener noreferrer"&gt;Hands-on kubectl lab&lt;/a&gt;, get fluent with the cluster the controller manages.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/labs/helm" rel="noopener noreferrer"&gt;Hands-on Helm lab&lt;/a&gt;, package the manifests ArgoCD and Flux deploy.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/gitops-argocd-flux" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>docker</category>
      <category>gitops</category>
    </item>
    <item>
      <title>Artifacts &amp; Registries: Where Your Builds Live</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Fri, 07 Aug 2026 11:50:01 +0000</pubDate>
      <link>https://dev.to/sri2614/artifacts-registries-where-your-builds-live-5ddk</link>
      <guid>https://dev.to/sri2614/artifacts-registries-where-your-builds-live-5ddk</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Learn what your pipeline actually produces and where it lives: immutable build artifacts, meaningful version tags (and why &lt;strong&gt;latest&lt;/strong&gt; is a trap), how registries work, and the build-once-promote-many pattern.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Your pipeline builds something. Where does it go?&lt;/li&gt;
&lt;li&gt;What is a build artifact?&lt;/li&gt;
&lt;li&gt;Immutability: the property that makes it trustworthy&lt;/li&gt;
&lt;li&gt;Versioning and tags, and why &lt;code&gt;latest&lt;/code&gt; is a trap&lt;/li&gt;
&lt;li&gt;What a registry is and how it works&lt;/li&gt;
&lt;li&gt;Build, tag, and push for real&lt;/li&gt;
&lt;li&gt;Build once, promote many&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Your pipeline builds something. Where does it go?
&lt;/h2&gt;

&lt;p&gt;You've got a pipeline that builds and tests your code. But "builds" produces a &lt;em&gt;thing&lt;/em&gt;, and that thing has to live somewhere between being built and being deployed. That thing is a &lt;strong&gt;build artifact&lt;/strong&gt;, and the place it lives is a &lt;strong&gt;registry&lt;/strong&gt;. They're quietly central to how every team ships software, and they're the part beginners tend to hand-wave past.&lt;/p&gt;

&lt;p&gt;Get this right and your deploys become trustworthy and repeatable. Get it wrong, especially the versioning, and you end up unable to answer the most basic question during an incident: "what is actually running in production right now?" This article makes sure you can always answer it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Junior engineers who've built a CI pipeline but treat the output as a black box. We focus on container images (the most common artifact today) but the principles, immutability, versioning, promote-don't-rebuild, apply to any artifact: a JAR, a zip, an npm package.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What is a build artifact?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;A build artifact is the packaged, ready-to-run output of your build, a single, self-contained unit that can be stored, versioned, and deployed without rebuilding from source.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your source code isn't directly runnable in production, it has to be compiled, bundled, and packaged first. The result of that packaging is the artifact. For a containerized app it's a Docker &lt;strong&gt;image&lt;/strong&gt;; for a Java app a &lt;code&gt;.jar&lt;/code&gt;; for a serverless function a &lt;code&gt;.zip&lt;/code&gt;. Whatever the format, the idea is the same: one frozen, deployable bundle.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🥕 Raw ingredients&lt;/td&gt;
&lt;td&gt;Source code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;👨‍🍳 Cooking&lt;/td&gt;
&lt;td&gt;The build step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🍱 A sealed, labeled meal&lt;/td&gt;
&lt;td&gt;The build artifact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🏷️ The expiry/batch label&lt;/td&gt;
&lt;td&gt;The version tag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🧊 The fridge it's stored in&lt;/td&gt;
&lt;td&gt;The registry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Source code is ingredients; the artifact is the sealed, labeled meal.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Immutability: the property that makes it trustworthy
&lt;/h2&gt;

&lt;p&gt;The single most important property of a good artifact is &lt;strong&gt;immutability&lt;/strong&gt;, once built, it never changes. You don't patch it, edit it, or rebuild it in place. If you need a change, you build a &lt;em&gt;new&lt;/em&gt; artifact with a &lt;em&gt;new&lt;/em&gt; version. The old one stays exactly as it was, forever.&lt;/p&gt;

&lt;p&gt;Why does this matter so much? Because immutability is what lets you reason about your system. If version &lt;code&gt;v1.4.2&lt;/code&gt; is running in production and you can pull that exact same &lt;code&gt;v1.4.2&lt;/code&gt; onto your laptop, you can debug precisely what users are hitting. If artifacts were mutable, if "the build" could quietly change underneath you, that guarantee evaporates, and you're back to "works on my machine." Immutability is also what makes rollback trivial: just redeploy the previous artifact, byte-for-byte.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Immutability is the same reason "build once, deploy many" works. The artifact you tested in staging is &lt;em&gt;provably&lt;/em&gt; the artifact you ship to prod, because it literally cannot have changed in between. Mutable artifacts break that proof.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Versioning and tags, and why &lt;code&gt;latest&lt;/code&gt; is a trap
&lt;/h2&gt;

&lt;p&gt;An artifact without a meaningful version is nearly useless, you can't tell which is which. Every artifact gets a &lt;strong&gt;tag&lt;/strong&gt; (a label) that identifies it. The quality of your tagging strategy directly determines whether you can answer "what's running in prod?"&lt;/p&gt;

&lt;p&gt;Docker's default tag, &lt;code&gt;latest&lt;/code&gt;, is the trap almost everyone falls into. &lt;code&gt;latest&lt;/code&gt; doesn't mean "the newest", it's just a label that &lt;em&gt;moves&lt;/em&gt; to whatever was pushed most recently. It points at different images over time, which destroys the one thing tags are for: knowing exactly what you have.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;latest&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Immutable tag (SHA / semver)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Points to&lt;/td&gt;
&lt;td&gt;Whatever was pushed last&lt;/td&gt;
&lt;td&gt;One specific, fixed image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reproducible deploy?&lt;/td&gt;
&lt;td&gt;No, meaning drifts&lt;/td&gt;
&lt;td&gt;Yes, always the same image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can you roll back to it?&lt;/td&gt;
&lt;td&gt;No, it already moved&lt;/td&gt;
&lt;td&gt;Yes, it never moves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"What's in prod?"&lt;/td&gt;
&lt;td&gt;Unanswerable&lt;/td&gt;
&lt;td&gt;The exact tag, every time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Why &lt;code&gt;latest&lt;/code&gt; fails at the one job a tag has.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Use tags that &lt;em&gt;pin&lt;/em&gt;. Two strategies, often combined: the &lt;strong&gt;Git commit SHA&lt;/strong&gt; (e.g. &lt;code&gt;app:9f2a1c7&lt;/code&gt;) ties an image to the exact code that built it, perfect for traceability, and &lt;strong&gt;semantic versioning&lt;/strong&gt; (e.g. &lt;code&gt;app:1.4.2&lt;/code&gt;) communicates intent to humans. A common pattern is to push both: the SHA for machines, the semver for releases.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;The &lt;code&gt;latest&lt;/code&gt; 2am story:&lt;/strong&gt; An incident hits. You check production: it's running &lt;code&gt;myapp:latest&lt;/code&gt;. Which commit is that? Nobody knows, &lt;code&gt;latest&lt;/code&gt; has been re-pushed a dozen times since. You can't reproduce it, can't roll back cleanly, and can't even be sure two servers are running the same image. This is why teams ban &lt;code&gt;latest&lt;/code&gt; in production deploys.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What a registry is and how it works
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;registry&lt;/strong&gt; is the storage service where artifacts live, a versioned warehouse your CI pushes to and your deploy targets pull from. For container images, common registries are Docker Hub, GitHub Container Registry (GHCR), AWS ECR, Google Artifact Registry, and Azure ACR. They all speak the same protocol; the workflow is identical.&lt;/p&gt;

&lt;p&gt;The flow is a simple loop, and it's the connective tissue between your pipeline and your running app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CI builds the artifact&lt;/strong&gt;: Your pipeline builds the image and tags it with the commit SHA (and maybe a version).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI pushes to the registry&lt;/strong&gt;: The tagged image is uploaded to the registry, now it's stored, versioned, and shareable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy pulls from the registry&lt;/strong&gt;: Staging pulls that exact tag and runs it. After verification, prod pulls the same tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback re-pulls an old tag&lt;/strong&gt;: Need to revert? Re-deploy a previous tag from the registry. It's still there, unchanged.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Registries are also a security boundary. Use a private registry for anything proprietary, scan images for vulnerabilities on push (most registries do this), and lock down who can push. A poisoned image in a registry deploys straight to production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Build, tag, and push for real
&lt;/h2&gt;

&lt;p&gt;Here's the whole loop in actual commands. This is what your CI runs under the hood; running it by hand once makes it click.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;build-and-push.sh&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Use the Git commit SHA as an immutable tag&lt;/span&gt;
&lt;span class="nv"&gt;TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--short&lt;/span&gt; HEAD&lt;span class="si"&gt;)&lt;/span&gt;      &lt;span class="c"&gt;# e.g. 9f2a1c7&lt;/span&gt;
&lt;span class="nv"&gt;REGISTRY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ghcr.io/your-org
&lt;span class="nv"&gt;IMAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGISTRY&lt;/span&gt;/myapp

&lt;span class="c"&gt;# 1. Build, tagging with the immutable SHA&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nv"&gt;$IMAGE&lt;/span&gt;:&lt;span class="nv"&gt;$TAG&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# 2. Also tag it as a human-readable version (optional)&lt;/span&gt;
docker tag &lt;span class="nv"&gt;$IMAGE&lt;/span&gt;:&lt;span class="nv"&gt;$TAG&lt;/span&gt; &lt;span class="nv"&gt;$IMAGE&lt;/span&gt;:1.4.2

&lt;span class="c"&gt;# 3. Log in to the registry (token from a secret, never hardcoded)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REGISTRY_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | docker login ghcr.io &lt;span class="nt"&gt;-u&lt;/span&gt; your-user &lt;span class="nt"&gt;--password-stdin&lt;/span&gt;

&lt;span class="c"&gt;# 4. Push BOTH tags, they point at the same image bytes&lt;/span&gt;
docker push &lt;span class="nv"&gt;$IMAGE&lt;/span&gt;:&lt;span class="nv"&gt;$TAG&lt;/span&gt;
docker push &lt;span class="nv"&gt;$IMAGE&lt;/span&gt;:1.4.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;docker tag&lt;/code&gt; doesn't copy anything, it just adds a second name pointing at the same underlying image, so pushing both tags is cheap. Later, your deploy step pulls the specific tag and runs it:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;deploy.sh&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Pull and run the EXACT immutable tag, never `latest`&lt;/span&gt;
docker pull ghcr.io/your-org/myapp:9f2a1c7
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 ghcr.io/your-org/myapp:9f2a1c7

&lt;span class="c"&gt;# Confirm exactly what's running (answers "what's in prod?")&lt;/span&gt;
docker ps &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'table {{.Image}}\t{{.Status}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the tag is the commit SHA, that last command doesn't just tell you the image is running, it tells you the &lt;em&gt;exact line of source code&lt;/em&gt; in production. That traceability, from a running container all the way back to a Git commit, is the entire point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build once, promote many
&lt;/h2&gt;

&lt;p&gt;This is the pattern everything in this article serves. You build the artifact &lt;strong&gt;once&lt;/strong&gt;, push it to the registry, and then &lt;em&gt;promote that same artifact&lt;/em&gt; through your environments, you never rebuild for staging and rebuild again for prod.&lt;/p&gt;

&lt;p&gt;It works precisely &lt;em&gt;because&lt;/em&gt; artifacts are immutable and registries store them: staging pulls &lt;code&gt;myapp:9f2a1c7&lt;/code&gt;, you verify it, and then prod pulls the byte-identical &lt;code&gt;myapp:9f2a1c7&lt;/code&gt;. The thing you tested is, provably, the thing you shipped. Rebuilding per environment throws that guarantee away, a dependency could shift between builds and you'd ship something subtly different from what you tested. We cover this from the pipeline angle in &lt;a href="https://thesimplifiedtech.com/blog/cicd-fundamentals-what-a-pipeline-does" rel="noopener noreferrer"&gt;CI/CD Fundamentals&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Warning:&lt;/strong&gt; If your deploy step contains a &lt;code&gt;docker build&lt;/code&gt; for production, you're rebuilding, not promoting, and you've lost the guarantee that prod matches what you tested. Production deploys should only ever &lt;code&gt;pull&lt;/code&gt; an existing, tested tag from the registry.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deploying &lt;code&gt;latest&lt;/code&gt; to production.&lt;/strong&gt; It's a moving label, not a version. You can't reproduce it, roll back to it, or even know what it is. Ban it from prod deploys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating artifacts as mutable.&lt;/strong&gt; Never patch a built artifact in place. Build a new version. Immutability is what makes rollback and debugging possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuilding per environment.&lt;/strong&gt; Build once, push, promote the same tag. A &lt;code&gt;docker build&lt;/code&gt; in your prod deploy means prod isn't what you tested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No traceability from artifact to commit.&lt;/strong&gt; Tag with the commit SHA so you can always trace a running container back to its exact source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoding registry credentials.&lt;/strong&gt; Registry tokens are secrets, inject them from your CI's secret store and pipe to &lt;code&gt;--password-stdin&lt;/code&gt;, never put them in a script or Git.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never cleaning up old tags.&lt;/strong&gt; Registries fill up and cost money. Set a retention policy, but keep enough history that you can always roll back.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An artifact is the packaged, ready-to-run output of your build (often a container image).&lt;/li&gt;
&lt;li&gt;Immutability is the key property: once built, it never changes, you build a new version instead.&lt;/li&gt;
&lt;li&gt;Tag with something that pins: the Git commit SHA and/or a semantic version.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;latest&lt;/code&gt; is a trap, it moves over time, so it can't reproduce, roll back, or identify a build.&lt;/li&gt;
&lt;li&gt;A registry stores artifacts: CI pushes, deploys pull, rollback re-pulls an old tag.&lt;/li&gt;
&lt;li&gt;Tag with the commit SHA for full traceability from a running container back to source.&lt;/li&gt;
&lt;li&gt;Build once, push, and promote the same tag through every environment, never rebuild for prod.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;You know what artifacts are and where they live. Next, make sure the images you push are small and secure, see how this fits the wider pipeline, and practice the commands hands-on.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/dockerfile-best-practices" rel="noopener noreferrer"&gt;Dockerfile Best Practices&lt;/a&gt;, the artifacts you push should be small, fast, and secure.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/cicd-fundamentals-what-a-pipeline-does" rel="noopener noreferrer"&gt;CI/CD Fundamentals&lt;/a&gt;, where the build, push, and promote steps sit in the bigger flow.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/labs/docker" rel="noopener noreferrer"&gt;Practice in the Docker lab&lt;/a&gt;, build, tag, and push images in an in-browser terminal.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/artifacts-and-registries" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>docker</category>
      <category>artifacts</category>
    </item>
    <item>
      <title>Environments &amp; Config: Dev / Staging / Prod Done Right</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Sat, 25 Jul 2026 11:00:25 +0000</pubDate>
      <link>https://dev.to/sri2614/environments-config-dev-staging-prod-done-right-4fh6</link>
      <guid>https://dev.to/sri2614/environments-config-dev-staging-prod-done-right-4fh6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Kill the "it worked in staging" disaster: keep environments at parity, push &lt;strong&gt;config into the environment&lt;/strong&gt; not the code, separate secrets from config, and promote one identical artifact through every stage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The four most expensive words in software&lt;/li&gt;
&lt;li&gt;Why separate environments at all?&lt;/li&gt;
&lt;li&gt;Environment parity: keep the rungs close&lt;/li&gt;
&lt;li&gt;The core rule: config lives in the environment, not the code&lt;/li&gt;
&lt;li&gt;Injecting config: env files and the real thing&lt;/li&gt;
&lt;li&gt;Secrets vs config: a critical distinction&lt;/li&gt;
&lt;li&gt;Promote one artifact, change only the config&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The four most expensive words in software
&lt;/h2&gt;

&lt;p&gt;"But it worked in staging." Those four words have preceded more outages than almost anything else in this field. You test a change, it passes everywhere you can see, you ship it, and it falls over in production for reasons that seem to make no sense. Nearly always, the cause isn't the code. It's a difference between environments, or a piece of configuration that was baked into the wrong place.&lt;/p&gt;

&lt;p&gt;Environments and configuration are unglamorous, but getting them right is what separates teams that ship calmly from teams that fear every deploy. The principles are simple and they pay off immediately. Let's build the model.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Junior engineers who can deploy an app but have hardcoded a database URL at least once and felt that something was off. No prior infra knowledge needed. The patterns here apply whether you deploy to a VM, a container platform, or a serverless runtime.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why separate environments at all?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;An environment is a complete, isolated copy of the place your software runs, its own servers, its own database, its own config, so you can test changes somewhere that isn't where your users live.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you only had one environment, every experiment would happen on your users. A bad migration would corrupt real data; a half-finished feature would be visible to everyone. So teams keep a ladder of environments, each closer to production, so a change has to survive several gates before it touches a real customer.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Who uses it&lt;/th&gt;
&lt;th&gt;Data&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Development&lt;/td&gt;
&lt;td&gt;Build &amp;amp; experiment, break things freely&lt;/td&gt;
&lt;td&gt;Engineers&lt;/td&gt;
&lt;td&gt;Fake / seed data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staging&lt;/td&gt;
&lt;td&gt;Final rehearsal, a production look-alike&lt;/td&gt;
&lt;td&gt;QA, the team&lt;/td&gt;
&lt;td&gt;Realistic, anonymized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;td&gt;The real thing, real users&lt;/td&gt;
&lt;td&gt;Customers&lt;/td&gt;
&lt;td&gt;Real, sensitive data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The classic three-rung ladder. Some teams add more (QA, UAT), but these three are the backbone.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Think of it like a stage play. &lt;strong&gt;Dev&lt;/strong&gt; is rehearsing lines in your living room. &lt;strong&gt;Staging&lt;/strong&gt; is the full dress rehearsal on the real stage with costumes and lighting. &lt;strong&gt;Production&lt;/strong&gt; is opening night with a paying audience. The whole point of dress rehearsal is that it matches opening night closely enough to catch problems before the audience does, which brings us to the most important concept here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment parity: keep the rungs close
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Environment parity&lt;/strong&gt; means your environments are as similar as possible. The bigger the gap between staging and production, the more bugs slip through staging undetected, because you tested under conditions that don't match reality. "It worked in staging" almost always means "staging and prod weren't actually the same."&lt;/p&gt;

&lt;p&gt;Classic parity gaps that bite teams: staging runs an older database version than prod; staging has one server while prod has ten behind a load balancer; staging uses SQLite while prod uses Postgres. Each difference is a place where a bug can hide. You'll never get perfect parity (prod has real traffic and real data scale), but every gap you close is a class of bug you eliminate.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Containers and infrastructure-as-code are the best parity tools there are. If staging and prod are built from the same Docker image and the same Terraform, the environments are identical by construction, not by someone remembering to keep them in sync. This is a major reason teams adopt them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The core rule: config lives in the environment, not the code
&lt;/h2&gt;

&lt;p&gt;Here's the principle that fixes most config pain, straight from the &lt;a href="https://thesimplifiedtech.com/blog/twelve-factor-app-cloud-engineers" rel="noopener noreferrer"&gt;Twelve-Factor App&lt;/a&gt; methodology: &lt;strong&gt;anything that differs between environments must live in the environment, not in your code.&lt;/strong&gt; Your database URL, API keys, feature flags, log levels, none of that belongs hardcoded in your source.&lt;/p&gt;

&lt;p&gt;The litmus test is simple: could you make your codebase open-source right now without leaking any secrets or breaking any environment? If the answer is no because there's a production password or a staging URL sitting in a source file, your config is in the wrong place.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;config.bad.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ BAD: config hardcoded in the code.&lt;/span&gt;
&lt;span class="c1"&gt;// Different per environment, secrets in source control,&lt;/span&gt;
&lt;span class="c1"&gt;// and you'd have to edit code to change environments.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;databaseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postgres://admin:hunter2@prod-db:5432/app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sk_live_abc123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;logLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;debug&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;config.good.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ GOOD: config read from environment variables.&lt;/span&gt;
&lt;span class="c1"&gt;// The SAME code runs in every environment; only the&lt;/span&gt;
&lt;span class="c1"&gt;// values injected from outside change.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Missing required env var: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;databaseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;logLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LOG_LEVEL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;info&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The good version reads everything from environment variables and fails loudly at startup if a required one is missing, so a misconfiguration crashes immediately on boot, not silently at 2am when the first request hits the missing setting. Note the sensible default for &lt;code&gt;LOG_LEVEL&lt;/code&gt;: non-secret config can have defaults; secrets never should.&lt;/p&gt;

&lt;h2&gt;
  
  
  Injecting config: env files and the real thing
&lt;/h2&gt;

&lt;p&gt;Locally, a &lt;code&gt;.env&lt;/code&gt; file (loaded by your framework or a library like dotenv) is the friendly way to set environment variables. Crucially, this file is &lt;strong&gt;never committed&lt;/strong&gt;, you commit a &lt;code&gt;.env.example&lt;/code&gt; template with the keys but no values, so teammates know what to set.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.env.example (committed, no real values)&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Copy to .env and fill in. .env itself is gitignored.&lt;/span&gt;
&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres://user:pass@localhost:5432/app_dev
&lt;span class="nv"&gt;API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-dev-key-here
&lt;span class="nv"&gt;LOG_LEVEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;debug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In real environments you don't use &lt;code&gt;.env&lt;/code&gt; files at all, the platform injects the variables. In a container platform or CI you set them as environment config; in Kubernetes they come from ConfigMaps (non-secret) and Secrets. The application code doesn't care where the values come from, it just reads &lt;code&gt;process.env&lt;/code&gt;. That's the whole elegance of the pattern: one code path, many environments.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;deploy.yml (excerpt, values come from the platform)&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;env&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;DATABASE_URL&lt;/span&gt;
    &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;secretKeyRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;       &lt;span class="c1"&gt;# a secret, stored encrypted&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;app-secrets&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;database-url&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;LOG_LEVEL&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;info"&lt;/span&gt;         &lt;span class="c1"&gt;# plain config, fine in the manifest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Secrets vs config: a critical distinction
&lt;/h2&gt;

&lt;p&gt;Both come from the environment, but they are &lt;em&gt;not&lt;/em&gt; the same and must be handled differently. &lt;strong&gt;Config&lt;/strong&gt; is non-sensitive (log level, feature flags, a region name). &lt;strong&gt;Secrets&lt;/strong&gt; are sensitive (passwords, API keys, tokens), if they leak, you have an incident.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Secrets&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Examples&lt;/td&gt;
&lt;td&gt;LOG_LEVEL, REGION, feature flags&lt;/td&gt;
&lt;td&gt;DB password, API keys, tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sensitive?&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes, leaking = incident&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can have a default?&lt;/td&gt;
&lt;td&gt;Often&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stored in&lt;/td&gt;
&lt;td&gt;ConfigMap / plain env&lt;/td&gt;
&lt;td&gt;Secret manager (encrypted)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safe to log?&lt;/td&gt;
&lt;td&gt;Usually&lt;/td&gt;
&lt;td&gt;Never log them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Treat them differently even though both are injected as environment variables.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Never commit a secret:&lt;/strong&gt; A secret pushed to Git is compromised the moment it's pushed, even if you delete it in the next commit, it lives in history forever, and bots scan public repos for keys within minutes. If it happens: rotate the secret immediately (assume it's burned), then clean history. Prevention beats cleanup: gitignore &lt;code&gt;.env&lt;/code&gt;, and use a secret scanner in CI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Promote one artifact, change only the config
&lt;/h2&gt;

&lt;p&gt;Now the payoff that ties it all together. Because your code reads config from the environment, &lt;strong&gt;the exact same build artifact can run in dev, staging, and prod&lt;/strong&gt;, you don't rebuild per environment. You build once, then promote that identical artifact up the ladder, injecting different config at each rung.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build once&lt;/strong&gt;: CI builds a single immutable artifact (e.g. a container image tagged with the commit SHA). No environment-specific code is baked in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy to staging with staging config&lt;/strong&gt;: Run that exact image in staging, injecting STAGING database URL, keys, and flags from the environment. Verify it works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promote the same image to prod&lt;/strong&gt;: Deploy the byte-for-byte identical image to production, injecting PROD config. Nothing about the code changed, only the values around it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is why "config in the environment" and "build once, deploy many" are two sides of the same coin. If config were baked into the code, you'd need a different build per environment, and you could never be sure the prod build matched the one you tested. Keep config outside, and the thing you tested in staging &lt;em&gt;is&lt;/em&gt; the thing that ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoding config in source.&lt;/strong&gt; A database URL or key in code means editing and rebuilding to change environments, and leaking secrets into version control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Committing the &lt;code&gt;.env&lt;/code&gt; file.&lt;/strong&gt; Commit &lt;code&gt;.env.example&lt;/code&gt; with empty values; gitignore the real &lt;code&gt;.env&lt;/code&gt;. A committed &lt;code&gt;.env&lt;/code&gt; is a leaked secret.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Big parity gaps.&lt;/strong&gt; If staging runs a different database, version, or scale than prod, staging stops predicting prod and "it worked in staging" becomes a recurring headline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating secrets like config.&lt;/strong&gt; Secrets need encrypted storage, no defaults, and must never be logged. Don't drop a production password into a plain config file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuilding per environment.&lt;/strong&gt; Build one artifact and promote it. Rebuilding for prod means you shipped something you never actually tested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent missing config.&lt;/strong&gt; Read required vars at startup and crash loudly if they're absent, far better than a vague failure deep in a request hours later.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Environments (dev/staging/prod) are isolated copies so you don't experiment on users.&lt;/li&gt;
&lt;li&gt;Environment parity is the goal, the closer staging is to prod, the fewer surprises.&lt;/li&gt;
&lt;li&gt;Keep config in the environment, not in the code (the 12-factor rule).&lt;/li&gt;
&lt;li&gt;Use .env locally (never committed); let the platform inject config in real environments.&lt;/li&gt;
&lt;li&gt;Config vs secrets are different: secrets get encrypted storage, no defaults, no logging.&lt;/li&gt;
&lt;li&gt;Validate required config at startup and fail loud and early.&lt;/li&gt;
&lt;li&gt;Build the artifact once and promote it up the ladder, changing only the config.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Config and environments are part of a larger discipline for building cloud-native apps. Go deeper on the methodology, wire config into your pipeline, and see the full DevOps track.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/twelve-factor-app-cloud-engineers" rel="noopener noreferrer"&gt;The Twelve-Factor App for Cloud Engineers&lt;/a&gt;, config is factor III; here are the other eleven.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/your-first-ci-pipeline-github-actions" rel="noopener noreferrer"&gt;Your First CI Pipeline with GitHub Actions&lt;/a&gt;, inject secrets into your pipeline the right way.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/career-paths/devops-engineer" rel="noopener noreferrer"&gt;Explore the DevOps Engineer path&lt;/a&gt;, the full guided track.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/environments-and-config-dev-staging-prod" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>environments</category>
      <category>config</category>
      <category>devops</category>
    </item>
    <item>
      <title>Dockerfile Best Practices: Small, Fast, Secure Images</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Fri, 24 Jul 2026 11:01:16 +0000</pubDate>
      <link>https://dev.to/sri2614/dockerfile-best-practices-small-fast-secure-images-4mfm</link>
      <guid>https://dev.to/sri2614/dockerfile-best-practices-small-fast-secure-images-4mfm</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Turn a 1.2 GB root-running &lt;strong&gt;Dockerfile&lt;/strong&gt; into an 80 MB image that rebuilds in seconds: pinned slim base, caching-friendly layer order, .dockerignore, multi-stage builds, and a non-root user. Smaller, faster, safer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Your first Dockerfile works, and it's terrible&lt;/li&gt;
&lt;li&gt;The mental model: an image is a stack of layers&lt;/li&gt;
&lt;li&gt;The bad Dockerfile&lt;/li&gt;
&lt;li&gt;Fix 1, pin a slim base image&lt;/li&gt;
&lt;li&gt;Fix 2, order layers for caching&lt;/li&gt;
&lt;li&gt;Fix 3, add a .dockerignore&lt;/li&gt;
&lt;li&gt;Fix 4, multi-stage build + non-root user&lt;/li&gt;
&lt;li&gt;The payoff, measured&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Your first Dockerfile works, and it's terrible
&lt;/h2&gt;

&lt;p&gt;Almost everyone's first Dockerfile follows the same pattern: start from a full OS image, copy everything in, install dependencies, run the app. It works, the container starts and serves traffic. Then reality arrives: the image is over a gigabyte, every tiny code change triggers a five-minute rebuild, a security scan lights up red, and it runs as root. None of that shows up in a tutorial; all of it shows up in production.&lt;/p&gt;

&lt;p&gt;The good news: fixing it comes down to about five techniques, and once you understand &lt;em&gt;why&lt;/em&gt; each works, you'll write good Dockerfiles by reflex. This article takes one bad Dockerfile and fixes it into a good one, explaining every change.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Anyone who can write a basic Dockerfile and run &lt;code&gt;docker build&lt;/code&gt; but whose images are big, slow, or flagged by security tooling. We use a Node.js app as the example; the principles (caching, multi-stage, non-root, slim base) apply to Python, Go, Java, any language.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The mental model: an image is a stack of layers
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;A Docker image is a stack of read-only layers, one per instruction in your Dockerfile. Docker caches each layer and rebuilds a layer (and everything above it) only when that instruction's inputs change.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This one fact explains almost every best practice. Each &lt;code&gt;RUN&lt;/code&gt;, &lt;code&gt;COPY&lt;/code&gt;, and &lt;code&gt;FROM&lt;/code&gt; creates a layer. On a rebuild, Docker walks down your Dockerfile reusing cached layers until it hits the first instruction whose inputs changed, then it rebuilds that layer and every layer below it. So the &lt;em&gt;order&lt;/em&gt; of your instructions directly controls how much gets rebuilt.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📄 A stack of tracing sheets&lt;/td&gt;
&lt;td&gt;The image's layers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✏️ Redraw one sheet → redraw all above it&lt;/td&gt;
&lt;td&gt;Change one layer → rebuild all later layers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🗂️ Put rarely-changed sheets at the bottom&lt;/td&gt;
&lt;td&gt;Put rarely-changed steps early in the Dockerfile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🚪 Leave the building tools outside the house&lt;/td&gt;
&lt;td&gt;Multi-stage: don't ship the build toolchain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Think of layers like a stack of transparent sheets, change one sheet and you redraw it and everything stacked on top.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bad Dockerfile
&lt;/h2&gt;

&lt;p&gt;Here's the version almost everyone writes first. It runs, and it does almost everything wrong. Read it, then we'll diagnose each problem.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Dockerfile (bad)&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node                 # untagged → unpredictable, and huge&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .                  # copies EVERYTHING, before installing deps&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;           &lt;span class="c"&gt;# cache busts on any file change&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; npm start             # runs as root&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;FROM node&lt;/code&gt;&lt;/strong&gt;, untagged, so "node" means whatever the latest happens to be today. Builds aren't reproducible, and the full &lt;code&gt;node&lt;/code&gt; image is ~1 GB of OS and tooling you don't need at runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;COPY . .&lt;/code&gt; before &lt;code&gt;npm install&lt;/code&gt;&lt;/strong&gt;, copies your entire source first, so &lt;em&gt;any&lt;/em&gt; code change invalidates the cache for the install step. You reinstall every dependency on every one-line edit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;.dockerignore&lt;/code&gt;&lt;/strong&gt;, &lt;code&gt;COPY . .&lt;/code&gt; drags in &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;.git&lt;/code&gt;, logs, and secrets, bloating the image and risking leaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No multi-stage build&lt;/strong&gt;, the final image carries the full toolchain, dev dependencies, and source instead of just what's needed to run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runs as root&lt;/strong&gt;, the default. If the app is compromised, the attacker is root inside the container, which is a real escalation risk.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fix 1, pin a slim base image
&lt;/h2&gt;

&lt;p&gt;Two changes, big payoff. &lt;strong&gt;Pin&lt;/strong&gt; an explicit version so builds are reproducible, and pick a &lt;strong&gt;slim&lt;/strong&gt; variant so you're not shipping a gigabyte of unused OS. &lt;code&gt;node:20-slim&lt;/code&gt; is a Debian-slim base; &lt;code&gt;node:20-alpine&lt;/code&gt; is even smaller (Alpine Linux). Alpine is tiny but uses musl libc, which occasionally trips up native modules, &lt;code&gt;slim&lt;/code&gt; is the safe default; reach for &lt;code&gt;alpine&lt;/code&gt; when you've verified your deps work on it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Dockerfile (excerpt)&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Pinned + slim: reproducible and ~10x smaller than `node`&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-slim&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Never ship &lt;code&gt;latest&lt;/code&gt;:&lt;/strong&gt; &lt;code&gt;FROM node&lt;/code&gt; (or &lt;code&gt;FROM node:latest&lt;/code&gt;) means your build depends on whatever Docker Hub serves that day. A new major version can break your build with zero changes on your side. Pin to a specific version, ideally a digest (&lt;code&gt;node:20-slim@sha256:...&lt;/code&gt;) for fully immutable builds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Fix 2, order layers for caching
&lt;/h2&gt;

&lt;p&gt;This is the single highest-impact change. Your dependencies change rarely; your source code changes constantly. So copy the dependency manifest and install &lt;em&gt;first&lt;/em&gt;, then copy the source. Now an edit to your code reuses the cached dependency layer, installs become instant.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Dockerfile (excerpt)&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# 1. Copy ONLY the manifest first, changes rarely&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json package-lock.json ./&lt;/span&gt;

&lt;span class="c"&gt;# 2. Install, this layer is cached until the manifest changes&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci

&lt;span class="c"&gt;# 3. NOW copy source, changes often, but doesn't bust the install&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the comments as a rule you can apply in any language: &lt;strong&gt;copy the thing that changes least, first.&lt;/strong&gt; In Python it's &lt;code&gt;requirements.txt&lt;/code&gt; before your code; in Go it's &lt;code&gt;go.mod&lt;/code&gt;/&lt;code&gt;go.sum&lt;/code&gt;. Same principle, same dramatic speedup on every rebuild after the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3, add a .dockerignore
&lt;/h2&gt;

&lt;p&gt;Even with good ordering, &lt;code&gt;COPY . .&lt;/code&gt; will happily copy &lt;code&gt;node_modules&lt;/code&gt;, your &lt;code&gt;.git&lt;/code&gt; history, local env files, and logs into the image. A &lt;code&gt;.dockerignore&lt;/code&gt; (same syntax as &lt;code&gt;.gitignore&lt;/code&gt;) keeps them out, smaller image, faster builds, and no accidentally-baked-in secrets.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.dockerignore&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node_modules
npm-debug.log
.git
.gitignore
.env
.env.&lt;span class="k"&gt;*&lt;/span&gt;
Dockerfile
.dockerignore
coverage
dist
&lt;span class="k"&gt;*&lt;/span&gt;.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Always ignore &lt;code&gt;node_modules&lt;/code&gt; (you reinstall it inside the image anyway), &lt;code&gt;.git&lt;/code&gt;, and any &lt;code&gt;.env&lt;/code&gt; files. Copying a local &lt;code&gt;.env&lt;/code&gt; into an image is one of the most common ways secrets accidentally end up published in a registry.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Fix 4, multi-stage build + non-root user
&lt;/h2&gt;

&lt;p&gt;The big two. A &lt;strong&gt;multi-stage build&lt;/strong&gt; uses one stage to build (with all the dev tooling) and a second, clean stage that copies in &lt;em&gt;only&lt;/em&gt; the finished artifact, so the toolchain never ships. And switching to a &lt;strong&gt;non-root user&lt;/strong&gt; means a compromised app isn't a root shell. Here's the full, good Dockerfile:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Dockerfile (good)&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ---- Stage 1: build ----&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json package-lock.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci                      &lt;span class="c"&gt;# includes dev deps for the build&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build               &lt;span class="c"&gt;# produce ./dist&lt;/span&gt;

&lt;span class="c"&gt;# ---- Stage 2: runtime ----&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runtime&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NODE_ENV=production&lt;/span&gt;

&lt;span class="c"&gt;# Install ONLY production deps&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json package-lock.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm cache clean &lt;span class="nt"&gt;--force&lt;/span&gt;

&lt;span class="c"&gt;# Copy just the built output from the build stage&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/dist ./dist&lt;/span&gt;

&lt;span class="c"&gt;# Run as a non-root user (the node image ships one called 'node')&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; node&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trace what the final image actually contains: the slim base, production dependencies only, and the built &lt;code&gt;./dist&lt;/code&gt;, nothing else. The compiler, dev dependencies, and source code all stayed in the &lt;code&gt;build&lt;/code&gt; stage and were thrown away. The &lt;code&gt;USER node&lt;/code&gt; line drops privileges. And &lt;code&gt;CMD&lt;/code&gt; uses the array form (&lt;code&gt;["node", "dist/server.js"]&lt;/code&gt;), which runs the process directly instead of wrapping it in a shell, so signals like graceful-shutdown reach your app correctly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; The official &lt;code&gt;node&lt;/code&gt; images already include a non-root user named &lt;code&gt;node&lt;/code&gt;. For other bases, create one: &lt;code&gt;RUN useradd -m appuser &amp;amp;&amp;amp; USER appuser&lt;/code&gt;. The rule is simply: never let the default (root) run your app.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The payoff, measured
&lt;/h2&gt;

&lt;p&gt;These aren't abstract improvements. Here's the before/after for a typical Node service. Your exact numbers vary, but the shape is always this dramatic.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Bad Dockerfile&lt;/th&gt;
&lt;th&gt;Good Dockerfile&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Final image size&lt;/td&gt;
&lt;td&gt;~1.1 GB&lt;/td&gt;
&lt;td&gt;~180 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rebuild after a code edit&lt;/td&gt;
&lt;td&gt;~4-5 min (reinstalls deps)&lt;/td&gt;
&lt;td&gt;~10 sec (cache hit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ships build toolchain?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (multi-stage)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs as&lt;/td&gt;
&lt;td&gt;root&lt;/td&gt;
&lt;td&gt;non-root (node)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reproducible build?&lt;/td&gt;
&lt;td&gt;No (untagged base)&lt;/td&gt;
&lt;td&gt;Yes (pinned base)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Same app, bad Dockerfile vs. good. The rebuild and security wins matter on every single commit.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Smaller images push and pull faster (cheaper CI, faster deploys), the cache turns multi-minute rebuilds into seconds, and the non-root + slim base shrinks your attack surface. Every one of these compounds across a team shipping dozens of times a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Untagged base images.&lt;/strong&gt; &lt;code&gt;FROM node&lt;/code&gt; is a time bomb, pin a version so a new release can't silently break your build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;COPY . .&lt;/code&gt; before installing deps.&lt;/strong&gt; This single ordering mistake makes every code change reinstall every dependency. Copy the manifest and install first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;.dockerignore&lt;/code&gt;.&lt;/strong&gt; Without it you bloat the image and risk copying &lt;code&gt;.env&lt;/code&gt; files and &lt;code&gt;.git&lt;/code&gt; straight into a published image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shipping the build toolchain.&lt;/strong&gt; If your runtime image contains a compiler and dev dependencies, you skipped multi-stage. Build in one stage, copy the artifact into a clean one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running as root.&lt;/strong&gt; The default, and a real risk. Add a &lt;code&gt;USER&lt;/code&gt; line so a compromised app isn't a privileged one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shell-form CMD.&lt;/strong&gt; &lt;code&gt;CMD npm start&lt;/code&gt; wraps your app in a shell that can swallow shutdown signals. Use the array form so signals reach your process.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Good Dockerfiles in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An image is a stack of cached layers, order instructions least-changed first.&lt;/li&gt;
&lt;li&gt;Pin a slim base (node:20-slim), never &lt;code&gt;latest&lt;/code&gt; or untagged.&lt;/li&gt;
&lt;li&gt;Copy the dependency manifest and install BEFORE copying source, to keep the cache.&lt;/li&gt;
&lt;li&gt;Add a .dockerignore to keep node_modules, .git, and .env out of the image.&lt;/li&gt;
&lt;li&gt;Use a multi-stage build so the toolchain never ships in the final image.&lt;/li&gt;
&lt;li&gt;Add a USER line, never run your app as root.&lt;/li&gt;
&lt;li&gt;Use array-form CMD so shutdown signals reach your process.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;You can build a small, fast, secure image. Next, see where containers fit in the bigger picture, practice the commands hands-on, and wire your image build into CI.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/docker-vs-kubernetes-when-to-use-each" rel="noopener noreferrer"&gt;Docker vs Kubernetes: When to Use Each&lt;/a&gt;, once you have great images, where do they run?&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/labs/docker" rel="noopener noreferrer"&gt;Practice in the Docker lab&lt;/a&gt;, build, tag, and inspect images in an in-browser terminal.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/your-first-ci-pipeline-github-actions" rel="noopener noreferrer"&gt;Your First CI Pipeline with GitHub Actions&lt;/a&gt;, build and push this image automatically on every commit.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/dockerfile-best-practices" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>containers</category>
      <category>devops</category>
      <category>bestpractices</category>
    </item>
    <item>
      <title>Your First CI Pipeline with GitHub Actions</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Thu, 23 Jul 2026 11:00:26 +0000</pubDate>
      <link>https://dev.to/sri2614/your-first-ci-pipeline-with-github-actions-3od2</link>
      <guid>https://dev.to/sri2614/your-first-ci-pipeline-with-github-actions-3od2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Build a real &lt;strong&gt;GitHub Actions&lt;/strong&gt; CI pipeline that installs, lints, and tests on every push, with caching and parallel jobs, then make it a required status check so broken code can't merge. Copy-paste YAML included.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;From theory to a green checkmark&lt;/li&gt;
&lt;li&gt;The vocabulary you need (just five words)&lt;/li&gt;
&lt;li&gt;Step 1, the smallest workflow that does something&lt;/li&gt;
&lt;li&gt;Step 2, install, lint, and test&lt;/li&gt;
&lt;li&gt;Step 3, make it fast with caching&lt;/li&gt;
&lt;li&gt;Step 4, run independent jobs in parallel&lt;/li&gt;
&lt;li&gt;Step 5, make the pipeline a required status check&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  From theory to a green checkmark
&lt;/h2&gt;

&lt;p&gt;You know &lt;em&gt;what&lt;/em&gt; a pipeline does, now let's build one. By the end of this article you'll have a working CI pipeline that runs on every push and every pull request, installs your dependencies, lints your code, runs your tests, and shows a green checkmark (or a red X) right in the GitHub UI. It's free, it ships with every GitHub repo, and you can have it running in ten minutes.&lt;/p&gt;

&lt;p&gt;We'll build it up one piece at a time so nothing is a black box. Each code block is a real, working &lt;code&gt;.github/workflows/*.yml&lt;/code&gt; file, copy them straight into a repo.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Anyone with a GitHub repo and basic Git who's never written a workflow file. The examples use a Node.js project (npm), but the structure is identical for Python, Go, or anything else, only the install/test commands change. If you've read &lt;a href="https://thesimplifiedtech.com/blog/cicd-fundamentals-what-a-pipeline-does" rel="noopener noreferrer"&gt;CI/CD Fundamentals&lt;/a&gt;, you have all the background you need.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The vocabulary you need (just five words)
&lt;/h2&gt;

&lt;p&gt;GitHub Actions has its own nouns. Learn these five and the YAML stops looking cryptic. They nest inside each other like Russian dolls.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Workflow&lt;/td&gt;
&lt;td&gt;One .yml file in .github/workflows/. The whole automated process.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event&lt;/td&gt;
&lt;td&gt;What triggers the workflow, a push, a pull_request, a schedule.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job&lt;/td&gt;
&lt;td&gt;A group of steps that run together on one machine. Jobs can run in parallel.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Step&lt;/td&gt;
&lt;td&gt;A single task in a job, run a command, or use a prebuilt action.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runner&lt;/td&gt;
&lt;td&gt;The machine a job runs on, e.g. ubuntu-latest, fresh every run.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The five building blocks, biggest to smallest.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Put together: a &lt;strong&gt;workflow&lt;/strong&gt; listens for an &lt;strong&gt;event&lt;/strong&gt;, which kicks off one or more &lt;strong&gt;jobs&lt;/strong&gt;, each running its &lt;strong&gt;steps&lt;/strong&gt; on a &lt;strong&gt;runner&lt;/strong&gt;. That's the entire model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1, the smallest workflow that does something
&lt;/h2&gt;

&lt;p&gt;Create the file &lt;code&gt;.github/workflows/ci.yml&lt;/code&gt; in your repo. The folder path matters exactly, GitHub only looks in &lt;code&gt;.github/workflows/&lt;/code&gt;. Start with the absolute minimum: check out the code and print a line.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;                      &lt;span class="c1"&gt;# shows up in the Actions tab&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;      &lt;span class="c1"&gt;# the events that trigger it&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;hello&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                       &lt;span class="c1"&gt;# job id (any name)&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;     &lt;span class="c1"&gt;# the runner&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;   &lt;span class="c1"&gt;# pull your repo onto the runner&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "Pipeline is alive!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit and push it. Open the &lt;strong&gt;Actions&lt;/strong&gt; tab on GitHub and you'll see the run executing live. That's a working pipeline, it just doesn't do anything useful yet. Two things worth understanding now: &lt;code&gt;actions/checkout@v4&lt;/code&gt; is a &lt;em&gt;prebuilt action&lt;/em&gt; (someone else's reusable step) that clones your repo onto the runner, and &lt;code&gt;run:&lt;/code&gt; executes a shell command. Almost every step is one of those two shapes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; YAML is whitespace-sensitive, indentation is two spaces, never tabs. About half of all "my workflow won't run" problems are an indentation slip. If GitHub shows a syntax error, check your spacing first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 2, install, lint, and test
&lt;/h2&gt;

&lt;p&gt;Now make it real. We'll set up Node, do a clean dependency install, lint, and run tests, the three checks that should gate every change. Each is just another step.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="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;Set up Node&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;20"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;          &lt;span class="c1"&gt;# clean install from the lockfile&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;Lint&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run lint&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few deliberate choices here. We use &lt;strong&gt;&lt;code&gt;npm ci&lt;/code&gt;&lt;/strong&gt;, not &lt;code&gt;npm install&lt;/code&gt;, &lt;code&gt;ci&lt;/code&gt; installs the exact versions from your lockfile and fails if the lockfile is out of sync, which is precisely what you want in automation: reproducible, no surprises. We also narrowed the &lt;code&gt;push&lt;/code&gt; trigger to the &lt;code&gt;main&lt;/code&gt; branch while keeping &lt;code&gt;pull_request&lt;/code&gt; open, so the pipeline runs on every PR and on merges to main, but not on every push to every random feature branch (saving CI minutes).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Warning:&lt;/strong&gt; In CI, prefer &lt;code&gt;npm ci&lt;/code&gt; over &lt;code&gt;npm install&lt;/code&gt;. &lt;code&gt;npm install&lt;/code&gt; can quietly update your lockfile and pull different versions than your teammates have, defeating the whole point of a reproducible build. &lt;code&gt;npm ci&lt;/code&gt; is stricter and faster. (Python: use &lt;code&gt;pip install -r requirements.txt&lt;/code&gt; with pinned versions; the principle is the same.)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3, make it fast with caching
&lt;/h2&gt;

&lt;p&gt;Right now every run re-downloads all your dependencies from scratch, slow and wasteful. Caching stores them between runs so repeat builds are dramatically faster. The &lt;code&gt;setup-node&lt;/code&gt; action has caching built in; you just turn it on.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.github/workflows/ci.yml (excerpt)&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="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;Set up Node&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;20"&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm"&lt;/span&gt;        &lt;span class="c1"&gt;# cache ~/.npm keyed on your lockfile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single &lt;code&gt;cache: "npm"&lt;/code&gt; line keys the cache on your &lt;code&gt;package-lock.json&lt;/code&gt;. As long as your dependencies don't change, the cache is reused and &lt;code&gt;npm ci&lt;/code&gt; finishes in seconds instead of minutes. When you add or update a package, the lockfile changes, the key changes, and the cache rebuilds automatically. You never manage it by hand.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Slow pipelines get ignored. If CI takes longer than your coffee refill, people stop watching it and start merging on faith, which defeats the purpose. Caching is the single highest-leverage speedup for most pipelines. Turn it on early.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 4, run independent jobs in parallel
&lt;/h2&gt;

&lt;p&gt;Linting and testing don't depend on each other, so why run them one after the other? Split them into separate &lt;strong&gt;jobs&lt;/strong&gt; and GitHub runs them on separate machines simultaneously, your total wall-clock time drops to whichever is slowest, not the sum.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;20"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run lint&lt;/span&gt;

  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;20"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;lint&lt;/code&gt; and &lt;code&gt;test&lt;/code&gt; run in parallel. Each job is fully isolated, a fresh runner with nothing shared, which is why each repeats the checkout and install. That isolation is a feature: one job can't pollute another. If you ever need a job to wait for another (say, deploy &lt;em&gt;after&lt;/em&gt; tests pass), you add &lt;code&gt;needs: test&lt;/code&gt; to it, and GitHub sequences them for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5, make the pipeline a required status check
&lt;/h2&gt;

&lt;p&gt;A green checkmark is nice, but the real power move is making it &lt;em&gt;mandatory&lt;/em&gt;: configure your repo so a pull request &lt;strong&gt;cannot be merged&lt;/strong&gt; until CI passes. This turns your pipeline from a suggestion into a gate that protects &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Open branch protection&lt;/strong&gt;: In your GitHub repo: Settings → Branches → Add branch protection rule. Set the branch name pattern to main.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Require status checks&lt;/strong&gt;: Tick "Require status checks to pass before merging," then search for and select your jobs (lint and test) from the list. They appear once they've run at least once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Require a PR&lt;/strong&gt;: Also tick "Require a pull request before merging" so nobody can push straight to main and skip the checks entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save and test it&lt;/strong&gt;: Open a PR that deliberately breaks a test. GitHub now blocks the Merge button until the pipeline is green. That's CI doing its actual job.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Warning:&lt;/strong&gt; Without branch protection, CI is advisory, a teammate in a hurry can merge a red build. The status check is what makes the pipeline &lt;em&gt;enforced&lt;/em&gt;. Set it up the moment your CI is stable; it's the difference between "we have tests" and "broken code can't reach main."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wrong file location.&lt;/strong&gt; Workflows only run from &lt;code&gt;.github/workflows/&lt;/code&gt;. A file in &lt;code&gt;.github/&lt;/code&gt; or &lt;code&gt;workflows/&lt;/code&gt; is silently ignored, no error, just nothing happens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tabs instead of spaces.&lt;/strong&gt; YAML demands spaces. A single tab anywhere breaks the whole file. Configure your editor to show whitespace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using &lt;code&gt;npm install&lt;/code&gt; instead of &lt;code&gt;npm ci&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;install&lt;/code&gt; can drift your lockfile and produce non-reproducible builds. &lt;code&gt;ci&lt;/code&gt; is the automation-correct choice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoding secrets in the YAML.&lt;/strong&gt; Tokens and passwords in a workflow file land in your Git history permanently. Use repo Settings → Secrets and reference them as &lt;code&gt;${{ secrets.NAME }}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never enabling branch protection.&lt;/strong&gt; A pipeline that doesn't block merges is decoration. Make it a required status check or broken code will still reach main.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting the cache.&lt;/strong&gt; Skipping &lt;code&gt;cache: "npm"&lt;/code&gt; makes every run re-download everything. People then stop waiting for slow CI, and a CI nobody watches is worthless.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Your first pipeline in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A workflow lives in .github/workflows/*.yml and is triggered by events.&lt;/li&gt;
&lt;li&gt;Workflow → jobs → steps → runners. Jobs run in parallel by default.&lt;/li&gt;
&lt;li&gt;Use actions/checkout to get your code, actions/setup-node to set up the toolchain.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;npm ci&lt;/code&gt; (not install) for reproducible builds; add &lt;code&gt;cache: "npm"&lt;/code&gt; for speed.&lt;/li&gt;
&lt;li&gt;Split independent work (lint, test) into separate jobs to run them in parallel.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;needs:&lt;/code&gt; to sequence jobs when one must wait for another.&lt;/li&gt;
&lt;li&gt;Enable branch protection with required status checks so red builds can't merge.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;You have CI gating every change. The natural next steps are understanding the bigger picture you just plugged into, practicing the commands hands-on, and adding a build step that packages your app into a container.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/cicd-fundamentals-what-a-pipeline-does" rel="noopener noreferrer"&gt;CI/CD Fundamentals&lt;/a&gt;, the full mental model your pipeline fits into.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/labs/cicd" rel="noopener noreferrer"&gt;Practice in the CI/CD lab&lt;/a&gt;, run real pipeline commands in an in-browser terminal.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/dockerfile-best-practices" rel="noopener noreferrer"&gt;Dockerfile Best Practices&lt;/a&gt;, package your app into a small, fast, secure image to deploy from CI.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/your-first-ci-pipeline-github-actions" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>cicd</category>
      <category>devops</category>
      <category>beginners</category>
    </item>
    <item>
      <title>CI/CD Fundamentals: What a Pipeline Really Does</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Wed, 22 Jul 2026 11:00:26 +0000</pubDate>
      <link>https://dev.to/sri2614/cicd-fundamentals-what-a-pipeline-really-does-12op</link>
      <guid>https://dev.to/sri2614/cicd-fundamentals-what-a-pipeline-really-does-12op</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; See exactly what happens between git push and live: the six pipeline stages, the CI-versus-CD distinction, and the &lt;strong&gt;build-once-deploy-many&lt;/strong&gt; rule that prevents whole classes of release disasters.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What happens between push and live?&lt;/li&gt;
&lt;li&gt;The one-sentence definition&lt;/li&gt;
&lt;li&gt;CI vs CD vs Continuous Deployment&lt;/li&gt;
&lt;li&gt;The picture: the six stages&lt;/li&gt;
&lt;li&gt;Build once, deploy many, the rule that prevents disasters&lt;/li&gt;
&lt;li&gt;A real pipeline you can read&lt;/li&gt;
&lt;li&gt;Why automate this at all?&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What happens between push and live?
&lt;/h2&gt;

&lt;p&gt;You write some code, run &lt;code&gt;git push&lt;/code&gt;, and a few minutes later it's running in production, or a red X appears and it isn't. Somewhere in that gap sits "the pipeline," and for most beginners it's a black box: a magic conveyor belt that either works or yells at you. This article opens the box.&lt;/p&gt;

&lt;p&gt;A CI/CD pipeline is just an &lt;strong&gt;automated sequence of steps&lt;/strong&gt; that takes your code from a commit to running software, checking it at every stage. Nothing about it is magic. Once you can name the stages and explain why each exists, you'll be able to read any team's pipeline and debug it when it goes red.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Beginners who've used Git but never built or really understood a pipeline. No prior CI/CD experience needed. We'll use GitHub Actions for the concrete example because it's free and ships with every GitHub repo, but the stages are identical in GitLab CI, Jenkins, CircleCI, and the rest.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The one-sentence definition
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;CI/CD is the practice of automatically building, testing, and delivering every change to your code, so that integrating and shipping software is a routine, low-risk, push-button event instead of a scary manual ritual.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Think of a pipeline like an airport security and boarding process for your code. Every passenger (a commit) goes through the same checkpoints in the same order, every time. No one skips screening because they're in a hurry. The whole system exists so that by the time a passenger reaches the plane (production), you're confident they're safe to fly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🎫 Check-in&lt;/td&gt;
&lt;td&gt;Source: a commit triggers the pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🧳 Bag scanning&lt;/td&gt;
&lt;td&gt;Build + automated tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🛂 Security checkpoint&lt;/td&gt;
&lt;td&gt;Lint, security scans, quality gates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🎒 Tagged, sealed luggage&lt;/td&gt;
&lt;td&gt;A versioned, immutable artifact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✈️ Boarding the plane&lt;/td&gt;
&lt;td&gt;Deploy + release to production&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Every commit is a passenger; the pipeline is the airport.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  CI vs CD vs Continuous Deployment
&lt;/h2&gt;

&lt;p&gt;Three terms get used interchangeably and they shouldn't be. They're three stages of maturity, each building on the last.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;What it automates&lt;/th&gt;
&lt;th&gt;Human still does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Continuous Integration (CI)&lt;/td&gt;
&lt;td&gt;Build + test every change, merged often&lt;/td&gt;
&lt;td&gt;Decides when/whether to deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continuous Delivery (CD)&lt;/td&gt;
&lt;td&gt;Above + package + deploy to staging, ready to ship&lt;/td&gt;
&lt;td&gt;Clicks 'approve' to release to prod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continuous Deployment&lt;/td&gt;
&lt;td&gt;Above + auto-release to prod if all checks pass&lt;/td&gt;
&lt;td&gt;Nothing, fully automated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Same acronym family, three distinct commitments. Each row assumes the one above it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Continuous Integration&lt;/strong&gt; is the foundation: every developer merges their work into the shared main branch frequently (at least daily), and every merge is automatically built and tested. The goal is to catch "it broke when our changes met" problems within minutes, not at the end of a painful multi-week "integration phase."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Continuous Delivery&lt;/strong&gt; extends that: every change that passes is automatically packaged and pushed as far as staging, leaving production one approval click away. You &lt;em&gt;could&lt;/em&gt; ship at any moment; a human just decides when. &lt;strong&gt;Continuous Deployment&lt;/strong&gt; removes that last click, if every check is green, it goes live automatically. Most teams live happily at Continuous Delivery; full Continuous Deployment requires deep confidence in your tests.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; The acronym "CD" is genuinely ambiguous, it means Delivery to some teams and Deployment to others. In an interview or a design doc, say which one you mean. It signals you know the difference.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The picture: the six stages
&lt;/h2&gt;

&lt;p&gt;Almost every pipeline, in any tool, runs the same six stages left to right. Here's the whole flow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY29tbWl0KCJTb3VyY2U8YnIvPmdpdCBwdXNoIC8gUFIiKQogIGNsYXNzIG5fY29tbWl0IGNsaWVudDsKICBuX2J1aWxkKCJCdWlsZDxici8-Y29tcGlsZSAvIGluc3RhbGwiKQogIGNsYXNzIG5fYnVpbGQgY29tcHV0ZTsKICBuX3Rlc3QoIlRlc3Q8YnIvPmxpbnQgKyB1bml0ICsgc2NhbiIpCiAgY2xhc3Mgbl90ZXN0IGNvbXB1dGU7CiAgbl9wYWNrYWdlKCJQYWNrYWdlPGJyLz5idWlsZCBhcnRpZmFjdCIpCiAgY2xhc3Mgbl9wYWNrYWdlIHF1ZXVlOwogIG5fcmVnaXN0cnkoIlJlZ2lzdHJ5PGJyLz5zdG9yZSBhcnRpZmFjdCIpCiAgY2xhc3Mgbl9yZWdpc3RyeSBkYXRhOwogIG5fc3RhZ2luZygiRGVwbG95IOKGkiBTdGFnaW5nPGJyLz52ZXJpZnkiKQogIGNsYXNzIG5fc3RhZ2luZyBlZGdlOwogIG5fcHJvZCgiUmVsZWFzZSDihpIgUHJvZDxici8-bGl2ZSIpCiAgY2xhc3Mgbl9wcm9kIGVkZ2U7CiAgbl9jb21taXQgLS0-fCJ0cmlnZ2VycyJ8IG5fYnVpbGQKICBuX2J1aWxkIC0tPiBuX3Rlc3QKICBuX3Rlc3QgLS0-fCJpZiBncmVlbiJ8IG5fcGFja2FnZQogIG5fcGFja2FnZSAtLi0-fCJzdG9yZSJ8IG5fcmVnaXN0cnkKICBuX3BhY2thZ2UgLS0-fCJzYW1lIGFydGlmYWN0Inwgbl9zdGFnaW5nCiAgbl9zdGFnaW5nIC0tPnwicHJvbW90ZSJ8IG5fcHJvZAogIG5fdGVzdCAtLi0-fCJmYWlsIOKGkiBub3RpZnkifCBuX2NvbW1pdA%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY29tbWl0KCJTb3VyY2U8YnIvPmdpdCBwdXNoIC8gUFIiKQogIGNsYXNzIG5fY29tbWl0IGNsaWVudDsKICBuX2J1aWxkKCJCdWlsZDxici8-Y29tcGlsZSAvIGluc3RhbGwiKQogIGNsYXNzIG5fYnVpbGQgY29tcHV0ZTsKICBuX3Rlc3QoIlRlc3Q8YnIvPmxpbnQgKyB1bml0ICsgc2NhbiIpCiAgY2xhc3Mgbl90ZXN0IGNvbXB1dGU7CiAgbl9wYWNrYWdlKCJQYWNrYWdlPGJyLz5idWlsZCBhcnRpZmFjdCIpCiAgY2xhc3Mgbl9wYWNrYWdlIHF1ZXVlOwogIG5fcmVnaXN0cnkoIlJlZ2lzdHJ5PGJyLz5zdG9yZSBhcnRpZmFjdCIpCiAgY2xhc3Mgbl9yZWdpc3RyeSBkYXRhOwogIG5fc3RhZ2luZygiRGVwbG95IOKGkiBTdGFnaW5nPGJyLz52ZXJpZnkiKQogIGNsYXNzIG5fc3RhZ2luZyBlZGdlOwogIG5fcHJvZCgiUmVsZWFzZSDihpIgUHJvZDxici8-bGl2ZSIpCiAgY2xhc3Mgbl9wcm9kIGVkZ2U7CiAgbl9jb21taXQgLS0-fCJ0cmlnZ2VycyJ8IG5fYnVpbGQKICBuX2J1aWxkIC0tPiBuX3Rlc3QKICBuX3Rlc3QgLS0-fCJpZiBncmVlbiJ8IG5fcGFja2FnZQogIG5fcGFja2FnZSAtLi0-fCJzdG9yZSJ8IG5fcmVnaXN0cnkKICBuX3BhY2thZ2UgLS0-fCJzYW1lIGFydGlmYWN0Inwgbl9zdGFnaW5nCiAgbl9zdGFnaW5nIC0tPnwicHJvbW90ZSJ8IG5fcHJvZAogIG5fdGVzdCAtLi0-fCJmYWlsIOKGkiBub3RpZnkifCBuX2NvbW1pdA%3FbgColor%3D0d1017%26type%3Dpng" alt="The canonical pipeline. A commit triggers build, then the artifact is tested and packaged ONCE. That same artifact is de" width="1381" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The canonical pipeline. A commit triggers build, then the artifact is tested and packaged ONCE. That same artifact is deployed to staging, then promoted to production. The dashed line back to the commit is the feedback loop: any failed stage stops the pipeline and notifies the author.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Source&lt;/strong&gt;: A push or pull request triggers the pipeline. The system checks out exactly the commit you pushed, nothing more, nothing less.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build&lt;/strong&gt;: Turn source into something runnable: install dependencies, compile, transpile. If it won't build, the pipeline stops here and tells you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test&lt;/strong&gt;: Run the automated checks, linting, unit tests, integration tests, security scans. This is the quality gate. A failure here blocks everything downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Package&lt;/strong&gt;: Bundle the built, tested code into a single immutable artifact, typically a container image or a versioned archive. This is the thing that will actually ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy&lt;/strong&gt;: Push that exact artifact to an environment, usually staging first, and run smoke tests to confirm it actually starts and serves traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release&lt;/strong&gt;: Promote the same artifact to production, making it live for users. In Continuous Delivery this needs a click; in Continuous Deployment it's automatic.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Build once, deploy many, the rule that prevents disasters
&lt;/h2&gt;

&lt;p&gt;Here's the single most important principle in the whole pipeline, and the one beginners most often get wrong: &lt;strong&gt;you build the artifact exactly once, and promote that identical artifact through every environment.&lt;/strong&gt; You do &lt;em&gt;not&lt;/em&gt; rebuild for staging and then rebuild again for production.&lt;/p&gt;

&lt;p&gt;Why does this matter so much? If you rebuild for each environment, you can never be sure the thing you tested is the thing you shipped. A dependency could publish a new version between builds. A base image could change. You'd test artifact A in staging and ship a subtly different artifact B to production, and that gap is exactly where the "but it worked in staging!" outages come from.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;The rebuild trap:&lt;/strong&gt; If your pipeline runs the build step separately for staging and prod, you don't have a reliable pipeline, you have two pipelines that happen to look alike. Build once, tag it, store it in a registry, and deploy that same tagged artifact everywhere. What you tested is then provably what you shipped.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is also why the Package and Registry stages exist as their own thing. The artifact gets a unique, immutable version (often the Git commit SHA), lands in a registry, and every later deploy just &lt;em&gt;pulls and runs&lt;/em&gt; it. We go deep on this in &lt;a href="https://thesimplifiedtech.com/blog/artifacts-and-registries" rel="noopener noreferrer"&gt;Artifacts &amp;amp; Registries&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real pipeline you can read
&lt;/h2&gt;

&lt;p&gt;Enough theory. Here's a minimal but genuine GitHub Actions workflow. Drop this file into a repo and every push runs CI automatically. Read it top to bottom, each block maps to a stage from the diagram.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;

&lt;span class="c1"&gt;# Source stage: what triggers the pipeline&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-and-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;        &lt;span class="c1"&gt;# a fresh machine, every run&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Check out exactly this commit&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="c1"&gt;# Build stage: set up the toolchain&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;20"&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm"&lt;/span&gt;             &lt;span class="c1"&gt;# speed up repeat runs&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;                 &lt;span class="c1"&gt;# clean, reproducible install&lt;/span&gt;

      &lt;span class="c1"&gt;# Test stage: the quality gate&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;Lint&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run lint&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;Unit tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;

      &lt;span class="c1"&gt;# Package stage: produce the artifact&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to notice. &lt;strong&gt;One:&lt;/strong&gt; the &lt;code&gt;on:&lt;/code&gt; block is the Source stage, it says "run this on every push to main and every pull request." &lt;strong&gt;Two:&lt;/strong&gt; every run starts on a clean &lt;code&gt;ubuntu-latest&lt;/code&gt; machine, so there's no "works on my laptop", it's a fresh, reproducible environment each time. &lt;strong&gt;Three:&lt;/strong&gt; the steps run in order and the first failure stops the rest, exactly like the airport: fail screening and you don't reach the gate.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; This is CI only, it builds and tests but doesn't deploy. That's the right place to start. Get a green checkmark gating every pull request first; add the deploy stages once your team trusts the tests. We build the full version step-by-step in the next article.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why automate this at all?
&lt;/h2&gt;

&lt;p&gt;If you've only ever deployed by hand, automation can feel like overhead. It isn't. The payoff compounds with every single commit for the life of the project.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt;, the same steps run the same way every time. No forgotten command, no "did you remember to run the migrations?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt;, feedback in minutes. A broken test is caught before the code is even merged, not discovered days later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence&lt;/strong&gt;, because every change is tested identically, shipping stops being scary. Scary deploys lead to rare, giant, risky deploys, the exact thing DevOps fights.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeatability&lt;/strong&gt;, a new teammate doesn't need a 12-step deployment wiki. They push code; the pipeline does the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An audit trail&lt;/strong&gt;, every build, test result, and deploy is logged. When something breaks, you can see exactly what shipped and when.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rebuilding per environment.&lt;/strong&gt; The cardinal sin. Build once, store the artifact, promote the &lt;em&gt;same&lt;/em&gt; one. Rebuilding means staging and prod can silently differ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests that don't actually block.&lt;/strong&gt; A test suite that runs but doesn't fail the pipeline (or that everyone ignores when red) is theater. A red pipeline must stop the line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow pipelines nobody waits for.&lt;/strong&gt; If CI takes 40 minutes, people stop paying attention to it. Cache dependencies, parallelize jobs, and keep the feedback loop tight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Putting secrets in the workflow file.&lt;/strong&gt; Never hardcode tokens or passwords in YAML, it's in your Git history forever. Use the CI tool's encrypted secrets store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confusing 'deploy' with 'release.'&lt;/strong&gt; Deploying puts code on a server; releasing exposes it to users. Conflating them makes safe rollout strategies impossible.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole pipeline in six lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A pipeline is an automated sequence: source → build → test → package → deploy → release.&lt;/li&gt;
&lt;li&gt;CI = build + test every change. CD = automatically deliver it, ready to ship.&lt;/li&gt;
&lt;li&gt;Continuous Deployment goes one step further: live automatically if all checks pass.&lt;/li&gt;
&lt;li&gt;Build the artifact ONCE; promote that identical artifact through every environment.&lt;/li&gt;
&lt;li&gt;The test stage is the quality gate, a failure must stop everything downstream.&lt;/li&gt;
&lt;li&gt;Automate it for consistency, speed, confidence, and a full audit trail.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;You understand the stages, now build one with your own hands and watch a green checkmark gate a pull request for the first time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/your-first-ci-pipeline-github-actions" rel="noopener noreferrer"&gt;Your First CI Pipeline with GitHub Actions&lt;/a&gt;, the hands-on, copy-paste version of this article.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/labs/cicd" rel="noopener noreferrer"&gt;Practice in the CI/CD lab&lt;/a&gt;, run pipeline commands in a real in-browser terminal.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/deployment-strategies-blue-green-canary" rel="noopener noreferrer"&gt;Deployment Strategies: Blue-Green &amp;amp; Canary&lt;/a&gt;, what the Release stage looks like when you do it safely.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/cicd-fundamentals-what-a-pipeline-does" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>pipelines</category>
      <category>automation</category>
      <category>foundations</category>
    </item>
    <item>
      <title>What DevOps Actually Is (It's Not a Job Title)</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Tue, 21 Jul 2026 11:00:26 +0000</pubDate>
      <link>https://dev.to/sri2614/what-devops-actually-is-its-not-a-job-title-4amm</link>
      <guid>https://dev.to/sri2614/what-devops-actually-is-its-not-a-job-title-4amm</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; &lt;strong&gt;DevOps&lt;/strong&gt; isn't a tool or a title, it's tearing down the wall between building and running software. The CALMS pillars and the Three Ways give you the mental model and show what actually changes day to day.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The most misunderstood word in tech&lt;/li&gt;
&lt;li&gt;The one-sentence definition&lt;/li&gt;
&lt;li&gt;The wall: why DevOps had to be invented&lt;/li&gt;
&lt;li&gt;CALMS: the five pillars&lt;/li&gt;
&lt;li&gt;The Three Ways: the engine underneath&lt;/li&gt;
&lt;li&gt;So what actually changes on a Tuesday?&lt;/li&gt;
&lt;li&gt;Common misunderstandings that hold beginners back&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The most misunderstood word in tech
&lt;/h2&gt;

&lt;p&gt;Open any job board and you'll see hundreds of "DevOps Engineer" roles. Read the listings and they describe someone who knows Docker, Kubernetes, Terraform, and a CI/CD tool. So most beginners conclude DevOps is a &lt;em&gt;job&lt;/em&gt;, a senior infrastructure person with a fancy title, or worse, a &lt;em&gt;tool&lt;/em&gt; you can buy. Both are wrong, and believing either will quietly hold your career back.&lt;/p&gt;

&lt;p&gt;DevOps is a &lt;strong&gt;way of working&lt;/strong&gt;. It's a set of cultural habits and practices that get the people who write software and the people who operate it to behave like one team with one goal: ship valuable changes safely, quickly, and often. The tools are downstream of that idea. You can own every tool in the ecosystem and still not be doing DevOps. You can do DevOps with almost no tooling at all.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Total beginners and career-switchers who keep hearing "DevOps" and want the real definition, not a tool list. If you've written any code or touched a server once, you have enough context. By the end you'll be able to explain DevOps to a hiring manager in a way that proves you actually get it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The one-sentence definition
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;DevOps is the practice of uniting development and operations, through culture, automation, and feedback, so a team can deliver software changes quickly and reliably, again and again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Notice what's &lt;em&gt;not&lt;/em&gt; in that sentence: no product names, no job titles. Now make it concrete. Imagine a restaurant where the chefs who invent dishes never set foot in the dining room, and the waiters who serve guests are forbidden from entering the kitchen. A guest complains a dish is cold, the waiter blames the kitchen, the kitchen blames the waiter, and nothing improves. That's the old way of building software:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;👨‍🍳 Chefs who never see guests&lt;/td&gt;
&lt;td&gt;Developers who never run their code in production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🤵 Waiters banned from the kitchen&lt;/td&gt;
&lt;td&gt;Operations who can't change the software&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🧱 The swinging door nobody crosses&lt;/td&gt;
&lt;td&gt;The 'throw it over the wall' handoff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🍽️ One team that owns the whole meal&lt;/td&gt;
&lt;td&gt;A DevOps team that owns build → run&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;DevOps tears down the wall between the kitchen and the dining room.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The wall: why DevOps had to be invented
&lt;/h2&gt;

&lt;p&gt;For decades, software organizations split into two tribes with opposite incentives. &lt;strong&gt;Developers&lt;/strong&gt; were rewarded for &lt;em&gt;change&lt;/em&gt;, ship features, fast. &lt;strong&gt;Operations&lt;/strong&gt; were rewarded for &lt;em&gt;stability&lt;/em&gt;, keep things up, don't break anything. Change and stability pull in opposite directions, so the two groups were structurally set up to fight.&lt;/p&gt;

&lt;p&gt;The result was the infamous handoff: developers built something, zipped it up, and "threw it over the wall" to ops to deploy. When it broke at 2am, ops got paged for code they didn't write and couldn't fix. They responded by adding gates, change-approval boards, and slower release schedules. Developers responded by batching up bigger, riskier releases. Everything got slower &lt;em&gt;and&lt;/em&gt; less stable, the worst of both worlds.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;The wall (old way)&lt;/th&gt;
&lt;th&gt;DevOps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who owns production&lt;/td&gt;
&lt;td&gt;Ops alone&lt;/td&gt;
&lt;td&gt;The whole team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release size&lt;/td&gt;
&lt;td&gt;Big, infrequent, risky&lt;/td&gt;
&lt;td&gt;Small, frequent, low-risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;When code breaks&lt;/td&gt;
&lt;td&gt;Blame the other tribe&lt;/td&gt;
&lt;td&gt;Blameless: fix the system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feedback loop&lt;/td&gt;
&lt;td&gt;Weeks (or never)&lt;/td&gt;
&lt;td&gt;Minutes to hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incentives&lt;/td&gt;
&lt;td&gt;Change vs. stability&lt;/td&gt;
&lt;td&gt;Both, shared by everyone&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The same two goals, treated as enemies (left) vs. as one shared goal (right).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;DevOps is the realization that you don't have to choose between speed and stability, done right, &lt;strong&gt;they reinforce each other.&lt;/strong&gt; Small frequent changes are easier to test, easier to debug, and easier to roll back than giant quarterly releases. The teams that deploy most often are also the most stable. That counter-intuitive finding is the heart of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  CALMS: the five pillars
&lt;/h2&gt;

&lt;p&gt;"Culture" is fuzzy, so the industry boiled DevOps down to a checklist you can actually reason about: &lt;strong&gt;CALMS&lt;/strong&gt;, Culture, Automation, Lean, Measurement, Sharing. If someone claims a team "does DevOps," run it through these five. A team strong on tooling but weak on culture is not doing DevOps; it's just automating its dysfunction.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Culture&lt;/strong&gt;, Shared ownership of the whole lifecycle. No "that's ops' problem." Blameless postmortems: when something breaks, you fix the &lt;em&gt;system&lt;/em&gt;, not punish the person.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automation&lt;/strong&gt;, Anything done by hand more than twice gets scripted: builds, tests, deployments, infrastructure. Humans are for judgement, not for repetitive toil.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lean&lt;/strong&gt;, Work in small batches and eliminate waste. Smaller changes flow faster and fail smaller. A one-line fix shipped today beats a hundred fixes shipped next quarter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measurement&lt;/strong&gt;, You can't improve what you don't measure. Track deployment frequency, lead time, change-failure rate, and time-to-restore (the DORA metrics).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sharing&lt;/strong&gt;, Knowledge, dashboards, and on-call are shared across the team. Developers see production. Operations understand the code. No silos, no single points of human failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; If you only remember one pillar, remember the first. Tools are the easy part, any team can install Jenkins. Culture is the hard part and the part that actually moves the needle. "You can't buy DevOps in a box" is the whole lesson in one line.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Three Ways: the engine underneath
&lt;/h2&gt;

&lt;p&gt;CALMS tells you &lt;em&gt;what&lt;/em&gt; a DevOps team looks like. The &lt;strong&gt;Three Ways&lt;/strong&gt; (from &lt;em&gt;The Phoenix Project&lt;/em&gt; and &lt;em&gt;The DevOps Handbook&lt;/em&gt;) tell you &lt;em&gt;how&lt;/em&gt; it improves over time. They build on each other, so the order matters.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The First Way, Flow&lt;/strong&gt;: Optimize the whole left-to-right flow of work, from idea to running in production. Make work visible, shrink batch sizes, and never pass defects downstream. Goal: a fast, smooth pipeline from commit to customer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Second Way, Feedback&lt;/strong&gt;: Create fast feedback loops flowing right-to-left, so problems are caught early and signals reach the people who can act. Monitoring, automated tests, and alerts turn 'we found out from an angry customer' into 'the pipeline caught it in 90 seconds.'&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Third Way, Continual Learning&lt;/strong&gt;: Build a culture of experimentation and learning from failure. Run blameless postmortems, allocate time to improve the system itself, and treat every incident as a lesson the whole org absorbs, not a witch hunt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Read them as a loop: &lt;strong&gt;Flow&lt;/strong&gt; gets changes out fast, &lt;strong&gt;Feedback&lt;/strong&gt; tells you what happened, and &lt;strong&gt;Learning&lt;/strong&gt; feeds improvements back into the flow. A team running all three gets a little better every week, that compounding is what separates great engineering orgs from average ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what actually changes on a Tuesday?
&lt;/h2&gt;

&lt;p&gt;Abstractions are nice, but here's what DevOps looks like in the day-to-day, so you can recognize it (or its absence) in a real job:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Without DevOps&lt;/th&gt;
&lt;th&gt;With DevOps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shipping a fix&lt;/td&gt;
&lt;td&gt;Ticket to ops, wait days&lt;/td&gt;
&lt;td&gt;Merge PR, pipeline deploys it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who's on call&lt;/td&gt;
&lt;td&gt;Ops, for code they didn't write&lt;/td&gt;
&lt;td&gt;The team that wrote it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A test fails&lt;/td&gt;
&lt;td&gt;Found manually, days later&lt;/td&gt;
&lt;td&gt;Pipeline blocks the merge instantly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spinning up a server&lt;/td&gt;
&lt;td&gt;File a request, wait&lt;/td&gt;
&lt;td&gt;Run terraform apply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After an outage&lt;/td&gt;
&lt;td&gt;Find someone to blame&lt;/td&gt;
&lt;td&gt;Blameless postmortem, fix the system&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Same Tuesday, two different worlds.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The tools you'll learn next, Git workflows, CI/CD pipelines, containers, infrastructure as code, are how teams &lt;em&gt;practice&lt;/em&gt; this. But always keep the order straight: the culture is the point; the tools just make the culture possible at scale. Learn both, and lead with the why.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;The interview trap:&lt;/strong&gt; When an interviewer asks "what is DevOps?", listing tools ("Docker, Kubernetes, Jenkins...") marks you as junior. Leading with culture, shared ownership, and fast feedback loops, then mentioning that tools enable them, marks you as someone who actually gets it. This distinction alone has changed careers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common misunderstandings that hold beginners back
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Thinking DevOps is a job title.&lt;/strong&gt; It's a way of working that &lt;em&gt;every&lt;/em&gt; engineer participates in. A "DevOps Engineer" is usually someone who builds the platform and automation that lets the whole team practice DevOps, not the only person who "does DevOps."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thinking DevOps is a tool you install.&lt;/strong&gt; No product makes you "DevOps." Buying Jenkins without changing how teams collaborate just automates the old dysfunction faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confusing automation with culture.&lt;/strong&gt; Automation is one of five CALMS pillars. A team that automates everything but still plays the blame game is missing the point entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Believing speed and stability are opposites.&lt;/strong&gt; The data says the opposite: teams that deploy frequently in small batches are &lt;em&gt;more&lt;/em&gt; stable, not less. Big rare releases are the risky ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping measurement.&lt;/strong&gt; Without the DORA metrics (deploy frequency, lead time, change-failure rate, time-to-restore), you're guessing whether you're improving. Measure, then improve.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DevOps is a way of working, not a job title or a tool.&lt;/li&gt;
&lt;li&gt;It exists to tear down the wall between dev (change) and ops (stability).&lt;/li&gt;
&lt;li&gt;Done right, speed and stability reinforce each other, small batches win.&lt;/li&gt;
&lt;li&gt;CALMS = Culture, Automation, Lean, Measurement, Sharing. Culture comes first.&lt;/li&gt;
&lt;li&gt;The Three Ways: Flow → Feedback → Continual Learning, looping forever.&lt;/li&gt;
&lt;li&gt;Tools (Git, CI/CD, containers, IaC) enable the culture, they aren't the culture.&lt;/li&gt;
&lt;li&gt;In an interview, lead with culture and feedback loops, not a tool list.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;You now have the mental model. The next step is to see how the practices show up in real workflows, start with the two that touch every engineer's day: how changes flow through a pipeline, and how teams collaborate on code without stepping on each other.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/cicd-fundamentals-what-a-pipeline-does" rel="noopener noreferrer"&gt;CI/CD Fundamentals: What a Pipeline Really Does&lt;/a&gt;, the automation backbone of the First Way.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/git-workflows-production-teams" rel="noopener noreferrer"&gt;Git Workflows for Production Teams&lt;/a&gt;, how shared ownership works in practice, one branch at a time.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/career-paths/devops-engineer" rel="noopener noreferrer"&gt;Explore the DevOps Engineer path&lt;/a&gt;, the full guided track from foundations to production.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/what-devops-actually-is" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>culture</category>
      <category>foundations</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Cloud Migration Strategies: The 6 Rs</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Mon, 20 Jul 2026 11:00:25 +0000</pubDate>
      <link>https://dev.to/sri2614/cloud-migration-strategies-the-6-rs-amf</link>
      <guid>https://dev.to/sri2614/cloud-migration-strategies-the-6-rs-amf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Migration is a per-app decision, not one big move. The &lt;strong&gt;6 Rs&lt;/strong&gt;, rehost, replatform, repurchase, refactor, retire, retain, give you a way to weigh effort against payoff and assign the right strategy to each workload.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Migration isn't one decision, it's hundreds&lt;/li&gt;
&lt;li&gt;The 6 Rs, defined&lt;/li&gt;
&lt;li&gt;Effort vs payoff: the whole framework in one table&lt;/li&gt;
&lt;li&gt;When to reach for each R&lt;/li&gt;
&lt;li&gt;How to actually assign Rs at scale&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Migration isn't one decision, it's hundreds
&lt;/h2&gt;

&lt;p&gt;"We're moving to the cloud" is the start of a conversation, not a plan. A real estate of 200 applications doesn't have one migration strategy, it has up to 200, because the right move for a legacy mainframe app is nothing like the right move for a stateless web service or a database you could replace with a managed offering tomorrow. The teams that fail treat migration as one big lift-and-shift; the teams that succeed sort each workload into the strategy that fits it.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;6 Rs&lt;/strong&gt; are the industry-standard framework for that sorting. Each R is a different answer to "what do we do with this specific application?", trading migration effort against the payoff you get on the other side. This article defines all six, lays them out by effort vs payoff, and gives you a way to assign the right R to each workload.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Engineers and architects planning or executing a cloud migration, or studying the framework for an interview or certification. No prior migration experience needed, but knowing the &lt;a href="https://thesimplifiedtech.com/blog/iaas-paas-saas-what-you-actually-manage" rel="noopener noreferrer"&gt;IaaS/PaaS/SaaS distinction&lt;/a&gt; will make the trade-offs land harder.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The 6 Rs, defined
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The 6 Rs are six distinct strategies for what to do with an application when you migrate: rehost, replatform, repurchase, refactor, retire, and retain.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's the one-line version of each, before we put numbers to them:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📦 Move the box as-is&lt;/td&gt;
&lt;td&gt;Rehost, lift and shift, no changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔧 Swap the old shelf for a built-in&lt;/td&gt;
&lt;td&gt;Replatform, minor cloud optimizations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🛒 Buy new instead of moving it&lt;/td&gt;
&lt;td&gt;Repurchase, switch to a SaaS product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🏗️ Rebuild the furniture for the new place&lt;/td&gt;
&lt;td&gt;Refactor, re-architect for the cloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🗑️ Throw out what you don't need&lt;/td&gt;
&lt;td&gt;Retire, decommission it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🚪 Leave it where it is for now&lt;/td&gt;
&lt;td&gt;Retain, keep it on-prem (for now)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Migrating apps is like moving house, every box gets a different decision.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Effort vs payoff: the whole framework in one table
&lt;/h2&gt;

&lt;p&gt;The core trade-off is always the same: the more you change an app, the more cloud-native benefit you unlock, and the more it costs to get there. Two of the six (retire, retain) are about &lt;em&gt;not&lt;/em&gt; migrating at all, and they're often the most valuable decisions you'll make.&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;What it means&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;th&gt;Pick it when…&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rehost&lt;/td&gt;
&lt;td&gt;Move as-is to cloud VMs ("lift &amp;amp; shift")&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;You need to exit a datacenter fast; app works fine as-is&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replatform&lt;/td&gt;
&lt;td&gt;Lift &amp;amp; shift with small cloud optimizations&lt;/td&gt;
&lt;td&gt;Low-Medium&lt;/td&gt;
&lt;td&gt;A quick win is available (e.g. move DB to a managed service)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repurchase&lt;/td&gt;
&lt;td&gt;Drop the app, buy a SaaS equivalent&lt;/td&gt;
&lt;td&gt;Low-Medium&lt;/td&gt;
&lt;td&gt;A SaaS product does the job better (e.g. self-hosted email → Microsoft 365)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refactor&lt;/td&gt;
&lt;td&gt;Re-architect to be cloud-native&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;The app is strategic and current architecture blocks scale/agility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retire&lt;/td&gt;
&lt;td&gt;Decommission, turn it off&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;td&gt;Nobody actually uses it (you'll be surprised how many)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retain&lt;/td&gt;
&lt;td&gt;Leave it where it is, revisit later&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Migration isn't worth it yet (compliance, cost, sunset planned)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The 6 Rs by effort and payoff. The right choice is per-application, there's no universally 'best' R.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Before you migrate anything, run a discovery pass for Retire and Retain. A typical portfolio has 10-20% of apps nobody uses anymore, migrating them is pure wasted effort. The cheapest migration is the one you don't do.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When to reach for each R
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Rehost, lift and shift
&lt;/h3&gt;

&lt;p&gt;Pick up the app, drop it onto cloud VMs, change as little as possible. It's the fastest path off your own hardware and the lowest-risk because the app barely changes. The catch: you've moved your problems, not solved them, you get cloud &lt;em&gt;location&lt;/em&gt; but little cloud &lt;em&gt;benefit&lt;/em&gt;. It's a legitimate first step when a datacenter lease is expiring and the clock is the priority; you can refactor later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replatform, lift, tinker, and shift
&lt;/h3&gt;

&lt;p&gt;Rehost, but take a couple of easy wins on the way: point the app at a managed database instead of a self-run one, put it behind a managed load balancer, containerize it. Modest effort, real operational payoff, you offload some undifferentiated maintenance without a full rewrite. The sweet spot for a lot of workloads.&lt;/p&gt;

&lt;h3&gt;
  
  
  Repurchase, replace, don't migrate
&lt;/h3&gt;

&lt;p&gt;Sometimes the smartest migration is deleting the app and subscribing to a SaaS product that does the same job better. Self-hosted CRM, wiki, or email servers are classic candidates. You trade control and customization for someone else running it entirely, often the right call for non-differentiating commodity software.&lt;/p&gt;

&lt;h3&gt;
  
  
  Refactor, rebuild for the cloud
&lt;/h3&gt;

&lt;p&gt;Re-architect the app to be genuinely cloud-native, break a monolith into services, go serverless, adopt managed data stores. This is the highest effort and cost by far, and only worth it for &lt;strong&gt;strategic&lt;/strong&gt; applications where the current architecture is actively holding the business back. Don't refactor a stable app just because cloud-native is fashionable; refactor when the payoff (scale, agility, cost-at-scale) is real and named.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retire &amp;amp; Retain, the non-migrations
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Retire&lt;/strong&gt;: discovery reveals apps no one uses, duplicate systems, things kept alive "just in case." Turn them off, that's a migration win with zero migration. &lt;strong&gt;Retain&lt;/strong&gt;: some apps shouldn't move yet, a strict compliance requirement, a workload that's cheaper on-prem, or a system slated for sunset within the year. "Not now" is a valid, deliberate answer, not a failure.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Don't default everything to Rehost:&lt;/strong&gt; Rehost is tempting because it's fast and low-risk, so teams under deadline pressure lift-and-shift everything. You then inherit every inefficiency you had on-prem, plus a cloud bill that's often &lt;em&gt;higher&lt;/em&gt; than your old hardware because you over-provisioned VMs to match physical servers. Rehost is a fine bridge, just have a plan to optimize afterward, not 'rehost and forget.'&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How to actually assign Rs at scale
&lt;/h2&gt;

&lt;p&gt;Across a big portfolio, you don't decide app-by-app from scratch, you run a structured pass.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Discover the full inventory&lt;/strong&gt;: You can't migrate what you can't see. Catalog every application, its dependencies, owner, and usage. This step alone surfaces the Retire candidates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score each app on two axes&lt;/strong&gt;: Business value (how strategic) and migration difficulty (how coupled, how legacy). High-value + high-difficulty apps are your refactor candidates; low-value ones lean toward retire or rehost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assign an R per app&lt;/strong&gt;: Map each app to its R using the table above. Be honest: most apps are rehost or replatform; refactor is reserved for the strategic few.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequence by risk and dependency&lt;/strong&gt;: Start with low-risk, low-dependency apps to build momentum and learn your tooling. Migrate tightly-coupled groups together. Save the scary monolith for when the team is warmed up.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Use early, easy rehost/replatform migrations to build organizational muscle, your runbooks, your landing zone, your team's confidence. By the time you reach the hard refactors, you've de-risked everything around them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migration is a per-application decision, not one company-wide strategy.&lt;/li&gt;
&lt;li&gt;The 6 Rs: rehost, replatform, repurchase, refactor, retire, retain.&lt;/li&gt;
&lt;li&gt;More change = more cloud-native payoff, but more effort and risk. Pick per app.&lt;/li&gt;
&lt;li&gt;Rehost = fast bridge with little benefit; have a plan to optimize after, not 'rehost and forget.'&lt;/li&gt;
&lt;li&gt;Replatform is the sweet spot for many apps; repurchase replaces commodity software with SaaS.&lt;/li&gt;
&lt;li&gt;Refactor only the strategic few where architecture genuinely blocks the business.&lt;/li&gt;
&lt;li&gt;Retire and Retain are real wins, the cheapest migration is the one you don't do.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;The 6 Rs tell you &lt;em&gt;whether&lt;/em&gt; and &lt;em&gt;how&lt;/em&gt; to move; the next questions are &lt;em&gt;what you'll manage&lt;/em&gt; afterward and &lt;em&gt;whether the result is well-architected&lt;/em&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/iaas-paas-saas-what-you-actually-manage" rel="noopener noreferrer"&gt;IaaS vs PaaS vs SaaS: what you actually manage&lt;/a&gt;, the responsibility shift behind replatform and repurchase.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/well-architected-framework-decoded" rel="noopener noreferrer"&gt;The Well-Architected Framework, decoded&lt;/a&gt;, how to make sure what you migrate is built right.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/career-paths/cloud-engineer" rel="noopener noreferrer"&gt;The Cloud Engineer path&lt;/a&gt;, from fundamentals through migration and governance.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/cloud-migration-strategies-6-rs" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>migration</category>
      <category>cloudstrategy</category>
      <category>6rs</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
