<?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: Yevhen Kotliarchuck</title>
    <description>The latest articles on DEV Community by Yevhen Kotliarchuck (@evcat6).</description>
    <link>https://dev.to/evcat6</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%2F1396466%2F3a117862-fcf9-40fe-a154-ec6619c64235.jpg</url>
      <title>DEV Community: Yevhen Kotliarchuck</title>
      <link>https://dev.to/evcat6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/evcat6"/>
    <language>en</language>
    <item>
      <title>💡 Deploying NPM Monorepos on Koyeb, Netlify, and Vercel — the Smart Way</title>
      <dc:creator>Yevhen Kotliarchuck</dc:creator>
      <pubDate>Fri, 25 Apr 2025 10:40:38 +0000</pubDate>
      <link>https://dev.to/evcat6/deploying-npm-monorepos-on-koyeb-netlify-and-vercel-the-smart-way-2an4</link>
      <guid>https://dev.to/evcat6/deploying-npm-monorepos-on-koyeb-netlify-and-vercel-the-smart-way-2an4</guid>
      <description>&lt;p&gt;Monorepos are powerful, but when it comes to deploying them to platforms like &lt;strong&gt;Koyeb&lt;/strong&gt;, &lt;strong&gt;Netlify&lt;/strong&gt;, and &lt;strong&gt;Vercel&lt;/strong&gt;, things can get messy fast.&lt;/p&gt;

&lt;p&gt;In this article, I’ll walk you through a common issue with monorepo deployments and how I solved it using &lt;strong&gt;GitHub Actions&lt;/strong&gt;, including how to &lt;strong&gt;disable auto-deployments&lt;/strong&gt; and &lt;strong&gt;trigger deploys only when specific apps/packages change&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 The Problem
&lt;/h2&gt;

&lt;p&gt;Modern deployment platforms listen to your repo and redeploy your app every time &lt;em&gt;anything&lt;/em&gt; changes. This works fine for single-app projects.&lt;/p&gt;

&lt;p&gt;But with monorepos? Not so much.&lt;/p&gt;

&lt;p&gt;Imagine this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have a monorepo with multiple packages or apps.&lt;/li&gt;
&lt;li&gt;You deploy only one package to a platform like Vercel.&lt;/li&gt;
&lt;li&gt;Then you change a file in another unrelated package.&lt;/li&gt;
&lt;li&gt;The platform still triggers a redeploy 😩&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s a waste of resources, time, and can cause unnecessary build failures.&lt;/p&gt;

&lt;p&gt;We want this instead:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Only deploy when files relevant to the deployed app/package are changed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🗂️ Example Project Structure
&lt;/h2&gt;

&lt;p&gt;To demonstrate the solution, let’s use a simple monorepo setup like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;monorepo/
├── netlify-frontend/       # Deployed to Netlify
├── vercel-nextjs-app/      # Deployed to Vercel
├── koyeb-server/           # Deployed to Koyeb
└── shared/                 # Shared code across apps (not deployed directly)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔑 Generating Deployment Credentials
&lt;/h2&gt;

&lt;p&gt;Before we disable auto-deploys, we need a way to trigger deploys manually — using &lt;strong&gt;webhooks&lt;/strong&gt; or &lt;strong&gt;API keys&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vercel – Deployment Webhook
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Navigate to your project page → &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Git&lt;/strong&gt; → &lt;strong&gt;Deploy Hooks&lt;/strong&gt; → &lt;strong&gt;Create Hook&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Netlify – Build Hook
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Go to your site dashboard → &lt;strong&gt;Site Configuration&lt;/strong&gt; → &lt;strong&gt;Build &amp;amp; Deploy&lt;/strong&gt; → &lt;strong&gt;Build Hooks&lt;/strong&gt; → &lt;strong&gt;Add Build Hook&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Koyeb – API Key
&lt;/h3&gt;

&lt;p&gt;Koyeb doesn’t use build hooks, but instead offers an API you can use to trigger deployments programmatically.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open your Koyeb dashboard → &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;API&lt;/strong&gt; → &lt;strong&gt;Create API Token&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🚫 Disabling Auto-Deployments
&lt;/h2&gt;

&lt;p&gt;Once we have the credentials, we can safely disable automatic deployments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Netlify
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Go to your site dashboard → &lt;strong&gt;Deploys&lt;/strong&gt; → Click &lt;strong&gt;Lock to stop auto publishing&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Vercel
&lt;/h3&gt;

&lt;p&gt;To prevent auto-deploys from Git integration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Navigate to your project page → &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Git&lt;/strong&gt; → &lt;strong&gt;Ignored Build Step&lt;/strong&gt; → Select &lt;strong&gt;"Don't build anything"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Koyeb
&lt;/h3&gt;

&lt;p&gt;To disable Git auto-deploy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to your service page → &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Configure Deployment Source&lt;/strong&gt; → Uncheck the &lt;strong&gt;Autodeploy&lt;/strong&gt; checkbox&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ⚙️ Step 2: GitHub Actions Workflow
&lt;/h2&gt;

