<?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: Alfat Jahan Rony</title>
    <description>The latest articles on DEV Community by Alfat Jahan Rony (@alfatcse123).</description>
    <link>https://dev.to/alfatcse123</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%2F1095242%2F535a3435-f690-4d35-b074-2a9271cece35.jpeg</url>
      <title>DEV Community: Alfat Jahan Rony</title>
      <link>https://dev.to/alfatcse123</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alfatcse123"/>
    <language>en</language>
    <item>
      <title>From Push to Production: Automating Deployments with GitHub Actions</title>
      <dc:creator>Alfat Jahan Rony</dc:creator>
      <pubDate>Mon, 14 Sep 2026 09:32:56 +0000</pubDate>
      <link>https://dev.to/alfatcse123/from-push-to-production-automating-deployments-with-github-actions-e78</link>
      <guid>https://dev.to/alfatcse123/from-push-to-production-automating-deployments-with-github-actions-e78</guid>
      <description>&lt;h1&gt;
  
  
  Automating Your Deploy Process with GitHub Actions CI/CD
&lt;/h1&gt;

&lt;p&gt;If you've ever deployed code by manually SSH-ing into a server, running a build, praying nothing breaks, and copying files over — you already know why CI/CD exists. GitHub Actions makes it possible to turn that entire ritual into something that happens automatically, consistently, and safely every time you push code.&lt;/p&gt;

&lt;p&gt;In this post, we'll walk through what CI/CD actually means in practice, and build a real GitHub Actions workflow that tests, builds, and deploys an application automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Automate Deployment?
&lt;/h2&gt;

&lt;p&gt;Manual deployments are slow, error-prone, and don't scale with a team. A good CI/CD pipeline gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt; — the same steps run every time, in the same environment, removing "it worked on my machine" problems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt; — merging a PR can trigger a deploy within minutes, instead of waiting on someone to do it by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety nets&lt;/strong&gt; — automated tests and checks run before anything reaches production, catching regressions early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditability&lt;/strong&gt; — every deployment is tied to a commit, a workflow run, and a log you can go back and inspect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub Actions is a natural fit here because it lives right next to your code. There's no separate CI server to maintain — your pipeline is just a YAML file in your repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Anatomy of a GitHub Actions Workflow
&lt;/h2&gt;

&lt;p&gt;Every GitHub Actions pipeline lives under &lt;code&gt;.github/workflows/&lt;/code&gt; as a &lt;code&gt;.yml&lt;/code&gt; file. A workflow is built from a few core concepts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Triggers (&lt;code&gt;on&lt;/code&gt;)&lt;/strong&gt; — what causes the workflow to run (a push, a pull request, a schedule, a manual trigger).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jobs&lt;/strong&gt; — groups of steps that run on a virtual machine (called a "runner").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steps&lt;/strong&gt; — individual commands or reusable actions within a job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets&lt;/strong&gt; — encrypted values (like API keys or SSH credentials) injected into the workflow at runtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A typical CI/CD pipeline has two conceptual halves: &lt;strong&gt;CI&lt;/strong&gt; (build and test the code) and &lt;strong&gt;CD&lt;/strong&gt; (deploy it somewhere). You can combine both into a single workflow, or split them — CI runs on every PR, CD runs only when code lands on &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a CI/CD Pipeline Step by Step
&lt;/h2&gt;

&lt;p&gt;Here's the shape of the pipeline we're about to build — two workflows in the same repo, one gating changes and one shipping them:&lt;/p&gt;

&lt;p&gt;Let's build a pipeline for a Node.js app that runs tests on every pull request, then deploys to production whenever code merges into &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Continuous Integration
&lt;/h3&gt;

&lt;p&gt;First, we want every pull request to be automatically tested before it can be merged.&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;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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This workflow triggers on every pull request targeting &lt;code&gt;main&lt;/code&gt;. It checks out the code, installs dependencies with a cached &lt;code&gt;npm ci&lt;/code&gt; for speed, then runs linting and tests. If any step fails, the PR gets a red X and merging can be blocked until it's fixed — a simple but powerful quality gate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Continuous Deployment
&lt;/h3&gt;

