<?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: Mahendra Singh</title>
    <description>The latest articles on DEV Community by Mahendra Singh (@akoode_tech).</description>
    <link>https://dev.to/akoode_tech</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3913898%2F22861018-69f3-4bf5-bba3-2d6a546b6512.jpg</url>
      <title>DEV Community: Mahendra Singh</title>
      <link>https://dev.to/akoode_tech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/akoode_tech"/>
    <language>en</language>
    <item>
      <title>Docker vs Kubernetes: Stop Comparing Them Like They Compete</title>
      <dc:creator>Mahendra Singh</dc:creator>
      <pubDate>Tue, 02 Jun 2026 20:14:59 +0000</pubDate>
      <link>https://dev.to/akoode_tech/docker-vs-kubernetes-stop-comparing-them-like-they-compete-4c4e</link>
      <guid>https://dev.to/akoode_tech/docker-vs-kubernetes-stop-comparing-them-like-they-compete-4c4e</guid>
      <description>&lt;p&gt;Every developer hitting their first production deployment runs into this question: &lt;strong&gt;Do I need Docker, Kubernetes, or both?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's the wrong framing. They don't compete. They operate at completely different layers of your infrastructure.&lt;/p&gt;

&lt;p&gt;Let me be blunt upfront:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt; = packages and runs your app as a container (single host)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes&lt;/strong&gt; = manages and scales those containers across many hosts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't choose between them. You choose &lt;em&gt;when&lt;/em&gt; to graduate from one to the other.&lt;/p&gt;




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

&lt;p&gt;Docker solves the "works on my machine" problem. It bundles your app code, runtime, libraries, and config into a &lt;strong&gt;container image&lt;/strong&gt; — a portable artifact that runs identically everywhere Docker is installed.&lt;/p&gt;

&lt;p&gt;Here's the simplest possible Dockerfile:&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:20-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--only&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&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", "server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build it, push it, run it anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; my-api:1.0 &lt;span class="nb"&gt;.&lt;/span&gt;
docker push my-registry/my-api:1.0
docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 my-registry/my-api:1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That image behaves the same on your MacBook, a CI runner, or a prod server. That's the whole value prop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Compose for Multi-Service Dev
&lt;/h3&gt;

&lt;p&gt;For local development with multiple services, Docker Compose is your best friend:&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;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL=postgres://user:pass@db:5432/myapp&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pass&lt;/span&gt;

  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;     &lt;span class="c"&gt;# spin everything up&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;   &lt;span class="c"&gt;# follow logs&lt;/span&gt;
docker compose down      &lt;span class="c"&gt;# tear it all down&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command spins up your entire local stack. This is where most teams should live until they genuinely need orchestration.&lt;/p&gt;




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

&lt;p&gt;Docker works great on &lt;em&gt;one machine&lt;/em&gt;. The moment you need your app running across &lt;em&gt;multiple&lt;/em&gt; machines — with auto-scaling, self-healing, load balancing, and zero-downtime deploys — Docker alone doesn't cut it.&lt;/p&gt;

&lt;p&gt;Kubernetes (K8s) is the orchestration layer. It doesn't build containers. It &lt;strong&gt;runs and manages them across a cluster&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's what K8s handles automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which node to schedule each container on&lt;/li&gt;
&lt;li&gt;Restarting crashed containers&lt;/li&gt;
&lt;li&gt;Scaling up/down based on CPU/memory load&lt;/li&gt;
&lt;li&gt;Distributing traffic across replicas&lt;/li&gt;
&lt;li&gt;Rolling out updates without downtime&lt;/li&gt;
&lt;li&gt;Managing secrets and config&lt;/li&gt;
&lt;li&gt;Network routing between services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mental model: you declare your desired state, and K8s continuously reconciles reality toward it.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Real Kubernetes Deployment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# deployment.yaml&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;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-api&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;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="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-api&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;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-api&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;my-registry/my-api:1.0&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;3000&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="na"&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;100m"&lt;/span&gt;
              &lt;span class="na"&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;128Mi"&lt;/span&gt;
            &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&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="na"&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="na"&gt;readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/health&lt;/span&gt;
              &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&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;10&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;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;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-api-svc&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;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-api&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;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&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;ClusterIP&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; deployment.yaml
kubectl get pods
kubectl rollout status deployment/my-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If one of those 3 pods crashes, K8s replaces it automatically. Scale on demand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl scale deployment my-api &lt;span class="nt"&gt;--replicas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or set up auto-scaling based on CPU:&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;my-api-hpa&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;my-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="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Utilization&lt;/span&gt;
          &lt;span class="na"&gt;averageUtilization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;70&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Container vs Pod: The Confusion Cleared Up
&lt;/h2&gt;