&lt;p&gt;Now that auto-deploy is off, we take control.&lt;/p&gt;

&lt;p&gt;Here’s how the GitHub Actions workflow works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It checks what files or folders have changed in the commit.&lt;/li&gt;
&lt;li&gt;If the changed files are part of &lt;code&gt;netlify-frontend/&lt;/code&gt;, &lt;code&gt;vercel-nextjs-app/&lt;/code&gt;, or &lt;code&gt;koyeb-server/&lt;/code&gt;, it triggers the corresponding deployment via webhook/API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s a sample workflow:&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/deploy.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;Conditional Deploy&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="s"&gt;master&lt;/span&gt;  &lt;span class="c1"&gt;# Triggers workflow only on push to master branch&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;detect-changes&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;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Expose matched filters as outputs for use in other jobs&lt;/span&gt;
      &lt;span class="na"&gt;netlify_frontend_changed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.filter.outputs.netlify_frontend }}&lt;/span&gt;
      &lt;span class="na"&gt;vercel_nextjs_app_changed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.filter.outputs.vercel_nextjs_app }}&lt;/span&gt;
      &lt;span class="na"&gt;koyeb_server_changed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.filter.outputs.koyeb_server }}&lt;/span&gt;
      &lt;span class="na"&gt;shared_changed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.filter.outputs.shared }}&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@v3&lt;/span&gt;  &lt;span class="c1"&gt;# Checkout the repository code&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check for changes&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;filter&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;dorny/paths-filter@v3&lt;/span&gt;  &lt;span class="c1"&gt;# Detect which paths have changed&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;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;netlify_frontend:&lt;/span&gt;
              &lt;span class="s"&gt;- 'netlify-frontend/**'&lt;/span&gt;
            &lt;span class="s"&gt;vercel_nextjs_app:&lt;/span&gt;
              &lt;span class="s"&gt;- 'vercel-nextjs-app/**'&lt;/span&gt;
            &lt;span class="s"&gt;koyeb_server:&lt;/span&gt;
              &lt;span class="s"&gt;- 'koyeb-server/**'&lt;/span&gt;
            &lt;span class="s"&gt;shared:&lt;/span&gt;
              &lt;span class="s"&gt;- 'shared/**'&lt;/span&gt;

  &lt;span class="na"&gt;deploy-netlify-frontend&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;detect-changes&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;needs.detect-changes.outputs.netlify_frontend_changed == 'true' || needs.detect-changes.outputs.shared_changed == 'true'&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;Trigger Netlify Deploy&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;curl -X POST -d '{}' https://api.netlify.com/build_hooks/${{ secrets.NETLIFY_FRONTEND_DEPLOY_KEY }}&lt;/span&gt;
        &lt;span class="c1"&gt;# Uses Netlify build hook URL to trigger deploy&lt;/span&gt;

  &lt;span class="na"&gt;deploy-nextjs-app&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;detect-changes&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;needs.detect-changes.outputs.vercel_nextjs_app_changed == 'true' || needs.detect-changes.outputs.shared_changed == 'true'&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;Trigger Vercel Deploy&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;curl -X POST -d '{}' https://api.vercel.com/v1/integrations/deploy/${{ secrets.VERCEL_APP_DEPLOY_KEY }}&lt;/span&gt;
        &lt;span class="c1"&gt;# Uses Vercel deploy webhook to trigger a deploy&lt;/span&gt;

  &lt;span class="na"&gt;deploy-koyeb-server&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;detect-changes&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;needs.detect-changes.outputs.koyeb_server_changed == 'true' || needs.detect-changes.outputs.shared_changed == 'true'&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;Install Koyeb CLI&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;curl -fsSL https://raw.githubusercontent.com/koyeb/koyeb-cli/master/install.sh | sh&lt;/span&gt;
          &lt;span class="s"&gt;echo "$HOME/.koyeb/bin" &amp;gt;&amp;gt; $GITHUB_PATH&lt;/span&gt;
        &lt;span class="c1"&gt;# Installs the Koyeb CLI and adds it to the 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;Verify CLI&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;koyeb version&lt;/span&gt;
        &lt;span class="c1"&gt;# Optional step to verify the CLI is working&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;Trigger redeploy&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;koyeb service redeploy ${{ secrets.KOYEB_SERVICE_NAME }} \&lt;/span&gt;
            &lt;span class="s"&gt;--app ${{ secrets.KOYEB_APP_NAME }} \&lt;/span&gt;
            &lt;span class="s"&gt;--token ${{ secrets.KOYEB_API_TOKEN }}&lt;/span&gt;
        &lt;span class="c1"&gt;# Redeploys the Koyeb service using the CLI and secret tokens&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎯 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;With this setup:&lt;/p&gt;

&lt;p&gt;✅ You stop wasting build minutes&lt;br&gt;&lt;br&gt;
✅ You only deploy what's actually changed&lt;br&gt;&lt;br&gt;
✅ You fully control the pipeline with GitHub Actions&lt;/p&gt;

&lt;p&gt;Let your monorepo breathe — deploy smarter, not harder.&lt;/p&gt;

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