&lt;p&gt;Once code is merged into &lt;code&gt;main&lt;/code&gt;, we want to build it and ship it automatically. Here's an example that builds a Docker image and deploys it to a server over SSH:&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;CD&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;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;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="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 and build&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;npm ci&lt;/span&gt;
          &lt;span class="s"&gt;npm run build&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.DOCKERHUB_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.DOCKERHUB_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;myorg/myapp:latest&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 server&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&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.SERVER_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.SERVER_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.SERVER_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 myorg/myapp:latest&lt;/span&gt;
            &lt;span class="s"&gt;docker compose up -d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Secrets&lt;/strong&gt; (&lt;code&gt;secrets.DOCKERHUB_TOKEN&lt;/code&gt;, &lt;code&gt;secrets.SERVER_SSH_KEY&lt;/code&gt;, etc.) are stored in your repository's Settings → Secrets and Variables, never hardcoded in the workflow file.&lt;/li&gt;
&lt;li&gt;The deploy step SSHes into your server and pulls the freshly built image, then restarts the service with Docker Compose. You could just as easily swap this for a deploy to AWS, a Kubernetes cluster, Vercel, or Netlify.&lt;/li&gt;
&lt;li&gt;Because this workflow only runs on pushes to &lt;code&gt;main&lt;/code&gt;, and &lt;code&gt;main&lt;/code&gt; is protected by the CI workflow's required checks, broken code effectively can't reach production.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2.5: Keeping Docker Inside the GitHub Ecosystem
&lt;/h3&gt;

&lt;p&gt;The example above pushes images to Docker Hub, which means juggling a separate account and separate secrets. If you'd rather keep everything under one roof, GitHub ships its own registry — &lt;strong&gt;GitHub Container Registry (&lt;code&gt;ghcr.io&lt;/code&gt;)&lt;/strong&gt; — and it authenticates using the token GitHub Actions already generates for you, &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;. No extra secrets to create or rotate.&lt;/p&gt;

&lt;p&gt;First, a simple Dockerfile for context:&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="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;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 ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&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="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; --from=build /app/dist ./dist&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/index.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a multi-stage build — the first stage installs dependencies and compiles the app, the second stage copies over only what's needed to run it, keeping the final image small.&lt;/p&gt;

&lt;p&gt;Now the GitHub Actions job that builds this image and pushes it straight to &lt;code&gt;ghcr.io&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&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;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;Log in to GitHub Container Registry&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;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io&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;${{ github.actor }}&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.GITHUB_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;Set up Docker Buildx&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/setup-buildx-action@v3&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;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;ghcr.io/${{ github.repository }}:latest&lt;/span&gt;
          &lt;span class="na"&gt;cache-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha&lt;/span&gt;
          &lt;span class="na"&gt;cache-to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha,mode=max&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few details worth understanding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;permissions: packages: write&lt;/code&gt;&lt;/strong&gt; grants this specific job the right to publish to your repo's package registry — GitHub Actions permissions are scoped per-job, so you only unlock what you actually need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;github.actor&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;secrets.GITHUB_TOKEN&lt;/code&gt;&lt;/strong&gt; are both provided automatically by GitHub for every workflow run — there's no manual setup required to authenticate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;docker/setup-buildx-action&lt;/code&gt;&lt;/strong&gt; enables BuildKit, which unlocks multi-platform builds (e.g., building for both &lt;code&gt;linux/amd64&lt;/code&gt; and &lt;code&gt;linux/arm64&lt;/code&gt; in one step) and smarter layer caching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cache-from&lt;/code&gt;/&lt;code&gt;cache-to: type=gha&lt;/code&gt;&lt;/strong&gt; stores build layers in GitHub's own Actions cache, so unchanged layers (like &lt;code&gt;npm ci&lt;/code&gt; when &lt;code&gt;package.json&lt;/code&gt; hasn't changed) are reused instead of rebuilt — often cutting build times dramatically.&lt;/li&gt;