&lt;p&gt;This trips up everyone new to K8s.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Container&lt;/strong&gt; = one packaged process. Docker's unit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pod&lt;/strong&gt; = K8s's deployable unit. Wraps one or more containers that share:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same network namespace (they talk via &lt;code&gt;localhost&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The same storage volumes&lt;/li&gt;
&lt;li&gt;The same lifecycle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most pods are single-container. But the sidecar pattern is real — you'll use it for logging agents, proxies (like Envoy in a service mesh), or secret injectors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;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;app&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-api:1.0&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-shipper&lt;/span&gt;        &lt;span class="c1"&gt;# sidecar&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;fluentd:latest&lt;/span&gt;
      &lt;span class="na"&gt;volumeMounts&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;logs&lt;/span&gt;
          &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/log/app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters practically&lt;/strong&gt;: K8s schedules pods, not individual containers. Resource requests, limits, and self-healing all operate at the pod level. If a pod crashes, K8s replaces the whole pod — not just the container inside it.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Docker and K8s Fit Together in a Real Pipeline
&lt;/h2&gt;

&lt;p&gt;They're sequential, not overlapping:&lt;br&gt;
Developer writes code&lt;/p&gt;

&lt;p&gt;→ Dockerfile defines the build&lt;br&gt;
→ CI runs: docker build + docker push → registry&lt;br&gt;
→ K8s pulls image from registry&lt;br&gt;
→ K8s schedules pods across nodes&lt;br&gt;
→ K8s manages lifecycle, scaling, health&lt;/p&gt;

&lt;p&gt;A typical GitHub Actions pipeline wiring both together:&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-and-deploy&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;Build and 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="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;docker build -t $REGISTRY/my-api:$GITHUB_SHA .&lt;/span&gt;
          &lt;span class="s"&gt;docker push $REGISTRY/my-api:$GITHUB_SHA&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;Deploy to K8s&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;kubectl set image deployment/my-api \&lt;/span&gt;
            &lt;span class="s"&gt;api=$REGISTRY/my-api:$GITHUB_SHA&lt;/span&gt;
          &lt;span class="s"&gt;kubectl rollout status deployment/my-api&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker handles the build. Kubernetes handles the deploy. Clean handoff.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on runtimes&lt;/strong&gt;: Kubernetes uses &lt;code&gt;containerd&lt;/code&gt; as its default runtime, not Docker directly. But Docker-built images follow the OCI standard, so they're fully compatible. You don't need to change your build workflow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When to Use Docker Without Kubernetes ✅
&lt;/h2&gt;

&lt;p&gt;Use Docker alone when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're in early development or pre-PMF&lt;/li&gt;
&lt;li&gt;Your app runs on a single server&lt;/li&gt;
&lt;li&gt;Your team is 1–3 engineers&lt;/li&gt;
&lt;li&gt;You have fewer than ~10 containers total&lt;/li&gt;
&lt;li&gt;You're serving under ~10k DAU&lt;/li&gt;
&lt;li&gt;You're on a managed PaaS (Railway, Fly.io, Cloud Run) that abstracts orchestration for you&lt;/li&gt;
&lt;li&gt;Fast iteration matters more than scaling headroom right now&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Docker Compose handles multi-service local dev and even modest production deployments cleanly. Adding K8s at this stage is premature optimization with real engineering cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Add Kubernetes ✅
&lt;/h2&gt;

&lt;p&gt;Add K8s when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're running 5+ microservices that need coordinated deployment and networking&lt;/li&gt;
&lt;li&gt;Traffic spikes are real and you need auto-scaling&lt;/li&gt;
&lt;li&gt;You have an uptime SLA above 99.9% that requires self-healing&lt;/li&gt;
&lt;li&gt;Multiple teams deploy independently to shared infrastructure&lt;/li&gt;
&lt;li&gt;You need canary releases, blue/green deploys, or sophisticated rollout strategies&lt;/li&gt;
&lt;li&gt;You're serving 100k+ DAU&lt;/li&gt;
&lt;li&gt;Your monthly compute spend is high enough that K8s bin-packing efficiency actually saves money&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A rough rule of thumb: if your compute bill is under $5k/month and you have fewer than 15 services, K8s will likely cost more in engineering time than it saves in ops.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to Use Kubernetes ❌
&lt;/h2&gt;

&lt;p&gt;Avoid K8s when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No one on your team has real K8s operational experience&lt;/li&gt;
&lt;li&gt;You're a solo dev or very small startup&lt;/li&gt;
&lt;li&gt;Your stack is a monolith or 2–3 services&lt;/li&gt;
&lt;li&gt;Feature velocity is the priority and K8s YAML would slow you down&lt;/li&gt;
&lt;li&gt;A managed PaaS already gives you what you need&lt;/li&gt;
&lt;li&gt;You're adding it because it looks good on the architecture diagram&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've seen small teams burn weeks configuring ingress controllers, cert-manager, and RBAC for a 3-service app. The overhead is real. Don't add it until the pain of &lt;em&gt;not&lt;/em&gt; having orchestration is specific and concrete.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Docker&lt;/th&gt;
&lt;th&gt;Kubernetes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary job&lt;/td&gt;
&lt;td&gt;Build + run containers&lt;/td&gt;
&lt;td&gt;Orchestrate at scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;Single host&lt;/td&gt;
&lt;td&gt;Multi-host cluster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup time&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;td&gt;Hours to days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-scaling&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-healing&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rolling updates&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Automated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;Days&lt;/td&gt;
&lt;td&gt;Weeks–months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Dev, small scale&lt;/td&gt;
&lt;td&gt;Production, microservices&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Learning Path
&lt;/h2&gt;

&lt;p&gt;Docker learning curve: &lt;strong&gt;days.&lt;/strong&gt;&lt;br&gt;
Kubernetes learning curve: &lt;strong&gt;weeks to months.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The progression that actually works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get fluent with &lt;code&gt;docker build&lt;/code&gt;, &lt;code&gt;docker run&lt;/code&gt;, image layers, multi-stage builds&lt;/li&gt;
&lt;li&gt;Master Docker Compose for local multi-service dev&lt;/li&gt;
&lt;li&gt;Understand container networking (bridge, host, overlay)&lt;/li&gt;
&lt;li&gt;Then introduce K8s concepts: pods → deployments → services → ingress → namespaces → RBAC&lt;/li&gt;
&lt;li&gt;Run locally with &lt;code&gt;kind&lt;/code&gt; or &lt;code&gt;minikube&lt;/code&gt; before touching production
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Spin up a local K8s cluster for learning&lt;/span&gt;
kind create cluster &lt;span class="nt"&gt;--name&lt;/span&gt; dev
kubectl cluster-info &lt;span class="nt"&gt;--context&lt;/span&gt; kind-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Managed K8s options when you're ready for production:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cloud&lt;/th&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;td&gt;EKS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GCP&lt;/td&gt;
&lt;td&gt;GKE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure&lt;/td&gt;
&lt;td&gt;AKS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They manage the control plane. You manage your workloads.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Docker and Kubernetes aren't rivals. They're different layers of the same stack.&lt;/li&gt;
&lt;li&gt;Docker Compose in production is a legitimate, underrated choice for small/medium workloads.&lt;/li&gt;
&lt;li&gt;The Docker → K8s transition is a natural graduation, not a required step.&lt;/li&gt;
&lt;li&gt;Before rolling your own cluster, check if Cloud Run, App Runner, or Container Apps gives you 80% of K8s for 20% of the complexity.&lt;/li&gt;
&lt;li&gt;Master Docker deeply first. The mental models (images, layers, networking, volumes) carry directly into K8s.&lt;/li&gt;
&lt;li&gt;Add K8s when the pain of &lt;em&gt;not&lt;/em&gt; having orchestration is specific and real — not theoretical.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.akoode.com/blog/docker-vs-kubernetes" rel="noopener noreferrer"&gt;akoode.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>containers</category>
    </item>
    <item>
      <title>GitHub Actions vs Jenkins vs GitLab CI: A Developer's Honest Comparison (2026)</title>
      <dc:creator>Mahendra Singh</dc:creator>
      <pubDate>Tue, 26 May 2026 18:49:48 +0000</pubDate>
      <link>https://dev.to/akoode_tech/github-actions-vs-jenkins-vs-gitlab-ci-a-developers-honest-comparison-2026-481k</link>
      <guid>https://dev.to/akoode_tech/github-actions-vs-jenkins-vs-gitlab-ci-a-developers-honest-comparison-2026-481k</guid>
      <description>&lt;h1&gt;
  
  
  GitHub Actions vs Jenkins vs GitLab CI: A Developer's Honest Comparison (2026)
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This article is a technical rewrite of the original comparison published at &lt;a href="https://www.akoode.com/blog/github-actions-vs-jenkins-vs-gitlab-ci" rel="noopener noreferrer"&gt;akoode.com&lt;/a&gt;. Code examples and architecture breakdowns added for Dev.to readers.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I've set up pipelines on all three. Migrated a team off Jenkins to GitHub Actions. Watched a startup pick GitLab CI because the security scanning was bundled. Watched another stay on Jenkins because their infra literally couldn't reach the internet.&lt;/p&gt;

&lt;p&gt;Here's the honest breakdown — no vendor spin, just the practical trade-offs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Philosophical Difference
&lt;/h2&gt;

&lt;p&gt;Before the feature tables, get this mental model right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions&lt;/strong&gt; — pipeline-as-code that lives &lt;em&gt;inside your repo&lt;/em&gt;, triggered by GitHub events. No server to manage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jenkins&lt;/strong&gt; — a self-hosted automation &lt;em&gt;server&lt;/em&gt; you fully control. Maximum flexibility, maximum maintenance cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitLab CI&lt;/strong&gt; — pipeline engine &lt;em&gt;inside a complete DevOps platform&lt;/em&gt;. If you want one tool for code + CI + security scanning + registry, this is it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule of thumb in 2026: &lt;strong&gt;pipeline tools follow platform gravity&lt;/strong&gt;. The best CI/CD tool is usually the one closest to where your code lives. That's why GitHub Actions and GitLab CI have eaten Jenkins' lunch for new projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Each Tool Actually Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  GitHub Actions
&lt;/h3&gt;

&lt;p&gt;Workflows live at &lt;code&gt;.github/workflows/*.yml&lt;/code&gt;. Triggered by any GitHub event — push, PR, release, schedule, manual dispatch, or an external webhook.&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;# .github/workflows/ci.yml&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;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;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="s1"&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jobs run on &lt;strong&gt;runners&lt;/strong&gt; — GitHub-hosted VMs (Ubuntu, Windows, macOS) or self-hosted machines you register. The &lt;a href="https://github.com/marketplace?type=actions" rel="noopener noreferrer"&gt;Marketplace&lt;/a&gt; has 20,000+ reusable actions. Most integrations are already there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jenkins
&lt;/h3&gt;

&lt;p&gt;Jenkins uses a &lt;strong&gt;controller-agent architecture&lt;/strong&gt;. The controller handles scheduling and the UI. Agents run the actual build jobs — you can scale to hundreds of distributed agents.&lt;/p&gt;

&lt;p&gt;Pipelines are &lt;code&gt;Jenkinsfile&lt;/code&gt;s written in Groovy (or declarative syntax):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Jenkinsfile&lt;/span&gt;
&lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;

  &lt;span class="n"&gt;stages&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Install'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'npm ci'&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Test'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'npm test'&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Build'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'npm run build'&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Deploy'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;when&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="s1"&gt;'main'&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sshPublisher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;publishers:&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
          &lt;span class="n"&gt;sshPublisherDesc&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;configName:&lt;/span&gt; &lt;span class="s1"&gt;'prod-server'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nl"&gt;transfers:&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sshTransfer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;sourceFiles:&lt;/span&gt; &lt;span class="s1"&gt;'dist/**'&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
        &lt;span class="o"&gt;])&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Groovy is a real programming language — loops, conditionals, shared libraries. YAML tools hit ceilings on complex logic. Jenkins doesn't.&lt;/p&gt;

&lt;p&gt;Over 1,800 plugins cover basically every tool in the DevOps ecosystem. If it exists, Jenkins can probably integrate with it.&lt;/p&gt;

&lt;p&gt;The cost: &lt;strong&gt;you run the server&lt;/strong&gt;. Security patches, plugin compatibility, scaling — all yours.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitLab CI
&lt;/h3&gt;

&lt;p&gt;Config lives at &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; in the repo root. Syntax is YAML, similar feel to GitHub Actions:&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;# .gitlab-ci.yml&lt;/span&gt;
&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deploy&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;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node:20&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules/&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&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="s"&gt;npm test&lt;/span&gt;

&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&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;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dist/&lt;/span&gt;

&lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rsync -avz dist/ user@prod-server:/var/www/app/&lt;/span&gt;
  &lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://myapp.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What makes GitLab different is the &lt;strong&gt;platform depth&lt;/strong&gt;. One installation gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source code management&lt;/li&gt;
&lt;li&gt;CI/CD runners&lt;/li&gt;
&lt;li&gt;Container registry&lt;/li&gt;
&lt;li&gt;Package registry&lt;/li&gt;
&lt;li&gt;Built-in SAST, DAST, dependency scanning&lt;/li&gt;
&lt;li&gt;Kubernetes deployment integration&lt;/li&gt;
&lt;li&gt;Feature flags&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For regulated industries, that bundled security scanning is a real compliance lever.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;GitHub Actions&lt;/th&gt;
&lt;th&gt;Jenkins&lt;/th&gt;
&lt;th&gt;GitLab CI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup time&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;td&gt;Hours–Days&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config language&lt;/td&gt;
&lt;td&gt;YAML&lt;/td&gt;
&lt;td&gt;Groovy / Declarative&lt;/td&gt;
&lt;td&gt;YAML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;Cloud-managed&lt;/td&gt;
&lt;td&gt;Self-hosted&lt;/td&gt;
&lt;td&gt;Cloud or self-hosted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance overhead&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low–Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ecosystem&lt;/td&gt;
&lt;td&gt;20,000+ Actions&lt;/td&gt;
&lt;td&gt;1,800+ plugins&lt;/td&gt;
&lt;td&gt;Built-in suite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-SCM support&lt;/td&gt;
&lt;td&gt;GitHub only&lt;/td&gt;
&lt;td&gt;Any VCS&lt;/td&gt;
&lt;td&gt;GitLab only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security scanning&lt;/td&gt;
&lt;td&gt;Via 3rd-party actions&lt;/td&gt;
&lt;td&gt;Via plugins&lt;/td&gt;
&lt;td&gt;Built-in SAST/DAST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes integration&lt;/td&gt;
&lt;td&gt;Via actions&lt;/td&gt;
&lt;td&gt;Via plugins&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free tier (private)&lt;/td&gt;
&lt;td&gt;2,000 min/month&lt;/td&gt;
&lt;td&gt;Free (infra costs)&lt;/td&gt;
&lt;td&gt;400 min/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vendor lock-in&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low–Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Jenkins vs GitHub Actions: Where Each Wins
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions wins on:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero-to-pipeline time (seriously, under an hour)&lt;/li&gt;
&lt;li&gt;Maintenance burden (GitHub manages the infra)&lt;/li&gt;
&lt;li&gt;Developer experience and discoverability&lt;/li&gt;
&lt;li&gt;Marketplace ecosystem&lt;/li&gt;
&lt;li&gt;Cost for standard workloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Jenkins wins on:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Air-gapped / private network environments&lt;/li&gt;
&lt;li&gt;Multi-SCM support (GitHub + GitLab + Bitbucket + SVN simultaneously)&lt;/li&gt;
&lt;li&gt;Complex pipeline logic that YAML can't cleanly express&lt;/li&gt;
&lt;li&gt;Full infrastructure control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest take: Jenkins teams in 2026 are there because their infra &lt;em&gt;requires&lt;/em&gt; it, or because migrating 200+ existing pipelines isn't worth the effort yet. For new projects with no constraints, GitHub Actions is almost always the faster path.&lt;/p&gt;




&lt;h2&gt;
  
  
  GitLab CI vs Jenkins: Platform vs Flexibility
&lt;/h2&gt;

&lt;p&gt;GitLab CI only works natively with GitLab repos. If your code is elsewhere, it's not a practical choice.&lt;/p&gt;

&lt;p&gt;Where GitLab CI beats Jenkins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Integration depth&lt;/strong&gt; — no plugin wrangling, everything's built-in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security tooling&lt;/strong&gt; — SAST, DAST, secret detection, dependency scanning without configuring separate tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single auth model&lt;/strong&gt; — one platform, one login, one audit trail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where Jenkins beats GitLab CI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;True flexibility&lt;/strong&gt; — Jenkins doesn't care where your code lives&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data isolation&lt;/strong&gt; — a Jenkins controller on-prem means your pipeline data, secrets, and logs never leave your network. GitLab Self-Managed achieves similar isolation but you're now maintaining a full GitLab installation.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Can You Use GitHub Actions + Jenkins Together?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Yes, and it's a legitimate architecture.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Common hybrid model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub PR opened
      │
      ▼
GitHub Actions  ←── Runs unit tests, lint, builds Docker image
      │                 pushes to registry
      │
      ▼
Jenkins (self-hosted) ←── Handles deploy to internal infrastructure,
                           legacy systems, compliance environments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire them together using GitHub Actions &lt;strong&gt;self-hosted runners&lt;/strong&gt; registered on Jenkins agents. GitHub-triggered workflows execute on your Jenkins infrastructure. Cloud-native triggers, on-prem execution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pricing: Total Cost of Ownership
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Free Tier&lt;/th&gt;
&lt;th&gt;Paid Entry&lt;/th&gt;
&lt;th&gt;Real Cost Driver&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;2,000 min/month (private)&lt;/td&gt;
&lt;td&gt;$4/user/month&lt;/td&gt;
&lt;td&gt;Extra minutes @ $0.008/min (Linux)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jenkins&lt;/td&gt;
&lt;td&gt;Free (open source)&lt;/td&gt;
&lt;td&gt;Infra costs only&lt;/td&gt;
&lt;td&gt;Engineer time for maintenance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitLab CI&lt;/td&gt;
&lt;td&gt;400 min/month&lt;/td&gt;
&lt;td&gt;$29/user/month (Premium)&lt;/td&gt;
&lt;td&gt;$99/user/month (Ultimate) for security&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Jenkins looks free. It isn't.&lt;/p&gt;

&lt;p&gt;If a DevOps engineer spends 20% of their time on Jenkins maintenance — plugin updates, security patches, agent scaling — that's easily $20–40K/year in burdened labour cost at typical engineering salaries. GitHub Actions or GitLab CI Premium is almost certainly cheaper at any team size over 5 people.&lt;/p&gt;

&lt;p&gt;The breakeven point where Jenkins makes economic sense: you need the flexibility, you have dedicated platform engineering capacity, and cloud-managed runners genuinely can't reach your deployment targets.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to Use Each
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✅ Use GitHub Actions when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Your code is on GitHub&lt;/li&gt;
&lt;li&gt;You're a startup or small-to-mid team without a dedicated platform team&lt;/li&gt;
&lt;li&gt;You want minimum setup and maximum focus on shipping product&lt;/li&gt;
&lt;li&gt;You're building web apps, APIs, open-source libraries&lt;/li&gt;
&lt;li&gt;You want to scale without hiring a CI/CD specialist&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ✅ Use Jenkins when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Your environment is air-gapped or on-premises&lt;/li&gt;
&lt;li&gt;You have code across multiple VCS (GitHub + GitLab + Bitbucket + SVN)&lt;/li&gt;
&lt;li&gt;You have complex, multi-stage pipeline logic that YAML can't express&lt;/li&gt;
&lt;li&gt;You have a dedicated DevOps team to own the infra&lt;/li&gt;
&lt;li&gt;You're deeply embedded in an existing Jenkins ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ✅ Use GitLab CI when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Your code already lives on GitLab&lt;/li&gt;
&lt;li&gt;You want one platform for source control + CI/CD + security scanning&lt;/li&gt;
&lt;li&gt;You're in a regulated industry where built-in SAST/DAST reduces compliance overhead&lt;/li&gt;
&lt;li&gt;You want self-hosted DevOps infra but don't want to depend on GitHub&lt;/li&gt;
&lt;/ul&gt;




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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Platform gravity is real&lt;/strong&gt; — pick the CI/CD tool that lives closest to your code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jenkins is free to license, not free to run&lt;/strong&gt; — factor engineering maintenance time into your TCO calculation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions + Jenkins hybrid&lt;/strong&gt; is a legitimate middle ground, not a compromise&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitLab Ultimate's bundled security scanning&lt;/strong&gt; can replace multiple standalone tool subscriptions — do the math before assuming it's expensive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For new projects in 2026 with no infra constraints:&lt;/strong&gt; GitHub Actions is the default. It's not even close&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matrix builds are underused&lt;/strong&gt; — test across Node 18/20/22, Python 3.11/3.12 in parallel without duplicating jobs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted runners&lt;/strong&gt; solve the "GitHub Actions can't reach my private network" problem without switching tools entirely&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Quick Decision Flowchart
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is your code on GitHub?
├── Yes → GitHub Actions (unless air-gapped)
└── No
    ├── Is your code on GitLab?
    │   └── Yes → GitLab CI
    └── Multiple VCS / air-gapped / complex logic?
        └── Yes → Jenkins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The right tool is the one that matches where your code lives, how much operational overhead your team can absorb, and what your compliance requirements demand.&lt;/p&gt;

&lt;p&gt;There's no universal winner. But there is a clear default for most teams.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>githubactions</category>
      <category>jenkins</category>
    </item>
    <item>
      <title>"Building a CI/CD Pipeline From Scratch: A Practical Guide for Developers (with GitHub Actions)"</title>
      <dc:creator>Mahendra Singh</dc:creator>
      <pubDate>Mon, 25 May 2026 19:49:06 +0000</pubDate>
      <link>https://dev.to/akoode_tech/building-a-cicd-pipeline-from-scratch-a-practical-guide-for-developers-with-github-actions-f98</link>
      <guid>https://dev.to/akoode_tech/building-a-cicd-pipeline-from-scratch-a-practical-guide-for-developers-with-github-actions-f98</guid>
      <description>&lt;h1&gt;
  
  
  Building a CI/CD Pipeline From Scratch: A Practical Guide for Developers
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Originally inspired by &lt;a href="https://www.akoode.com/blog/how-to-build-a-ci-cd-pipeline" rel="noopener noreferrer"&gt;Akoode's CI/CD pipeline guide&lt;/a&gt; — rewritten here with more depth, code, and less hand-waving.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I've seen teams spend hours manually running tests, zipping build artifacts, SSHing into servers, and crossing fingers before every deploy. CI/CD pipelines exist to kill that workflow. This guide skips the theory lecture and gets into how to actually build one.&lt;/p&gt;

&lt;p&gt;We'll use &lt;strong&gt;GitHub Actions&lt;/strong&gt; as the CI/CD platform — it's free for public repos, tightly integrated with GitHub, and requires zero external infrastructure to get started.&lt;/p&gt;




&lt;h2&gt;
  
  
  What CI/CD Actually Does (Plain English)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CI (Continuous Integration):&lt;/strong&gt; Every time code is pushed or a PR is opened, automatically run your build and tests. Catch breakage early, not in prod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CD (Continuous Delivery/Deployment):&lt;/strong&gt; After CI passes, automatically ship the artifact to staging or production — no human clicking "deploy" required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pipeline is just a sequence of automated steps triggered by a git event.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pipeline Architecture
&lt;/h2&gt;

&lt;p&gt;git push / PR open&lt;br&gt;
│&lt;br&gt;
▼&lt;br&gt;
┌─────────────┐&lt;br&gt;
│   Trigger   │  ← GitHub webhook fires&lt;br&gt;
└─────┬───────┘&lt;br&gt;
│&lt;br&gt;
▼&lt;br&gt;
┌─────────────┐&lt;br&gt;
│    Build    │  ← Install deps, compile, bundle&lt;br&gt;
└─────┬───────┘&lt;br&gt;
│&lt;br&gt;
▼&lt;br&gt;
┌─────────────┐&lt;br&gt;
│    Test     │  ← Unit, integration, lint&lt;br&gt;
└─────┬───────┘&lt;br&gt;
│&lt;br&gt;
▼&lt;br&gt;
┌─────────────┐&lt;br&gt;
│   Deploy    │  ← Push to staging/prod&lt;br&gt;
└─────────────┘&lt;br&gt;
Each stage is a &lt;strong&gt;job&lt;/strong&gt;. Jobs run on &lt;strong&gt;runners&lt;/strong&gt; (GitHub-hosted VMs or your own). They can run in parallel or sequentially with dependencies between them.&lt;/p&gt;


&lt;h2&gt;
  
  
  Setting Up Your First Pipeline with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Create this file in your repo:&lt;/p&gt;

&lt;p&gt;.github/&lt;br&gt;
workflows/&lt;br&gt;
ci-cd.yml&lt;/p&gt;
&lt;h3&gt;
  
  
  Minimal CI Pipeline (Node.js Example)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="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;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;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="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&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.js&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="s1"&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;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="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 linter&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;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;That's it. Push this file, and every PR gets auto-tested. No server, no webhook config.&lt;/p&gt;


&lt;h3&gt;
  
  
  Adding CD: Deploy to a Server
&lt;/h3&gt;

&lt;p&gt;After CI passes, deploy to production. Here we'll SSH into a VPS and pull + restart:&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;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build-and-test&lt;/span&gt;       &lt;span class="c1"&gt;# only runs if CI passes&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;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.ref == 'refs/heads/main'&lt;/span&gt;   &lt;span class="c1"&gt;# only on main branch&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy via SSH&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;appleboy/ssh-action@v1.0.3&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;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_USER }}&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;${{ secrets.DEPLOY_SSH_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;cd /var/www/myapp&lt;/span&gt;
            &lt;span class="s"&gt;git pull origin main&lt;/span&gt;
            &lt;span class="s"&gt;npm ci --omit=dev&lt;/span&gt;
            &lt;span class="s"&gt;pm2 restart myapp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store your SSH key and server IP in &lt;strong&gt;GitHub Secrets&lt;/strong&gt; (&lt;code&gt;Settings → Secrets and variables → Actions&lt;/code&gt;). Never hardcode credentials in the YAML.&lt;/p&gt;




&lt;h3&gt;
  
  
  Docker-Based Deploy (More Portable)
&lt;/h3&gt;

&lt;p&gt;If you're deploying containers:&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;build-and-push&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;Log in to Docker Hub&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;docker/login-action@v3&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;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKER_USERNAME }}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKER_TOKEN }}&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 and push Docker image&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;docker/build-push-action@v5&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;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
          &lt;span class="na"&gt;push&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;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yourusername/myapp:${{ github.sha }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the commit SHA as the image tag gives you a clean audit trail — every deploy is traceable to a specific commit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Environment Separation
&lt;/h2&gt;

&lt;p&gt;Don't deploy everything to production. Use branch-based environment targeting:&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;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="s"&gt;main&lt;/span&gt;       &lt;span class="c1"&gt;# → production&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;staging&lt;/span&gt;    &lt;span class="c1"&gt;# → staging env&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;feat/**'&lt;/span&gt;  &lt;span class="c1"&gt;# → preview envs (optional)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pair with GitHub Environments (&lt;code&gt;Settings → Environments&lt;/code&gt;) to add manual approval gates before production:&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;deploy-prod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://myapp.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub will pause and require an approver before proceeding. Useful for regulated teams or high-stakes deploys.&lt;/p&gt;




&lt;h2&gt;
  
  
  Caching Dependencies
&lt;/h2&gt;

&lt;p&gt;Don't reinstall &lt;code&gt;node_modules&lt;/code&gt; from scratch on every run. Cache it:&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;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="s1"&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&lt;/span&gt;        &lt;span class="c1"&gt;# ← this line handles caching automatically&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Python:&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&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;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.12'&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pip'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This alone can cut pipeline runtime by 60–70% on most projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  Matrix Testing: Test Across Multiple Versions
&lt;/h2&gt;

&lt;p&gt;Need to support Node 18 and 20? Don't write two jobs:&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;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;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;18&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;20&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;22&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.node-version }}&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 &amp;amp;&amp;amp; npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub runs these in parallel — fast and zero duplication.&lt;/p&gt;




&lt;h2&gt;
  
  
  Secrets Management
&lt;/h2&gt;

&lt;p&gt;Rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store secrets in GitHub Secrets, not in &lt;code&gt;.env&lt;/code&gt; files committed to the repo&lt;/li&gt;
&lt;li&gt;Use environment-scoped secrets for prod vs staging differences&lt;/li&gt;
&lt;li&gt;Rotate secrets regularly (SSH keys, API tokens)&lt;/li&gt;
&lt;li&gt;Never &lt;code&gt;echo&lt;/code&gt; secrets in run steps — they'll be masked in logs, but it's still bad practice
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PROD_API_KEY }}&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;./deploy.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  When to Use CI/CD
&lt;/h2&gt;

&lt;p&gt;✅ Any team with more than one developer&lt;br&gt;&lt;br&gt;
✅ Frequent deploys (more than once a week)&lt;br&gt;&lt;br&gt;
✅ You have a test suite (even a small one)&lt;br&gt;&lt;br&gt;
✅ Multiple environments (dev, staging, prod)&lt;br&gt;&lt;br&gt;
✅ Open source projects where contributors submit PRs  &lt;/p&gt;


&lt;h2&gt;
  
  
  When NOT to Use (or Keep It Simple)
&lt;/h2&gt;

&lt;p&gt;❌ Solo hobby project with no test suite — a basic deploy script is fine&lt;br&gt;&lt;br&gt;
❌ Legacy monolith where builds take 45 minutes — fix the build first&lt;br&gt;&lt;br&gt;
❌ Highly regulated environments where automated prod deploys are prohibited — use CD to staging only, with manual prod promotion  &lt;/p&gt;


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

&lt;p&gt;&lt;strong&gt;1. Not pinning action versions&lt;/strong&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;# Bad — can break silently when the action updates&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@main&lt;/span&gt;

&lt;span class="c1"&gt;# Good — locked to a specific version&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Running everything on every push&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Use path filters to skip unnecessary runs:&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;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;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src/**'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;package.json'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Storing secrets in env files&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Don't commit &lt;code&gt;.env.production&lt;/code&gt; to the repo. Use GitHub Secrets + a secrets manager (HashiCorp Vault, AWS Secrets Manager) for anything sensitive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. No rollback plan&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Tag your Docker images with the git SHA. If prod breaks, you can redeploy the previous image in 30 seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Pipeline at a Glance
&lt;/h2&gt;



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

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

&lt;span class="na"&gt;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;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="s1"&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run lint&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;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&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;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.ref == 'refs/heads/main'&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&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 Docker 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 myapp:${{ github.sha }} .&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 to registry&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo ${{ secrets.DOCKER_TOKEN }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin&lt;/span&gt;
          &lt;span class="s"&gt;docker push myapp:${{ github.sha }}&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;Deploy&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;appleboy/ssh-action@v1.0.3&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;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_USER }}&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;${{ secrets.DEPLOY_SSH_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;docker pull myapp:${{ github.sha }}&lt;/span&gt;
            &lt;span class="s"&gt;docker stop myapp || true&lt;/span&gt;
            &lt;span class="s"&gt;docker run -d --name myapp -p 3000:3000 myapp:${{ github.sha }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;ul&gt;
&lt;li&gt;Start small: even a single &lt;code&gt;npm test&lt;/code&gt; in CI adds real value&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;needs:&lt;/code&gt; keyword is your sequencing primitive — use it&lt;/li&gt;
&lt;li&gt;Branch protection rules + required CI checks = no broken code on main&lt;/li&gt;
&lt;li&gt;Commit SHA tagging on Docker images = instant rollback capability&lt;/li&gt;
&lt;li&gt;Cache dependencies — it's free performance&lt;/li&gt;
&lt;li&gt;Use GitHub Environments for approval gates before prod&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't a perfect pipeline on day one. It's getting &lt;em&gt;something&lt;/em&gt; automated, then adding stages as your confidence and test coverage grow.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>github</category>
      <category>devops</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