&lt;li&gt;Once pushed, the image shows up under your repository's &lt;strong&gt;Packages&lt;/strong&gt; tab, versioned alongside your code and visible to anyone with repo access — no separate dashboard to check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From here, the deploy step is the same idea as before: SSH into your server (or trigger a Kubernetes rollout, or hit a webhook) and tell it to pull &lt;code&gt;ghcr.io/yourorg/yourapp:latest&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Adding Environments and Approvals
&lt;/h3&gt;

&lt;p&gt;For anything beyond a side project, you'll usually want a staging environment before production, and possibly a manual approval gate. GitHub Actions supports this natively through &lt;strong&gt;Environments&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="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy-production&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;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;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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "Deploying to production"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By configuring the &lt;code&gt;production&lt;/code&gt; environment in your repo settings with required reviewers, this job will pause and wait for a human to click "approve" before it proceeds — giving you automation with a safety checkpoint where it matters most.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Real-World Example: Backend + Frontend + VM Deploy
&lt;/h2&gt;

&lt;p&gt;Toy examples are great for learning the syntax, but real applications are rarely a single job. A more realistic setup often has a separate backend and frontend, each with their own checks, that only get built and shipped once both pass. Here's how that looks stitched together as one pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Two independent jobs run checks in parallel.&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="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;REGISTRY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io&lt;/span&gt;
  &lt;span class="na"&gt;IMAGE_BACKEND&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/myorg/webapp-backend&lt;/span&gt;
  &lt;span class="na"&gt;IMAGE_FRONTEND&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/myorg/webapp-frontend&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;backend&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;Backend (typecheck + tests)&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;defaults&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="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;
    &lt;span class="na"&gt;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@v5&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@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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;22&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;
          &lt;span class="na"&gt;cache-dependency-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend/package-lock.json&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;npx prisma generate&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;npx tsc -b --noEmit&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;frontend&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;Frontend (lint + typecheck + tests + build)&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;defaults&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="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;frontend&lt;/span&gt;
    &lt;span class="na"&gt;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@v5&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@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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;22&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;
          &lt;span class="na"&gt;cache-dependency-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;frontend/package-lock.json&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;npx tsc -b --noEmit&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="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 build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;defaults.run.working-directory&lt;/code&gt; — this scopes every step in the job to a subfolder, which is exactly what you want in a monorepo where &lt;code&gt;backend/&lt;/code&gt; and &lt;code&gt;frontend/&lt;/code&gt; each have their own &lt;code&gt;package.json&lt;/code&gt;. Because these two jobs share no dependency between them, GitHub Actions runs them &lt;strong&gt;in parallel&lt;/strong&gt;, so a slow frontend build doesn't hold up backend tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A build job waits for both, then pushes two images.&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="na"&gt;build-and-push&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 &amp;amp; Push Images&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;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;frontend&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;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@v5&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;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;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.REGISTRY }}&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;${{ github.actor }}&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.GHCR_TOKEN }}&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;docker/setup-buildx-action@v3&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 &amp;amp; push backend 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@v6&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;./backend&lt;/span&gt;
          &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./backend/Dockerfile.prod&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="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;${{ env.IMAGE_BACKEND }}:latest&lt;/span&gt;
            &lt;span class="s"&gt;${{ env.IMAGE_BACKEND }}:${{ github.sha }}&lt;/span&gt;
          &lt;span class="na"&gt;cache-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha&lt;/span&gt;
          &lt;span class="na"&gt;cache-to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha,mode=max,ignore-error=true&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 &amp;amp; push frontend 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@v6&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;./frontend&lt;/span&gt;
          &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./frontend/Dockerfile.prod&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="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;${{ env.IMAGE_FRONTEND }}:latest&lt;/span&gt;
            &lt;span class="s"&gt;${{ env.IMAGE_FRONTEND }}:${{ github.sha }}&lt;/span&gt;
          &lt;span class="na"&gt;cache-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha&lt;/span&gt;
          &lt;span class="na"&gt;cache-to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha,mode=max,ignore-error=true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;needs: [backend, frontend]&lt;/code&gt; line is the key piece — this job simply won't start until both check jobs succeed. Tagging every image with both &lt;code&gt;:latest&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;:${{ github.sha }}&lt;/code&gt; means you always have an immutable, traceable tag to roll back to, even after &lt;code&gt;:latest&lt;/code&gt; has moved on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The deploy job ships compose files, then runs everything over SSH.&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="na"&gt;deploy&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 VM&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;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build-and-push&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@v5&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;Copy docker-compose.prod.yml to VM&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/scp-action@v0.1.7&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.VM_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.VM_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.VM_SSH_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker-compose.prod.yml&lt;/span&gt;
          &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VM_PROJECT_PATH }}&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 on VM&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.2.5&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.VM_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.VM_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.VM_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;set -e&lt;/span&gt;
            &lt;span class="s"&gt;cd "${{ secrets.VM_PROJECT_PATH }}"&lt;/span&gt;

            &lt;span class="s"&gt;echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin&lt;/span&gt;

            &lt;span class="s"&gt;cat &amp;gt; .env &amp;lt;&amp;lt;'EOF'&lt;/span&gt;
            &lt;span class="s"&gt;DATABASE_URL=postgresql://${{ secrets.DB_USER }}:${{ secrets.DB_PASSWORD }}@db:5432/${{ secrets.DB_NAME }}&lt;/span&gt;
            &lt;span class="s"&gt;IMAGE_TAG=${{ github.sha }}&lt;/span&gt;
            &lt;span class="s"&gt;EOF&lt;/span&gt;

            &lt;span class="s"&gt;docker compose -f docker-compose.prod.yml pull&lt;/span&gt;
            &lt;span class="s"&gt;docker compose -f docker-compose.prod.yml up -d --remove-orphans&lt;/span&gt;
            &lt;span class="s"&gt;docker compose -f docker-compose.prod.yml exec -T backend npx prisma db push --accept-data-loss --skip-generate&lt;/span&gt;
            &lt;span class="s"&gt;docker image prune -f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few patterns here are worth stealing for your own pipelines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;scp-action&lt;/code&gt; before &lt;code&gt;ssh-action&lt;/code&gt;.&lt;/strong&gt; Config files like &lt;code&gt;docker-compose.prod.yml&lt;/code&gt; live in your repo, not on the server, so the pipeline copies the current version over before every deploy — the server never drifts from what's checked in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing &lt;code&gt;.env&lt;/code&gt; on the fly with a heredoc.&lt;/strong&gt; Nothing sensitive is ever stored on disk in your repo; secrets are injected straight from GitHub's encrypted store into a file that only exists for this deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;IMAGE_TAG=${{ github.sha }}&lt;/code&gt;&lt;/strong&gt; flows into the compose file's image reference, so &lt;code&gt;docker compose pull&lt;/code&gt; fetches the exact commit that just passed CI — never a stale or ambiguous &lt;code&gt;:latest&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--skip-generate&lt;/code&gt; on &lt;code&gt;prisma db push&lt;/code&gt;.&lt;/strong&gt; Small flag, real reason: if your Docker build already runs &lt;code&gt;prisma generate&lt;/code&gt; inside the image, doing it again after every deploy is wasted work — and on a memory-constrained VM, generating engine binaries for multiple platforms can be enough to get the process OOM-killed. Know what your tooling repeats by default and turn off what you don't need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;docker image prune -f&lt;/code&gt;&lt;/strong&gt; at the end keeps old, now-unreferenced image layers from slowly filling the VM's disk over months of deploys.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is exotic — it's the same three ideas from earlier (test → build → deploy) applied to a project with more moving parts. The complexity lives in the details of &lt;em&gt;your&lt;/em&gt; stack (a database migration, a reverse proxy, a monorepo layout), not in GitHub Actions itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Tips
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache dependencies.&lt;/strong&gt; Using &lt;code&gt;cache: 'npm'&lt;/code&gt; (or the equivalent for your package manager) can cut minutes off every run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail fast.&lt;/strong&gt; Put cheap checks (linting) before expensive ones (integration tests) so you get feedback sooner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use matrix builds&lt;/strong&gt; to test across multiple Node/Python versions or operating systems simultaneously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin action versions&lt;/strong&gt; (&lt;code&gt;@v4&lt;/code&gt;, not &lt;code&gt;@main&lt;/code&gt;) so a third-party action update doesn't silently break your pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate build from deploy&lt;/strong&gt; for anything beyond a static site — build once, deploy the same artifact to staging and production to avoid "it built differently" surprises.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;At its core, a GitHub Actions CI/CD pipeline is just YAML describing the same steps you'd otherwise do by hand — install, test, build, ship — except now they run automatically, consistently, and with a full audit trail every single time. Start small: get tests running on pull requests first. Once that feels solid, layer in automated deployment, then environments and approval gates as your project grows.&lt;/p&gt;

&lt;p&gt;The best part is that all of this lives in your repository alongside your code, versioned and reviewable just like everything else you ship.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>cicd</category>
      <category>devops</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Unlocking the Power of ChatGPT: Key Applications and Use Cases</title>
      <dc:creator>Alfat Jahan Rony</dc:creator>
      <pubDate>Sun, 04 Jun 2023 11:58:01 +0000</pubDate>
      <link>https://dev.to/alfatcse123/react-new-feature-2foh</link>
      <guid>https://dev.to/alfatcse123/react-new-feature-2foh</guid>
      <description>&lt;p&gt;ChatGPT, powered by OpenAI's advanced language model, is revolutionizing the way we interact with AI systems. This cutting-edge technology opens up a world of possibilities across various domains. In this post, we'll explore some of the most important and impactful use cases of ChatGPT, highlighting how it can be applied to enhance productivity, customer support, content generation, and more.&lt;br&gt;
&lt;strong&gt;&lt;em&gt;1.Customer Support and Chatbots:&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
ChatGPT can be leveraged to develop intelligent chatbots and virtual assistants. These AI-powered agents can engage with customers, answer inquiries, provide support, and offer personalized recommendations. By integrating ChatGPT into customer support systems, businesses can enhance user experiences, reduce response times, and scale their support capabilities.&lt;br&gt;
&lt;strong&gt;&lt;em&gt;2.Content Creation and Generation:&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
Generating high-quality content can be time-consuming and challenging. ChatGPT can assist content creators by generating blog posts, articles, social media captions, and more. With its natural language processing capabilities, ChatGPT can provide creative ideas, proofreading assistance, and even help with brainstorming new content topics.&lt;br&gt;
&lt;strong&gt;&lt;em&gt;3.Programming Assistance and Code Generation:&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
Writing code and solving programming challenges can be complex tasks. ChatGPT can serve as a programming companion by helping developers troubleshoot issues, providing code suggestions, and guiding them through logic and syntax problems. It can be a valuable resource for learning new programming languages and exploring different programming concepts.&lt;br&gt;
&lt;strong&gt;&lt;em&gt;4.Language Translation and Interpretation:&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
In today's globalized world, language barriers can hinder communication. ChatGPT can facilitate language translation and interpretation by translating text or providing real-time interpretation services. This functionality can bridge language gaps and enable effective communication between individuals who speak different languages.&lt;br&gt;
&lt;strong&gt;&lt;em&gt;5.Personal Productivity and Task Automation:&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
ChatGPT can be a personal productivity assistant, helping users manage their schedules, set reminders, create to-do lists, and provide recommendations. By leveraging ChatGPT's natural language processing capabilities, users can interact with the system in a conversational manner, making task management more intuitive and efficient.&lt;br&gt;
&lt;strong&gt;&lt;em&gt;6.Educational and Learning Support:&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
ChatGPT can serve as an interactive educational tool, offering explanations, answering questions, and providing learning resources across various subjects. It can assist students with homework, offer tutoring support, and facilitate self-paced learning. ChatGPT's ability to adapt to different learning styles can make education more engaging and accessible.&lt;br&gt;
The uses of ChatGPT are vast and varied, with applications ranging from customer support and content generation to programming assistance and educational support. As AI technology continues to advance, ChatGPT opens up new possibilities for businesses and individuals to streamline processes, enhance productivity, and provide personalized experiences. By harnessing the power of ChatGPT, we can unlock innovative solutions and revolutionize the way we interact with AI systems in various domains.&lt;/p&gt;

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