<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Console-Alt-Deploy]]></title><description><![CDATA[Here to break down cloud deployment, DevOps, and backend development to help you ship faster!]]></description><link>https://blog.dannysantino.com</link><generator>RSS for Node</generator><lastBuildDate>Thu, 30 Apr 2026 00:54:46 GMT</lastBuildDate><atom:link href="https://blog.dannysantino.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Setting up a multi-cloud deployment pipeline with CircleCI, AWS & Google Cloud]]></title><description><![CDATA[Multi-cloud application deployment strategies offer a host of benefits, such as flexibility, resilience, and avoiding cloud provider lock-in. Spreading workloads across different clouds not only helps cut costs but also lets you pick the best tools a...]]></description><link>https://blog.dannysantino.com/multi-cloud-deployment-pipeline-circleci</link><guid isPermaLink="true">https://blog.dannysantino.com/multi-cloud-deployment-pipeline-circleci</guid><category><![CDATA[CircleCI]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[multi-cloud]]></category><category><![CDATA[deployment]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Google]]></category><category><![CDATA[google cloud]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Danny Santino]]></dc:creator><pubDate>Wed, 17 Sep 2025 08:39:56 GMT</pubDate><content:encoded><![CDATA[<p>Multi-cloud application deployment strategies offer a host of benefits, such as flexibility, resilience, and avoiding cloud provider lock-in. Spreading workloads across different clouds not only helps cut costs but also lets you pick the best tools and regions to deliver a smoother experience for your users.</p>
<p>This tutorial demonstrates how to set up a multi-cloud architecture with Amazon Web Services (AWS) and Google Cloud. Working with a JavaScript monorepo, you will learn how to deploy a Node.js + MySQL server app to AWS Elastic Container Service (ECS) with the EC2 (Elastic Compute Cloud) launch type, and a React client to Google Cloud Run using <a target="_blank" href="https://circleci.com/docs/orbs/author/orb-concepts/">CircleCI orbs</a>.</p>
<p>By the end, you will have a fully functional, production-ready pipeline that delivers your app across two major clouds. You will also learn valuable tips to apply to real-world projects.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow this tutorial, here is a checklist of what you will need:</p>
<ul>
<li><p>A <a target="_blank" href="https://circleci.com/signup">CircleCI account</a>.</p>
</li>
<li><p><a target="_blank" href="https://app.docker.com/signup">A Docker account</a> to publish images to Docker Hub.</p>
</li>
<li><p>An AWS account with the AWS CLI installed and configured. Follow the instructions on the <a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/cli-authentication-user.html">Authenticating using IAM user credentials for the AWS CLI</a> page to:</p>
<ul>
<li><p>Create an IAM user with administrative access (you should avoid using the root user to spin up AWS resources).</p>
</li>
<li><p>Generate two sets of credentials for CLI and CircleCI.</p>
</li>
<li><p>Install the AWS CLI on your machine.</p>
</li>
<li><p>Add your credentials and configure the IAM user profile for CLI use.</p>
</li>
</ul>
</li>
<li><p>A Google Cloud account with billing enabled and the <a target="_blank" href="https://cloud.google.com/sdk/docs/install"><code>gcloud</code> CLI</a> configured.</p>
</li>
<li><p>A terminal to run bash scripts, with OpenSSL and <code>envsubst</code> installed.</p>
</li>
<li><p><a target="_blank" href="https://git-scm.com/">Git</a> installed on your local machine.</p>
</li>
<li><p>A <a target="_blank" href="https://github.com/signup">GitHub account</a>.</p>
</li>
<li><p>An IDE or code editor of your choice.</p>
</li>
</ul>
<h2 id="heading-preparing-the-cloud-infrastructure">Preparing the cloud infrastructure</h2>
<p>Before getting into the CI/CD pipeline, you need to set up the infrastructure where CircleCI will deploy the client and server apps. Once created, both services will be empty until the first CI/CD pipeline runs. After the pipeline config is ready, you will push your code to trigger CircleCI and automatically update the services. We will kick things off with AWS.</p>
<p>First, make sure you have the application code on your machine. <a target="_blank" href="https://github.com/dannysantino/cara-store-catalog/">Fork the repository</a> on GitHub and clone it:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> &lt;your-fork-url&gt;

<span class="hljs-comment"># navigate into the project directory</span>
<span class="hljs-built_in">cd</span> cara-store-catalog
</code></pre>
<h3 id="heading-provisioning-aws-resources">Provisioning AWS resources</h3>
<p>Inside the <code>deployments/</code> directory, you will find two main folders, <code>ecs/</code> and <code>cloud-run/</code>. Within them are scripts and configuration files to automate the process of provisioning resources on their respective cloud platforms.</p>
<p>One such file is <code>ecs/scripts/export-env.template.sh</code>, which exports the custom environment variables required for setup. Rename this file to <code>export-env.sh</code> and fill in the following values:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> AWS_PROFILE=
<span class="hljs-built_in">export</span> DOCKERHUB_USERNAME=
<span class="hljs-built_in">export</span> MYSQL_PASSWORD=
<span class="hljs-built_in">export</span> MYSQL_ROOT_PASSWORD=
</code></pre>
<p>Next, rename <code>.env.circleci.template</code> to <code>.env.circleci</code>. This file contains a list of all the environment variables you’ll need to add to CircleCI. The setup script will auto-populate some of them (by executing <code>set-circleci-env.sh</code>), so all you have to do is copy and paste into CircleCI when needed.</p>
<p>Now, you can run the script to create the deployment resources. The script assumes your AWS account already has a default Virtual Private Cloud (VPC) configured with networking components like subnets and an internet gateway.</p>
<p>From the project’s root directory, enter these commands:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># add execution permissions</span>
chmod +x deployments/ecs/scripts/export-env.sh deployments/ecs/scripts/set-circleci-env.sh deployments/ecs/scripts/setup-ecs.sh

<span class="hljs-comment"># run the script</span>
<span class="hljs-built_in">source</span> deployments/ecs/scripts/setup-ecs.sh
</code></pre>
<p>Here’s an overview of what this does:</p>
<ul>
<li><p>Creates an ECS cluster.</p>
</li>
<li><p>Sets up AWS Secrets Manager by:</p>
<ul>
<li><p>Substituting the placeholders in <code>templates/ecs-secrets.template.json</code> using the values exported from <code>export-env.sh</code>.</p>
</li>
<li><p>Uploading the env variables in <code>init/ecs-secrets.json</code>.</p>
</li>
</ul>
</li>
<li><p>Creates IAM and Task Execution roles for the EC2 instance.</p>
</li>
<li><p>Creates an application load balancer (ALB) and defines two security groups for the ALB and the ECS instance. This opens up the ALB for public access while it communicates requests to the server app running on the ECS instance.</p>
</li>
<li><p>Generates and uploads a self-signed TLS certificate to allow <code>https</code> support for the ALB URL. This ensures the client app can communicate with the server securely.</p>
</li>
<li><p>Creates a key pair to allow secure SSH access to the EC2 instance.</p>
</li>
<li><p>Launches an EC2 instance and uploads the database initialization script <code>server/db/init.sql</code>.</p>
</li>
<li><p>Runs <code>set-circleci-env.sh</code> to populate <code>.env.circleci</code> with the available values.</p>
</li>
</ul>
<p>The setup script stops short of registering the task definition and creating the ECS service to keep infrastructure setup separate from deployment. If everything succeeds, you should see this message printed to the console:</p>
<pre><code class="lang-bash">[SUCCESS] ECS infrastructure with EC2 launch <span class="hljs-built_in">type</span> successfully created
          Service will be launched and deployed via CircleCI
</code></pre>
<p>After the script completes, open the ECS and EC2 dashboards in your AWS account to confirm the creation of resources such as the load balancer, target group, and ECS cluster.</p>
<p>Inside the <code>scripts/</code> folder are a few other scripts like <code>pause-ecs.sh</code> and <code>resume-ecs.sh</code>. Use these to suspend or restart the ECS service temporarily to save costs.</p>
<h3 id="heading-provisioning-google-cloud-resources">Provisioning Google Cloud resources</h3>
<p>You might be wondering why the React app is Dockerized instead of hosted as a static website on Google Cloud Storage. While static hosting does offer simplicity, using Docker provides a unified way to manage the client app across environments. It allows you to spin up multiple instances of your app, enabling easy testing, scaling, and consistent behavior across deployments.</p>
<p>The Google Cloud Run setup files are in the <code>deployments/cloud-run/</code> directory. Rename the <code>cloud-run/scripts/export-env.template.sh</code> file to <code>export-env.sh</code> and update these values from your Google Cloud account:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> GCP_REGION=
<span class="hljs-built_in">export</span> BILLING_ACCOUNT_NAME=<span class="hljs-string">""</span>
</code></pre>
<p>Just like with AWS, run the setup script for Google Cloud Run from the project’s root directory:</p>
<pre><code class="lang-bash">
<span class="hljs-comment"># add execution permissions</span>
chmod +x deployments/cloud-run/scripts/export-env.sh deployments/cloud-run/scripts/set-circleci-env.sh deployments/cloud-run/scripts/setup-cloud-run.sh

<span class="hljs-comment"># run the script</span>
<span class="hljs-built_in">source</span> deployments/cloud-run/scripts/setup-cloud-run.sh
</code></pre>
<p>What this script does:</p>
<ul>
<li><p>Creates a new project on Google Cloud.</p>
</li>
<li><p>Links the project to the specified billing account.</p>
</li>
<li><p>Enables all required APIs.</p>
</li>
<li><p>Creates a runtime service account for the Cloud Run instance and a deployer service account for CircleCI.</p>
</li>
<li><p>Creates a JSON key for CircleCI to authenticate with Google Cloud.</p>
</li>
<li><p>Runs the Cloud Run <code>set-circleci-env.sh</code> to populate values into <code>.env.circleci</code>.</p>
</li>
</ul>
<p>A successful setup should print a message like this to the console:</p>
<pre><code class="lang-bash">[SUCCESS] Cloud Run service environment prepared:
          Service: carastore-client
          App will be deployed via CircleCI
</code></pre>
<p>With both AWS and Google Cloud configured, this concludes the infrastructure setup portion of this tutorial. Next up is the CI/CD pipeline.</p>
<h2 id="heading-configuring-the-cicd-pipeline-for-deployment">Configuring the CI/CD pipeline for deployment</h2>
<p>Since we’re working with a monorepo, the best approach is to use CircleCI’s dynamic configuration. This allows the configuration of different workflows to run based on file changes. If you are unfamiliar with the concept, check out my tutorial on <a target="_blank" href="https://blog.dannysantino.com/circleci-dynamic-config-javascript-monorepo">dynamic configuration with CircleCI</a>.</p>
<p>In the project’s root directory, create the following folder and files:</p>
<pre><code class="lang-bash">mkdir .circleci

<span class="hljs-built_in">cd</span> .circleci

touch config.yml continue_config.yml
</code></pre>
<h3 id="heading-the-setup-file">The setup file</h3>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">To avoid pipeline errors, maintain proper indentation in your YAML files.</div>
</div>

<p>Add this code to <code>config.yml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-number">2.1</span>

<span class="hljs-attr">setup:</span> <span class="hljs-literal">true</span>

<span class="hljs-attr">orbs:</span>
  <span class="hljs-attr">path-filtering:</span> <span class="hljs-string">circleci/path-filtering@2.0.1</span>

<span class="hljs-attr">workflows:</span>
  <span class="hljs-attr">filter-path:</span>
    <span class="hljs-attr">jobs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">path-filtering/filter:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">detect-modified-directories</span>
          <span class="hljs-attr">mapping:</span> <span class="hljs-string">|
            server/.* run-server-jobs true
            client/.* run-client-jobs true
            .circleci/.* run-server-jobs true
            .circleci/.* run-client-jobs true
            deployments/ecs/.* run-task-job true
</span>          <span class="hljs-attr">base-revision:</span> <span class="hljs-string">main</span>
</code></pre>
<p>This configuration uses the <code>filter</code> job from the <code>path-filtering</code> orb to detect changes and set these pipeline parameters:</p>
<ul>
<li><p><code>run-server-jobs</code>: triggers the server workflow when <code>server/</code> files change.</p>
</li>
<li><p><code>run-client-jobs</code>: triggers the client workflow when <code>client/</code> files change.</p>
</li>
<li><p><code>run-task-job</code> triggers the server deployment job within the server workflow when changes occur in the <code>deployments/ecs/</code> folder. This ensures CircleCI updates the task definition and the ECS service accordingly.</p>
</li>
</ul>
<h3 id="heading-continuation-configuration">Continuation configuration</h3>
<p>After setting the pipeline parameters, CircleCI automatically executes the continuation config file and passes those pipeline values to it.</p>
<p>Open <code>continue_config.yml</code> and add this:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-number">2.1</span>

<span class="hljs-attr">parameters:</span>
  <span class="hljs-attr">run-server-jobs:</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">boolean</span>
    <span class="hljs-attr">default:</span> <span class="hljs-literal">false</span>
  <span class="hljs-attr">run-client-jobs:</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">boolean</span>
    <span class="hljs-attr">default:</span> <span class="hljs-literal">false</span>
  <span class="hljs-attr">run-task-job:</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">boolean</span>
    <span class="hljs-attr">default:</span> <span class="hljs-literal">false</span>

<span class="hljs-attr">orbs:</span>
  <span class="hljs-attr">gcp-cli:</span> <span class="hljs-string">circleci/gcp-cli@3.2.2</span>
  <span class="hljs-attr">aws-ecs:</span> <span class="hljs-string">circleci/aws-ecs@7.1.0</span>
  <span class="hljs-attr">aws-cli:</span> <span class="hljs-string">circleci/aws-cli@5.4.1</span>

<span class="hljs-attr">executors:</span>
  <span class="hljs-attr">node-exec:</span>
    <span class="hljs-attr">docker:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">image:</span> <span class="hljs-string">node:22.17-alpine3.22</span>
  <span class="hljs-attr">base-exec:</span>
    <span class="hljs-attr">docker:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">image:</span> <span class="hljs-string">cimg/base:current-24.04</span>

<span class="hljs-attr">commands:</span>
  <span class="hljs-attr">installdeps:</span>
    <span class="hljs-attr">description:</span> <span class="hljs-string">"Install dependencies"</span>
    <span class="hljs-attr">parameters:</span>
      <span class="hljs-attr">directory:</span>
        <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">checkout:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">~/project</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">restore_cache:</span>
          <span class="hljs-attr">keys:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">v1-&lt;&lt;</span> <span class="hljs-string">parameters.directory</span> <span class="hljs-string">&gt;&gt;-deps-{{</span> <span class="hljs-string">checksum</span> <span class="hljs-string">"package.json"</span> <span class="hljs-string">}}-{{</span> <span class="hljs-string">checksum</span> <span class="hljs-string">"package-lock.json"</span> <span class="hljs-string">}}</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">v1-&lt;&lt;</span> <span class="hljs-string">parameters.directory</span> <span class="hljs-string">&gt;&gt;-deps-{{</span> <span class="hljs-string">checksum</span> <span class="hljs-string">"package.json"</span> <span class="hljs-string">}}</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">v1-&lt;&lt;</span> <span class="hljs-string">parameters.directory</span> <span class="hljs-string">&gt;&gt;-deps-</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">dependencies</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">npm</span> <span class="hljs-string">ci</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">save_cache:</span>
          <span class="hljs-attr">key:</span> <span class="hljs-string">v1-&lt;&lt;</span> <span class="hljs-string">parameters.directory</span> <span class="hljs-string">&gt;&gt;-deps-{{</span> <span class="hljs-string">checksum</span> <span class="hljs-string">"package.json"</span> <span class="hljs-string">}}-{{</span> <span class="hljs-string">checksum</span> <span class="hljs-string">"package-lock.json"</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">paths:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">node_modules</span>
  <span class="hljs-attr">get-image-tag:</span>
    <span class="hljs-attr">description:</span> <span class="hljs-string">"Retrieve tag for Docker image"</span>
    <span class="hljs-attr">parameters:</span>
      <span class="hljs-attr">directory:</span>
        <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Export</span> <span class="hljs-string">version</span> <span class="hljs-string">from</span> <span class="hljs-string">package.json</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">|
            IMAGE_TAG=$(jq -r '.version' ~/project/&lt;&lt; parameters.directory &gt;&gt;/package.json)
            echo "export IMAGE_TAG=$IMAGE_TAG" &gt;&gt; $BASH_ENV
            echo "IMAGE_TAG: $IMAGE_TAG"
            source $BASH_ENV</span>
</code></pre>
<p>The bulk of this code declares reusable elements to avoid unnecessary code duplication:</p>
<ul>
<li><p>Default values for the pipeline parameters.</p>
</li>
<li><p>CircleCI orbs to simplify the deployment setup.</p>
</li>
<li><p>Executors to define the execution environment for the steps in each job.</p>
</li>
<li><p>Commands to run across multiple jobs:</p>
<ul>
<li><p><code>installdeps</code> installs <code>npm</code> dependencies, stores them in a cache, and restores them in subsequent builds.</p>
</li>
<li><p><code>get-image-tag</code> dynamically retrieves the tag number from each app’s <code>package.json</code> file.</p>
</li>
</ul>
</li>
</ul>
<p>Start defining the jobs for the pipeline by adding the test jobs:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">test-client:</span>
    <span class="hljs-attr">executor:</span> <span class="hljs-string">node-exec</span>
    <span class="hljs-attr">working_directory:</span> <span class="hljs-string">~/project/client</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">installdeps:</span>
          <span class="hljs-attr">directory:</span> <span class="hljs-string">client</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">client</span> <span class="hljs-string">tests</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">npm</span> <span class="hljs-string">test</span>

  <span class="hljs-attr">test-server:</span>
    <span class="hljs-attr">executor:</span> <span class="hljs-string">base-exec</span>
    <span class="hljs-attr">working_directory:</span> <span class="hljs-string">~/project/server</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">checkout:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">~/project</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">setup_remote_docker:</span>
          <span class="hljs-attr">docker_layer_caching:</span> <span class="hljs-literal">true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">get-image-tag:</span>
          <span class="hljs-attr">directory:</span> <span class="hljs-string">server</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Spin</span> <span class="hljs-string">up</span> <span class="hljs-string">containers</span> <span class="hljs-string">and</span> <span class="hljs-string">run</span> <span class="hljs-string">server</span> <span class="hljs-string">tests</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">|
            docker images
            docker compose -f compose.yaml -f compose.cicd.yaml up --build -d
            docker exec -it --user root nodejs-server-prod npm install
            docker exec -it nodejs-server-prod npm test
</span>      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Stop</span> <span class="hljs-string">and</span> <span class="hljs-string">remove</span> <span class="hljs-string">containers</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">docker</span> <span class="hljs-string">compose</span> <span class="hljs-string">down</span> <span class="hljs-string">-v</span>
</code></pre>
<p><code>test-client</code> uses the <code>node-exec</code> executor to maintain consistency with the Node.js version defined in the client app’s Dockerfile. It invokes the reusable <code>installdeps</code> command and then calls the test command specified in the client app’s <code>package.json</code> file. The tests must be run directly on the source code since they can’t run effectively on a static Dockerized build served by NGINX.</p>
<p>The <code>test-server</code> job’s structure is a little different. It uses the <code>base-exec</code> executor, which is CircleCI’s official Ubuntu Docker image. Here, tests <em>can</em> run on the server app’s Docker image, so the job spins up a container, installs dependencies as the root user, and runs tests inside it. The <code>compose.cicd.yaml</code> file is a Docker Compose merge file which specifies the production configuration. In contrast, the <code>compose.override.yaml</code> file in the <code>server/</code> folder contains the development configuration.</p>
<p>Update <code>continue_config.yml</code> with the build job:</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Remember to indent each job under the <code>jobs</code> key, as in <code>test-client</code> and <code>test-server</code> above.</div>
</div>

<pre><code class="lang-yaml">  <span class="hljs-attr">build-docker-image:</span>
    <span class="hljs-attr">description:</span> <span class="hljs-string">Build</span> <span class="hljs-string">and</span> <span class="hljs-string">publish</span> <span class="hljs-string">&lt;&lt;</span> <span class="hljs-string">parameters.service</span> <span class="hljs-string">&gt;&gt;</span> <span class="hljs-string">Docker</span> <span class="hljs-string">image</span>
    <span class="hljs-attr">parameters:</span>
      <span class="hljs-attr">service:</span>
        <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
      <span class="hljs-attr">build_context:</span>
        <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
    <span class="hljs-attr">executor:</span> <span class="hljs-string">base-exec</span>
    <span class="hljs-attr">working_directory:</span> <span class="hljs-string">~/project/&lt;&lt;</span> <span class="hljs-string">parameters.service</span> <span class="hljs-string">&gt;&gt;</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">checkout:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">~/project</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">setup_remote_docker:</span>
          <span class="hljs-attr">docker_layer_caching:</span> <span class="hljs-literal">true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">get-image-tag:</span>
          <span class="hljs-attr">directory:</span> <span class="hljs-string">&lt;&lt;</span> <span class="hljs-string">parameters.service</span> <span class="hljs-string">&gt;&gt;</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">&lt;&lt;</span> <span class="hljs-string">parameters.service</span> <span class="hljs-string">&gt;&gt;</span> <span class="hljs-string">image</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">|
            docker build \
              -t $DOCKERHUB_USERNAME/carastore-&lt;&lt; parameters.service &gt;&gt;:$IMAGE_TAG \
              -t $DOCKERHUB_USERNAME/carastore-&lt;&lt; parameters.service &gt;&gt;:latest \
              &lt;&lt; parameters.build_context &gt;&gt;
</span>      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Authenticate</span> <span class="hljs-string">and</span> <span class="hljs-string">push</span> <span class="hljs-string">image</span> <span class="hljs-string">to</span> <span class="hljs-string">Docker</span> <span class="hljs-string">Hub</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">|
            echo "$DOCKERHUB_PASSWORD" | docker login -u $DOCKERHUB_USERNAME --password-stdin
            docker push -a $DOCKERHUB_USERNAME/carastore-&lt;&lt; parameters.service &gt;&gt;</span>
</code></pre>
<p>Both client and server apps share a similar build process, so the <code>build-docker-image</code> takes on a parameterized structure to make it reusable across workflows.</p>
<p>Add the client deployment job:</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">deploy-client:</span>
    <span class="hljs-attr">executor:</span> <span class="hljs-string">gcp-cli/default</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">checkout</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">get-image-tag:</span>
          <span class="hljs-attr">directory:</span> <span class="hljs-string">client</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">gcp-cli/setup:</span>
          <span class="hljs-attr">gcloud_service_key:</span> <span class="hljs-string">GCLOUD_SERVICE_KEY</span>
          <span class="hljs-attr">google_compute_region:</span> <span class="hljs-string">GCP_REGION</span>
          <span class="hljs-attr">google_project_id:</span> <span class="hljs-string">GOOGLE_PROJECT_ID</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">to</span> <span class="hljs-string">Google</span> <span class="hljs-string">Cloud</span> <span class="hljs-string">Run</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">|
            IMAGE="docker.io/$DOCKERHUB_USERNAME/carastore-client:$IMAGE_TAG"
</span>
            <span class="hljs-string">gcloud</span> <span class="hljs-string">run</span> <span class="hljs-string">deploy</span> <span class="hljs-string">"$SERVICE_NAME"</span> <span class="hljs-string">\</span>
              <span class="hljs-string">--image</span> <span class="hljs-string">"$IMAGE"</span> <span class="hljs-string">\</span>
              <span class="hljs-string">--service-account</span> <span class="hljs-string">"$RUNTIME_SA_EMAIL"</span> <span class="hljs-string">\</span>
              <span class="hljs-string">--allow-unauthenticated</span> <span class="hljs-string">\</span>
              <span class="hljs-string">--region</span> <span class="hljs-string">"$GCP_REGION"</span> <span class="hljs-string">\</span>
              <span class="hljs-string">--platform</span> <span class="hljs-string">managed</span> <span class="hljs-string">\</span>
              <span class="hljs-string">--port</span> <span class="hljs-string">$CLIENT_APP_PORT</span> <span class="hljs-string">\</span>
              <span class="hljs-string">--cpu</span> <span class="hljs-number">1</span> <span class="hljs-string">\</span>
              <span class="hljs-string">--memory</span> <span class="hljs-string">512Mi</span> <span class="hljs-string">\</span>
              <span class="hljs-string">--min-instances</span> <span class="hljs-number">0</span> <span class="hljs-string">\</span>
              <span class="hljs-string">--max-instances</span> <span class="hljs-number">1</span> <span class="hljs-string">\</span>
              <span class="hljs-string">--set-env-vars</span> <span class="hljs-string">VITE_API_URL=$VITE_API_URL</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Verify</span> <span class="hljs-string">deployment</span> <span class="hljs-string">success</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">|
            URL=$(gcloud run services describe "$SERVICE_NAME" \
              --region "$GCP_REGION" \
              --format='value(status.url)')
            echo "[INFO] Service URL: $URL"
            for i in {1..30}; do
              STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$URL")
              if [ "$STATUS" -eq 200 ]; then
                echo "[SUCCESS] Deployment verified"
                exit 0
              fi
              echo "[WARN] Got $STATUS, retrying in 10s... ($i/30)"
              sleep 10
            done
            echo "[ERROR] Deployment verification timed out after 5 minutes"
            exit 1</span>
</code></pre>
<p>Here, the previously defined <code>gcp-cli</code> orb installs and configures the <code>gcloud</code> CLI. You already set up the infrastructure by running the <code>setup-cloud-run.sh</code> script, so this job handles the client app deployment to Google Cloud Run. The final step confirms whether the deployment was successful.</p>
<p>Next, add the server deployment job:</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">deploy-server:</span>
    <span class="hljs-attr">executor:</span> <span class="hljs-string">base-exec</span>
    <span class="hljs-attr">working_directory:</span> <span class="hljs-string">~/project/deployments/ecs</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">checkout:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">~/project</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">get-image-tag:</span>
          <span class="hljs-attr">directory:</span> <span class="hljs-string">server</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Substitute</span> <span class="hljs-string">env</span> <span class="hljs-string">placeholders</span> <span class="hljs-string">in</span> <span class="hljs-string">task</span> <span class="hljs-string">definition</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">|
            envsubst &lt; templates/task-definition.template.json &gt; task-definition.json
</span>      <span class="hljs-bullet">-</span> <span class="hljs-attr">aws-cli/setup:</span>
          <span class="hljs-attr">aws_access_key_id:</span> <span class="hljs-string">$AWS_ACCESS_KEY_ID</span>
          <span class="hljs-attr">aws_secret_access_key:</span> <span class="hljs-string">$AWS_SECRET_ACCESS_KEY</span>
          <span class="hljs-attr">region:</span> <span class="hljs-string">$AWS_REGION</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">aws-ecs/update_task_definition_from_json:</span>
          <span class="hljs-attr">region:</span> <span class="hljs-string">$AWS_REGION</span>
          <span class="hljs-attr">task_definition_json:</span> <span class="hljs-string">task-definition.json</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">aws-ecs/update_service:</span>
          <span class="hljs-attr">region:</span> <span class="hljs-string">$AWS_REGION</span>
          <span class="hljs-attr">family:</span> <span class="hljs-string">"$MY_APP_PREFIX-server"</span>
          <span class="hljs-attr">service_name:</span> <span class="hljs-string">"$MY_APP_PREFIX-service"</span>
          <span class="hljs-attr">cluster:</span> <span class="hljs-string">"$MY_APP_PREFIX-cluster"</span>
          <span class="hljs-attr">create_service:</span> <span class="hljs-literal">true</span>
          <span class="hljs-attr">desired_count:</span> <span class="hljs-string">"1"</span>
          <span class="hljs-attr">container_name:</span> <span class="hljs-string">"nodejs-server"</span>
          <span class="hljs-attr">container_port:</span> <span class="hljs-string">"5000"</span>
          <span class="hljs-attr">target_group:</span> <span class="hljs-string">$TG_ARN</span>
          <span class="hljs-attr">skip_task_definition_registration:</span> <span class="hljs-literal">true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">aws-ecs/verify_revision_is_deployed:</span>
          <span class="hljs-attr">region:</span> <span class="hljs-string">$AWS_REGION</span>
          <span class="hljs-attr">family:</span> <span class="hljs-string">"$MY_APP_PREFIX-server"</span>
          <span class="hljs-attr">service_name:</span> <span class="hljs-string">"$MY_APP_PREFIX-service"</span>
          <span class="hljs-attr">cluster:</span> <span class="hljs-string">"$MY_APP_PREFIX-cluster"</span>
          <span class="hljs-attr">task_definition_arn:</span> <span class="hljs-string">$CCI_ORB_AWS_ECS_REGISTERED_TASK_DFN</span>
          <span class="hljs-attr">max_poll_attempts:</span> <span class="hljs-number">20</span>

  <span class="hljs-attr">deployment-coordinator:</span>
    <span class="hljs-attr">type:</span> <span class="hljs-literal">no</span><span class="hljs-string">-op</span>
</code></pre>
<p>There are two CircleCI orbs working hand-in-hand here to execute the server deployment: <code>aws-cli</code> to install and configure the AWS CLI, and <code>aws-ecs</code> to manage the core deployment process.</p>
<p>This job can be summarized in four major steps:</p>
<ol>
<li><p><code>envsubst</code> replaces placeholders in <code>task-definition.template.json</code> with values from CircleCI project environment variables and outputs a valid JSON task definition.</p>
</li>
<li><p>The in-built <code>aws-ecs/update_task_definition_from_json</code> command registers a new task definition with this file.</p>
</li>
<li><p><code>aws-ecs/update_service</code> handles the core service deployment with the specified parameters.</p>
</li>
<li><p>And finally, <code>aws-ecs/verify_revision_is_deployed</code> confirms if the rollout was successful.</p>
</li>
</ol>
<p>The <code>deployment-coordinator</code> job is a no-op that performs no specific action or consumes credits. Its purpose is to ensure the server app gets re-deployed if only the task definition changes.</p>
<p>This will become clearer after adding the final piece of the config, workflows:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">workflows:</span>
  <span class="hljs-attr">test-build-and-deploy-client:</span>
    <span class="hljs-attr">when:</span> <span class="hljs-string">&lt;&lt;</span> <span class="hljs-string">pipeline.parameters.run-client-jobs</span> <span class="hljs-string">&gt;&gt;</span>
    <span class="hljs-attr">jobs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">test-client</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">build-docker-image:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">build-client-image</span>
          <span class="hljs-attr">service:</span> <span class="hljs-string">client</span>
          <span class="hljs-attr">build_context:</span> <span class="hljs-string">.</span>
          <span class="hljs-attr">requires:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">test-client</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">deploy-client:</span>
          <span class="hljs-attr">requires:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">build-client-image</span>

  <span class="hljs-attr">build-test-and-deploy-server:</span>
    <span class="hljs-attr">when:</span>
      <span class="hljs-attr">or:</span> [<span class="hljs-string">&lt;&lt;</span> <span class="hljs-string">pipeline.parameters.run-server-jobs</span> <span class="hljs-string">&gt;&gt;</span>, <span class="hljs-string">&lt;&lt;</span> <span class="hljs-string">pipeline.parameters.run-task-job</span> <span class="hljs-string">&gt;&gt;</span>]
    <span class="hljs-attr">jobs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">build-docker-image:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">build-server-image</span>
          <span class="hljs-attr">service:</span> <span class="hljs-string">server</span>
          <span class="hljs-attr">build_context:</span> <span class="hljs-string">--target</span> <span class="hljs-string">prod</span> <span class="hljs-string">.</span>
          <span class="hljs-attr">filters:</span>
            <span class="hljs-string">pipeline.parameters.run-server-jobs</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">test-server:</span>
          <span class="hljs-attr">requires:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">build-server-image</span>
          <span class="hljs-attr">filters:</span>
            <span class="hljs-string">pipeline.parameters.run-server-jobs</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">deployment-coordinator</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">deploy-server:</span>
          <span class="hljs-attr">requires:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">test-server</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">deployment-coordinator</span>
</code></pre>
<p>Workflows determine how (and if) the outlined jobs should run. In both of these, CircleCI will only trigger the listed jobs “when” the specified pipeline parameters evaluate to <code>true</code>.</p>
<p>A brief explanation of what is going on in the <code>build-test-and-deploy-server</code> workflow:</p>
<ul>
<li><p>The logical <code>or</code> operator means the workflow will only run when one OR both pipeline parameters are true.</p>
</li>
<li><p>If both or only the <code>run-server-jobs</code> pipeline parameter is true, it runs all jobs.</p>
</li>
<li><p>The <code>filters</code> rule skips the <code>build-docker-image</code> and <code>test-server</code> jobs if only <code>run-task-job</code> is true. In that case, the no-op <code>deployment-coordinator</code> ensures <code>deploy-server</code> still runs, because it remains as a dependency, even when <code>test-server</code> gets filtered out. Without it, <code>deploy-server</code> would have no valid dependency, and CircleCI would skip it.</p>
</li>
<li><p>The <code>requires</code> key stalls execution until the dependent job attains the default <code>success</code> status.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Note on workflow job order</strong></div>
</div>

<p>While the typical flow is build → test → deploy in most CI/CD pipelines, the client and server workflows diverge slightly in practice:</p>
<ul>
<li><p>Client workflow runs tests before the Docker build, since tests cannot run meaningfully on a static image.</p>
</li>
<li><p>Server workflow runs the Docker build before tests, since tests depend on the built production image.</p>
</li>
</ul>
<p>So, while technical constraints mean a disparity in order, the overall progression is the same: validate and package, then deploy.</p>
<h2 id="heading-running-your-deployment-pipeline-on-circleci">Running your deployment pipeline on CircleCI</h2>
<p>To connect your project to CircleCI, start by following the instructions on the <a target="_blank" href="https://circleci.com/docs/guides/getting-started/create-project/#set-up-a-project">Set up a project</a> page. CircleCI will prompt you with a few options. For now, select <strong>Commit a starter CI pipeline to a new branch.</strong> This will immediately trigger a successful test pipeline on a new branch, confirming CircleCI has connected your project correctly.</p>
<p>Next, open the <code>deployments/env.circleci</code> file you renamed earlier. It contains a list of all the environment variables required for CircleCI and should have the following values pre-populated:</p>
<ul>
<li><p><code>ACCOUNT_ID</code></p>
</li>
<li><p><code>AWS_REGION</code></p>
</li>
<li><p><code>GCP_REGION</code></p>
</li>
<li><p><code>RUNTIME_SA_EMAIL</code></p>
</li>
<li><p><code>TG_ARN</code></p>
</li>
<li><p><code>VITE_API_URL</code></p>
</li>
</ul>
<p>Follow the instructions on the <a target="_blank" href="https://circleci.com/docs/guides/security/set-environment-variable/#set-an-environment-variable-in-a-project">Set an environment variable</a> page to add them all to your project:</p>
<pre><code class="lang-plaintext">ACCOUNT_ID= # auto-generated by set-circleci-env.sh
AWS_ACCESS_KEY_ID= # CircleCI credentials you generated earlier
AWS_REGION= # auto-generated by set-circleci-env.sh
AWS_SECRET_ACCESS_KEY= # CircleCI credentials you generated earlier
CLIENT_APP_PORT=8080
DOCKERHUB_USERNAME= # your docker hub username
DOCKERHUB_PASSWORD= # your docker hub password
GCLOUD_SERVICE_KEY= # copy the contents of deployments/cloud-run/keys/circleci-deployer-$GCP_PROJECT_ID.json
GCP_REGION= # auto-generated by set-circleci-env.sh
GOOGLE_PROJECT_ID=carastore-client-prod
MYSQL_DATABASE=carastore_catalog
MYSQL_HOST=mysql-db
MYSQL_PASSWORD= # same value you used in ecs/scripts/export-env.sh
MYSQL_ROOT_PASSWORD= # same value you used in ecs/scripts/export-env.sh
MYSQL_USER=carastore_admin
MY_APP_PREFIX=carastore
PORT=5000
RUNTIME_SA_EMAIL= # auto-generated by set-circleci-env.sh
SERVICE_NAME=carastore-client
SM_SECRET_NAME=/carastore/server/env
TG_ARN= # auto-generated by set-circleci-env.sh
VITE_API_URL= # auto-generated by set-circleci-env.sh
</code></pre>
<p>With the set, commit and push your config files to GitHub:</p>
<pre><code class="lang-bash">git add .

git commit -m <span class="hljs-string">"&lt;add-a-commit-message-here&gt;"</span>

git push -u origin main
</code></pre>
<p>CircleCI will detect the branch automatically and run the defined workflows. Go to the <a target="_blank" href="https://app.circleci.com/">CircleCI web app</a>, select <strong>Pipelines</strong> in the sidebar, and you’ll see your workflows progress from build → test → deploy:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758081235852/89cef966-d26f-4822-8b18-1652daab6346.png" alt class="image--center mx-auto" /></p>
<p>And with that, you have a working deployment pipeline live on CircleCI.</p>
<h2 id="heading-testing-your-app-in-production">Testing your app in production</h2>
<p>Test the server app by copying the value of <code>VITE_API_URL</code> from your <code>env.circleci</code> file and pasting it into a browser. You may see a warning screen like this telling you the connection is not secure:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758083492329/36cc81a7-1a00-4bfb-9283-d8d022588de6.png" alt class="image--center mx-auto" /></p>
<p>This happens because the app uses a self-signed TLS certificate to ensure the secure client can communicate with the server. While this is fine for a practice project, in the real world, you would provision a valid ACM certificate and issue it for a domain you own.</p>
<p>Click the link to continue, and you should be directed to a screen with the message:</p>
<p>“<strong>Hello from the server!</strong>”</p>
<p>To test the React app:</p>
<ol>
<li><p>Navigate to the Cloud Run section in the Google Cloud console.</p>
</li>
<li><p>Select the service named <strong>carastore-client</strong>, and you should see the URL displayed.</p>
</li>
<li><p>Click on it to see the app in production.</p>
</li>
</ol>
<p>You will see a message saying no products are available.</p>
<p>Click <strong>Add Product</strong> in the top right corner, enter a test value in each text box, and submit. The app should redirect you to the home page, where you’ll see the product details. You can edit and or delete it for further testing.</p>
<p>This confirms your client app is successfully communicating with the server.</p>
<h2 id="heading-clean-up-and-best-practices">Clean up and best practices</h2>
<p>Run the cleanup scripts to avoid incurring unnecessary charges.</p>
<p>For AWS:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># add execution permissions</span>
chmod +x deployments/ecs/scripts/cleanup-ecs.sh

<span class="hljs-comment"># run the script</span>
<span class="hljs-built_in">source</span> deployments/ecs/scripts/cleanup-ecs.sh
</code></pre>
<p>For Google Cloud Run:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># add execution permissions</span>
chmod +x deployments/cloud-run/scripts/cleanup-cloud-run.sh

<span class="hljs-comment"># run the script</span>
<span class="hljs-built_in">source</span> deployments/cloud-run/scripts/cleanup-cloud-run.sh
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Maintaining a multi-cloud deployment strategy in the real world adds complexity, especially in terms of environmental drift. Over time, production and staging environments may diverge due to resource limits or networking behavior. Here are a couple of helpful tips to mitigate this:</p>
<ul>
<li><p>IaC (Infrastructure as Code) implementation (e.g., Terraform, Pulumi) to maintain parity.</p>
</li>
<li><p>Careful monitoring and observability to catch inconsistencies.</p>
</li>
</ul>
<p>As next steps, you should consider:</p>
<ul>
<li><p>Using CircleCI contexts for cloud credentials. E.g., staging vs production.</p>
</li>
<li><p>Maintaining cloud-agnostic logic and avoiding hard-coding cloud-specific behavior into your app.</p>
</li>
<li><p>Exporting logs to a centralized dashboard like <a target="_blank" href="https://www.datadoghq.com/">Datadog</a> or <a target="_blank" href="https://grafana.com/">Grafana</a>.</p>
</li>
</ul>
<p>This CircleCI blog post, <a target="_blank" href="https://circleci.com/blog/ci-cd-for-multi-cloud-setup/">CI/CD for multi-cloud: Automate and unify deployments across providers</a>, highlights key strategies for dealing with the increasing complexities of a robust multi-cloud architecture. I highly recommend it for a deeper dive into managing deployments across different providers.</p>
<p>I really enjoyed putting this together, and I hope it was just as fun for you to follow along. You can check out the full CircleCI configuration on the project’s <code>multi_cloud</code> branch.</p>
]]></content:encoded></item><item><title><![CDATA[Managing CI/CD pipelines in a JavaScript monorepo with CircleCI's dynamic configuration]]></title><description><![CDATA[Wherever you stand on the Monorepo vs polyrepo debate, managing continuous integration presents its own set of challenges, especially as your application scales up. Usually, when working with CircleCI in a polyrepo setup (where each project lives in ...]]></description><link>https://blog.dannysantino.com/circleci-dynamic-config-javascript-monorepo</link><guid isPermaLink="true">https://blog.dannysantino.com/circleci-dynamic-config-javascript-monorepo</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[CircleCI]]></category><category><![CDATA[ci-cd]]></category><dc:creator><![CDATA[Danny Santino]]></dc:creator><pubDate>Thu, 24 Jul 2025 20:33:45 GMT</pubDate><content:encoded><![CDATA[<p>Wherever you stand on the <a target="_blank" href="https://medium.com/@cfryerdev/monorepo-vs-polyrepo-the-great-debate-7b71068e005c">Monorepo vs polyrepo debate</a>, managing continuous integration presents its own set of challenges, especially as your application scales up. Usually, when working with CircleCI in a polyrepo setup (where each project lives in a separate Git repository), you create individual pipelines for each project using a <code>config.yml</code> file. This approach runs the same jobs or workflows every time an event triggers the pipeline; a process known as static configuration. However, this strategy can quickly become inefficient when working in a monorepo containing multiple projects or services. Jobs will often run unnecessarily on unchanged services, leading to slower pipeline execution and wasteful builds.</p>
<p>With CircleCI’s <a target="_blank" href="https://circleci.com/docs/dynamic-config/">dynamic configuration</a>, you can ensure that only the jobs and workflows related to a modified service will run when you push your commits. Dynamic configuration is a mechanism that allows programmatic execution of your CI/CD pipeline based on pre-defined parameters and conditions, providing the benefit of efficiency, speed, and scalability.</p>
<p>In this tutorial, you will learn how to set up a CircleCI pipeline to automatically detect changes in your repo and trigger targeted workflows accordingly. You will define jobs and configure conditional workflows for a monorepo containing a React frontend and a Dockerized Node.js + MySQL backend.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you begin, make sure you have the following:</p>
<ul>
<li><p>A <a target="_blank" href="https://circleci.com/signup/">CircleCI</a> account.</p>
</li>
<li><p>A <a target="_blank" href="https://app.docker.com/signup">Docker</a> account.</p>
</li>
<li><p>A <a target="_blank" href="https://github.com/signup">GitHub</a> account.</p>
</li>
<li><p><a target="_blank" href="https://git-scm.com/">Git</a> installed on your computer.</p>
</li>
<li><p>An IDE or code editor such as <a target="_blank" href="https://code.visualstudio.com/">VSCode</a>.</p>
</li>
<li><p>Basic familiarity with CI/CD concepts and Docker.</p>
</li>
</ul>
<h2 id="heading-setting-up-dynamic-configuration">Setting up dynamic configuration</h2>
<p>With all of the prerequisites in place, it’s time to configure the monorepo. For this tutorial, you’ll be working with this <a target="_blank" href="https://github.com/dannysantino/cara-store-catalog.git">simple JavaScript project</a>.</p>
<p>Start by <a target="_blank" href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo">forking the repository</a> on GitHub and cloning it to your local machine:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> &lt;your-fork-url&gt;

<span class="hljs-comment"># navigate into the project directory</span>
<span class="hljs-built_in">cd</span> cara-store-catalog
</code></pre>
<p>From the project’s root directory, create a <code>.circleci</code> directory, along with the following files:</p>
<pre><code class="lang-bash">mkdir .circleci

<span class="hljs-built_in">cd</span> .circleci

touch config.yml continue_config.yml
</code></pre>
<p>The <code>config.yml</code> file in the <code>.circleci</code> folder at the root of your project is where your pipeline’s main configuration lives. When you connect a repo to CircleCI, it automatically detects this file and begins orchestrating your workflows. When implementing dynamic configuration, this file also handles the initial setup phase.</p>
<p>The <code>continue_config.yml</code> contains the <em>continuation configuration</em>. It outlines the jobs to run and conditionally triggers workflows based on the pipeline parameters defined in <code>config.yml</code>.</p>
<p>Let us begin.</p>
<h3 id="heading-the-setup-phase">The setup phase</h3>
<p>Open the <code>config.yml</code> file in your code editor and enter the code below:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-number">2.1</span>

<span class="hljs-attr">setup:</span> <span class="hljs-literal">true</span>

<span class="hljs-attr">orbs:</span>
  <span class="hljs-attr">path-filtering:</span> <span class="hljs-string">circleci/path-filtering@2.0.1</span>

<span class="hljs-attr">workflows:</span>
  <span class="hljs-attr">filter-path:</span>
    <span class="hljs-attr">jobs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">path-filtering/filter:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">detect-modified-directories</span>
          <span class="hljs-comment"># &lt;directory&gt; &lt;pipeline parameter&gt; &lt;value&gt;</span>
          <span class="hljs-attr">mapping:</span> <span class="hljs-string">|
            server/.* run-server-jobs true
            client/.* run-client-jobs true
            .circleci/.* run-server-jobs true
            .circleci/.* run-client-jobs true
</span>          <span class="hljs-attr">base-revision:</span> <span class="hljs-string">main</span>
</code></pre>
<p>The code above defines the initial pipeline configuration. The <code>setup: true</code> field tells CircleCI to enable dynamic configuration features for this file. The <code>orbs</code> key imports the <a target="_blank" href="https://circleci.com/developer/orbs/orb/circleci/path-filtering">path filtering orb</a> to simplify the process of detecting modified paths and files. Behind the scenes, this orb also uses the <a target="_blank" href="https://circleci.com/developer/orbs/orb/circleci/continuation">continuation orb</a> to trigger the next phase of the pipeline using updated parameter values.</p>
<p>The <code>filter</code> job, available by default on the <code>path-filtering</code> orb, maps each directory where you wish to detect changes to a pipeline parameter and sets an initial value for the parameter. What this means is, when the <code>filter</code> job detects file changes in, say, <code>server/</code>, it sets the <code>run-server-jobs</code> parameter to <code>true</code> and passes it on to the continuation config. You can map additional paths as you wish. Finally, <code>base-revision</code> indicates which branch to compare for changes.</p>
<p>That covers all the logic required for the <code>config.yml</code> file.</p>
<h3 id="heading-continuation-configuration">Continuation configuration</h3>
<p>Next up is the <code>continue_config.yml</code> file. As mentioned earlier, this is where you will define the jobs and workflows you want to run.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">To avoid pipeline errors, ensure to properly indent each block of code in your YAML.</div>
</div>

<p>Start by declaring the CircleCI version along with default values for the pipeline parameters from the setup phase:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-number">2.1</span>

<span class="hljs-attr">parameters:</span>
  <span class="hljs-attr">run-server-jobs:</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">boolean</span>
    <span class="hljs-attr">default:</span> <span class="hljs-literal">false</span>
  <span class="hljs-attr">run-client-jobs:</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">boolean</span>
    <span class="hljs-attr">default:</span> <span class="hljs-literal">false</span>
</code></pre>
<p>To avoid needless code repetition across your config file, CircleCI provides the convenience of reusable configuration. Think of this as a feature similar to “functions” from traditional programming languages that you can call at various points in your code. You can learn more about this in the <a target="_blank" href="https://circleci.com/docs/reusing-config/">reusable configuration reference</a>.</p>
<p>Go ahead and define a reusable <code>executor</code> and a <code>command</code> in your <code>continuation_config.yml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">executors:</span>
  <span class="hljs-attr">node-exec:</span>
    <span class="hljs-attr">docker:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">image:</span> <span class="hljs-string">cimg/node:22.17</span>

<span class="hljs-attr">commands:</span>
  <span class="hljs-attr">installdeps:</span>
    <span class="hljs-attr">description:</span> <span class="hljs-string">"Install dependencies"</span>
    <span class="hljs-attr">parameters:</span>
      <span class="hljs-attr">directory:</span>
        <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">checkout:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">~/project</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">restore_cache:</span>
          <span class="hljs-attr">keys:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">v1-&lt;&lt;</span> <span class="hljs-string">parameters.directory</span> <span class="hljs-string">&gt;&gt;-deps-{{</span> <span class="hljs-string">checksum</span> <span class="hljs-string">"package.json"</span> <span class="hljs-string">}}-{{</span> <span class="hljs-string">checksum</span> <span class="hljs-string">"package-lock.json"</span> <span class="hljs-string">}}</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">v1-&lt;&lt;</span> <span class="hljs-string">parameters.directory</span> <span class="hljs-string">&gt;&gt;-deps-{{</span> <span class="hljs-string">checksum</span> <span class="hljs-string">"package.json"</span> <span class="hljs-string">}}</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">v1-&lt;&lt;</span> <span class="hljs-string">parameters.directory</span> <span class="hljs-string">&gt;&gt;-deps-</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">dependencies</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">npm</span> <span class="hljs-string">ci</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">save_cache:</span>
          <span class="hljs-attr">key:</span> <span class="hljs-string">v1-&lt;&lt;</span> <span class="hljs-string">parameters.directory</span> <span class="hljs-string">&gt;&gt;-deps-{{</span> <span class="hljs-string">checksum</span> <span class="hljs-string">"package.json"</span> <span class="hljs-string">}}-{{</span> <span class="hljs-string">checksum</span> <span class="hljs-string">"package-lock.json"</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">paths:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">node_modules</span>
</code></pre>
<p>The <code>node-exec</code> executor sets a <code>docker</code> execution environment using the CircleCI Node.js convenience image. The <code>installdeps</code> command lays out a typical process for installing Node.js dependencies in CircleCI. The command takes a parameter called <code>directory</code> of type <code>string</code>, which each job will pass at the point of invocation. Instead of hardcoded keys like <code>v1-server-deps</code> to save and restore dependencies, the caching steps use this dynamic string with <code>« parameters.directory »</code>, allowing multiple jobs to reuse the command.</p>
<p>Following this is an outline of a series of <code>steps</code>:</p>
<ul>
<li><p><code>checkout</code> clones the repository. This step specifies a path because, as you will see later, each set of jobs that uses this step will do so from a different working directory. Doing this ensures CircleCI checks out the code to the right directory.</p>
</li>
<li><p><code>restore_cache</code> tells CircleCI to reuse a previously saved cache of installed dependencies, if any, to minimize redundancy. The <code>keys</code> attribute uses the <code>directory</code> parameter to read the saved cache. If it fails to find an exact match, it gracefully falls back to less specific keys.</p>
</li>
<li><p>The <code>npm ci</code> command does a clean installation of the dependencies in your <code>package.json</code> file. It is typically used in CI environments and requires an existing <code>package-lock.json</code> file. See the <a target="_blank" href="https://docs.npmjs.com/cli/v8/commands/npm-ci">npm-ci</a> docs for more information about using this command.</p>
</li>
<li><p>With <code>save_cache</code>, you are instructing CircleCI to store a cache of the dependencies based on the checksums of <code>package.json</code> and <code>package-lock.json</code>. You normally would not want CircleCI to install the same dependencies from scratch every time you push your code, and those files have not changed. That would slow down your execution while also using up precious compute resources. See the <a target="_blank" href="https://circleci.com/docs/caching/">Caching dependencies</a> page for more information about how it works.</p>
</li>
</ul>
<p>Now it’s time to define the jobs your pipeline will run:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">build-client:</span>
    <span class="hljs-attr">executor:</span> <span class="hljs-string">node-exec</span>
    <span class="hljs-attr">working_directory:</span> <span class="hljs-string">~/project/client</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">installdeps:</span>
          <span class="hljs-attr">directory:</span> <span class="hljs-string">client</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">client</span> <span class="hljs-string">app</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">npm</span> <span class="hljs-string">run</span> <span class="hljs-string">build</span>

  <span class="hljs-attr">test-client:</span>
    <span class="hljs-attr">executor:</span> <span class="hljs-string">node-exec</span>
    <span class="hljs-attr">working_directory:</span> <span class="hljs-string">~/project/client</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">installdeps:</span>
          <span class="hljs-attr">directory:</span> <span class="hljs-string">client</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">client</span> <span class="hljs-string">tests</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">npm</span> <span class="hljs-string">test</span>
</code></pre>
<p>The <code>build-client</code> and <code>test-client</code> jobs share a similar structure:</p>
<ul>
<li><p>Apply <code>node-exec</code> as the executor environment.</p>
</li>
<li><p>Set the working directory to the <code>client</code> folder.</p>
</li>
<li><p>Invoke the <code>installdeps</code> command and pass the <code>client</code> value for <code>save_cache</code> and <code>restore_cache</code> to use.</p>
</li>
<li><p>Run the corresponding commands to build and test the code.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Make sure to correctly indent the following jobs under the top-level <code>jobs</code> key.</div>
</div>

<p>Add the <code>test-server</code> job to your continuation config:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">test-server:</span>
    <span class="hljs-attr">docker:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">image:</span> <span class="hljs-string">cimg/node:22.17</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">image:</span> <span class="hljs-string">cimg/mysql:8.0</span>
        <span class="hljs-attr">environment:</span>
          <span class="hljs-attr">MYSQL_ROOT_PASSWORD:</span> <span class="hljs-string">djs0_32</span>
    <span class="hljs-attr">working_directory:</span> <span class="hljs-string">~/project/server</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">installdeps:</span>
          <span class="hljs-attr">directory:</span> <span class="hljs-string">server</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Wait</span> <span class="hljs-string">for</span> <span class="hljs-string">MySQL</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">dockerize</span> <span class="hljs-string">-wait</span> <span class="hljs-string">tcp://localhost:3306</span> <span class="hljs-string">-timeout</span> <span class="hljs-string">1m</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">MySQL</span> <span class="hljs-string">client</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">|
            sudo apt-get update
            sudo apt-get install default-mysql-client
</span>      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">up</span> <span class="hljs-string">database</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">|
            mysql -h 127.0.0.1 -u root -pdjs0_32 -e "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED WITH mysql_native_password BY '$MYSQL_PASSWORD'"
            mysql -h 127.0.0.1 -u root -pdjs0_32 &lt; db/init.sql
</span>      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">server</span> <span class="hljs-string">tests</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">npm</span> <span class="hljs-string">test</span>
</code></pre>
<p>The <code>test-server</code> job sets up the following:</p>
<ul>
<li><p>A primary container: <code>cimg/node:22.17</code>.</p>
</li>
<li><p>A secondary/service container: <code>cimg/mysql:8.0</code>.</p>
</li>
<li><p>A root password for the MySQL database for initial access.</p>
</li>
<li><p>And, as mentioned earlier, the working directory to run the steps in.</p>
</li>
</ul>
<p>It then invokes the <code>installdeps</code> command and passes the <code>server</code> value. To avoid race conditions, the job uses <code>dockerize</code> to wait for the container to start before attempting to use it. Once ready, it installs the MySQL client, sets up the database by creating a user and running the <code>server/db/init.sql</code> script, and then runs the server test suite.</p>
<p>Next, add the <code>publish-server</code> job:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">publish-server:</span>
    <span class="hljs-attr">docker:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">image:</span> <span class="hljs-string">cimg/base:current</span>
    <span class="hljs-attr">working_directory:</span> <span class="hljs-string">~/project/server</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">checkout:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">~/project</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">setup_remote_docker:</span>
          <span class="hljs-attr">docker_layer_caching:</span> <span class="hljs-literal">true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">image</span> <span class="hljs-string">tag</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">|
            IMAGE_TAG=$(jq -r '.version' package.json)
            echo "export IMAGE_TAG=$IMAGE_TAG" &gt;&gt; $BASH_ENV
            echo "IMAGE_TAG: $IMAGE_TAG"
            source $BASH_ENV
</span>      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">production</span> <span class="hljs-string">image</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">|
            docker build -t $DOCKERHUB_USERNAME/dynamic-config:$IMAGE_TAG --target prod .
</span>      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Authenticate,</span> <span class="hljs-string">tag,</span> <span class="hljs-string">and</span> <span class="hljs-string">push</span> <span class="hljs-string">image</span> <span class="hljs-string">to</span> <span class="hljs-string">Docker</span> <span class="hljs-string">Hub</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">|
            echo "$DOCKERHUB_PASSWORD" | docker login -u $DOCKERHUB_USERNAME --password-stdin
            docker tag $DOCKERHUB_USERNAME/dynamic-config:$IMAGE_TAG $DOCKERHUB_USERNAME/dynamic-config:latest
            docker push $DOCKERHUB_USERNAME/dynamic-config:$IMAGE_TAG
            docker push $DOCKERHUB_USERNAME/dynamic-config:latest</span>
</code></pre>
<p>Here is a rundown of what this block does:</p>
<ul>
<li><p>Sets <code>setup_remote_docker: true</code> to enable <a target="_blank" href="https://circleci.com/docs/docker-layer-caching/">Docker layer caching</a> for reuse in future builds.</p>
</li>
<li><p>Extracts the version number from the server’s <code>package.json</code> file to tag the image.</p>
</li>
<li><p>Builds a production version of the image. The <code>FROM base AS prod</code> line in the <code>server/Dockerfile</code> outlines the build instructions.</p>
</li>
<li><p>Authenticates with Docker, sets the <code>latest</code> tag on the built image, and pushes both tags to Docker Hub.</p>
</li>
</ul>
<p>And for the final piece of the continuation config:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">workflows:</span>
  <span class="hljs-attr">test-and-publish-server:</span>
    <span class="hljs-attr">when:</span> <span class="hljs-string">&lt;&lt;</span> <span class="hljs-string">pipeline.parameters.run-server-jobs</span> <span class="hljs-string">&gt;&gt;</span>
    <span class="hljs-attr">jobs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">test-server</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">publish-server:</span>
          <span class="hljs-attr">requires:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">test-server</span>
  <span class="hljs-attr">build-and-test-client:</span>
    <span class="hljs-attr">when:</span> <span class="hljs-string">&lt;&lt;</span> <span class="hljs-string">not</span> <span class="hljs-string">pipeline.parameters.run-client-jobs</span> <span class="hljs-string">&gt;&gt;</span>
    <span class="hljs-attr">jobs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">build-client</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">test-client:</span>
          <span class="hljs-attr">requires:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">build-client</span>
</code></pre>
<p>Workflows orchestrate the jobs in your config. Here, the <code>when</code> clause directs CircleCI to run the specified jobs only when the pipeline parameter is true. And with <code>requires</code>, you are pausing the execution of a job until the previous one is successful.</p>
<p>That concludes the configuration setup.</p>
<h3 id="heading-setting-the-environment-variables">Setting the environment variables</h3>
<p>If you take a look at some of the project files, like <code>index.js</code> and <code>compose.yaml</code>, you will notice the use of a number of environment variables. These are necessary to avoid exposing sensitive credentials on GitHub.</p>
<p>To set your project variables, start by pushing the changes you have made so CircleCI can detect your <code>.circleci/config.yml</code> file. From your project’s root directory, enter the following commands:</p>
<pre><code class="lang-bash">git add .

git commit -m <span class="hljs-string">'&lt;add-your-commit-message&gt;'</span>

git push -u origin main
</code></pre>
<p>Next, follow the instructions on the <a target="_blank" href="https://circleci.com/docs/create-project/#set-up-a-project">Set up a project</a> page to connect your repo. After linking your project, CircleCI will kick off a pipeline and begin running all the workflows. The initial run will likely fail, but you can fix that right away.</p>
<p>Navigate to the <a target="_blank" href="https://circleci.com/docs/set-environment-variable/#set-an-environment-variable-in-a-project">Set an environment variable</a> page and follow the instructions to add the following to your project:</p>
<pre><code class="lang-plaintext">MYSQL_USER=carastore_admin
MYSQL_PASSWORD=&lt;assign-any-value&gt;
MYSQL_DATABASE=carastore_catalog
DOCKERHUB_USERNAME=&lt;your-dockerhub-username&gt;
DOCKERHUB_PASSWORD=&lt;your-dockerhub-password&gt;
</code></pre>
<p>Please note that you may also assign any values you want to the MySQL user and database; however, you would have to update them accordingly in the <code>server/db/init.sql</code> file.</p>
<h3 id="heading-testing-your-pipeline">Testing your pipeline</h3>
<p>With your configuration and environment variables in place, CircleCI can now automatically detect which folders you have modified and then run your workflows based on those modifications. To see this in full action, edit a file inside the <code>client/</code> or <code>server/</code> folder.</p>
<p>Commit and push your changes:</p>
<pre><code class="lang-bash">git add .

git commit -m <span class="hljs-string">'&lt;add-your-commit-message&gt;'</span>

git push -u origin main
</code></pre>
<p>Open the project on <a target="_blank" href="https://app.circleci.com/">your dashboard</a> and watch as CircleCI triggers a new pipeline that runs only the corresponding workflow, demonstrating the efficiency of dynamic configuration.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Running jobs for unchanged services in a monorepo can result in wasted resources, slower pipelines, and increased costs in the long run. By implementing dynamic configuration, you can mitigate these downsides and optimize your pipelines for speed and efficiency.</p>
<p>I hope this tutorial has helped you understand how to improve your CI/CD workflows. If you wish to see the complete setup of the config files, check out the <code>dynamic_config</code> branch of the <a target="_blank" href="https://github.com/dannysantino/cara-store-catalog.git">project repository</a>. For more examples on how to further customise your workflows, see the CircleCI guide on <a target="_blank" href="https://circleci.com/docs/using-dynamic-configuration/">Using dynamic configuration</a>.</p>
]]></content:encoded></item><item><title><![CDATA[How to Dockerize an Express App]]></title><description><![CDATA[Introduction
Differences across development environments tend to cause compatibility issues for developers during deployment. Enter Docker, a platform that helps solve this problem by providing a consistent environment for developing and deploying ap...]]></description><link>https://blog.dannysantino.com/dockerize-express-app</link><guid isPermaLink="true">https://blog.dannysantino.com/dockerize-express-app</guid><category><![CDATA[Docker]]></category><category><![CDATA[Express]]></category><category><![CDATA[containers]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Dockerfile]]></category><dc:creator><![CDATA[Danny Santino]]></dc:creator><pubDate>Thu, 06 Mar 2025 14:45:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1741161692450/0376f05f-707b-434c-a2ae-afde70b97a6a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>Differences across development environments tend to cause compatibility issues for developers during deployment. Enter <a target="_blank" href="https://www.docker.com/">Docker</a>, a platform that helps solve this problem by providing a consistent environment for developing and deploying applications, otherwise known as a container. A container is a running instance of a software package containing all the code, libraries, and dependencies required to run an application anywhere, whether it’s on a local machine, in the cloud, or on a production server.</p>
<p>The software package we use to run a container is called a Docker image. Think of this as a read-only snapshot of your code alongside everything else it needs to run, all packaged into one neat, (not so) little executable piece of software. You can then run this anywhere, at any time, and thanks to the isolation technology of containers, you can be certain it will work the same way, 100% of the time. A core Docker component called the Docker Engine handles the creation, management, and execution of Docker containers.</p>
<p>This tutorial provides a guide to containerizing an <a target="_blank" href="https://expressjs.com/">Express.js</a> application in a development environment. In it, you will learn how to:</p>
<ul>
<li><p>Set up a basic Express app.</p>
</li>
<li><p>Write a Dockerfile that contains the instructions for creating a Docker image.</p>
</li>
<li><p>Build an image of your application.</p>
</li>
<li><p>Run a container using that image.</p>
</li>
</ul>
<p>And, as a bonus, you will also learn how to upload your image to Docker Hub where you can share it with other developers, just like you would with your code on GitHub.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To complete this tutorial, you will need the following:</p>
<ul>
<li><p>A local development environment for <a target="_blank" href="https://nodejs.org">Node.js</a>. You can <a target="_blank" href="https://nodejs.org/en/download/current">download</a> the latest stable version from the Node.js official website or install it using a version manager like <a target="_blank" href="https://github.com/nvm-sh/nvm">nvm</a>.</p>
</li>
<li><p>Basic familiarity with the Express framework. If you are new to Express, you can check out the <a target="_blank" href="https://expressjs.com/en/starter/installing.html">official documentation</a> to get started.</p>
</li>
<li><p>Docker installed on your machine. You can find specific OS instructions in the <a target="_blank" href="https://docs.docker.com/desktop/">docs</a> (no pun intended).</p>
</li>
<li><p>A <a target="_blank" href="https://hub.docker.com/">Docker Hub</a> account to upload and manage your Docker images.</p>
</li>
<li><p>Knowledge of basic command line operations. This <a target="_blank" href="https://www.earthdatascience.org/courses/intro-to-earth-data-science/open-reproducible-science/bash/bash-commands-to-manage-directories-files/#:~:text=Similarly%2C%20you%20can%20copy%20an,directory%2Dname%2D2%20\).">guide</a> provides a good starting point.</p>
</li>
<li><p>A code editor of your choice. <a target="_blank" href="https://code.visualstudio.com/">Visual Studio Code</a> is a very popular option.</p>
</li>
</ul>
<h2 id="heading-setting-up-the-application">Setting up the Application</h2>
<p>The first step is to build a basic Express app to serve as the foundation for your project. Begin by creating a new directory to house all of the files and configurations, and then navigate into it:</p>
<pre><code class="lang-bash">mkdir express-docker-app
<span class="hljs-built_in">cd</span> express-docker-app
</code></pre>
<p>Next, initialize a new project using <code>npm</code> (Node Package Manager for installing and managing software packages). This will generate a <code>package.json</code> file to keep track of your project’s metadata and dependencies. The <code>-y</code> flag in the command automatically answers “yes” to all setup questions and creates the <code>package.json</code> file with default settings. If, however, you would rather customize these settings, run the command without the flag:</p>
<pre><code class="lang-bash">npm init -y
</code></pre>
<p>Now you can install the Express framework with the command below. This will add it to the previously mentioned <code>package.json</code> file and also create a <code>node_modules</code> folder which stores all the libraries and frameworks your application requires:</p>
<pre><code class="lang-bash">npm install express
</code></pre>
<p>Create a new file named <code>index.js</code> to contain your application code:</p>
<pre><code class="lang-bash">touch index.js
</code></pre>
<p>Now open this file in your code editor and input the following code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express"</span>);
<span class="hljs-keyword">const</span> app = express();

<span class="hljs-keyword">const</span> PORT = <span class="hljs-number">3000</span>;

app.get(<span class="hljs-string">"/"</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
    res.send(<span class="hljs-string">"Hello, Express App!"</span>);
});

app.listen(PORT, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server running on port <span class="hljs-subst">${PORT}</span>`</span>);
});
</code></pre>
<p>The code above creates a very basic Express app. We start by importing the Express framework we installed earlier with <code>require(“express”)</code> and then we initialize an instance with <code>const app = express()</code>. We use this instance to define a route <code>app.get(“/”, …)</code> that responds with a simple message when we access the server. And with <code>app.listen(PORT, …)</code>, we are starting the server on the predefined port <code>const PORT = 3000</code> and printing a message to the command line to let us know the server is running successfully.</p>
<p>Test your application by running the command below:</p>
<pre><code class="lang-bash">node index.js
</code></pre>
<p>If your app has been correctly set up, you should see the terminal output: <code>Server running on port 3000</code>.</p>
<p>Navigate to <em>http://localhost:3000</em> on a web browser and the message “Hello, Express App!” will be displayed on the page:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740747239312/335e8782-b081-4dc2-b66b-623eb5464826.png" alt="Partial screenshot of a browser window showing &quot;localhost:3000&quot; in the address bar and &quot;Hello, Express App!&quot; on the page." class="image--center mx-auto" /></p>
<p>Close the server for now with <strong>Ctrl + C</strong> and move on to the next step.</p>
<h2 id="heading-writing-the-dockerfile">Writing the Dockerfile</h2>
<p>The next step is to create and set up a <code>Dockerfile</code>. This is a document outlining a set of instructions that tell Docker how to build a container image for your application. These instructions consist of a series of commands, with each one creating a new layer in the image. The layers are stacked on top of one another, and Docker caches them to allow for faster builds.</p>
<p>Create a Dockerfile in your project directory:</p>
<pre><code class="lang-bash">touch Dockerfile
</code></pre>
<p>Open this file in your code editor and input the following lines of code:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> node:<span class="hljs-number">22</span>-alpine
</code></pre>
<h3 id="heading-from">FROM</h3>
<p>This is the starting point for the image, and it tells Docker which base image to use in building the container. A base image is necessary because it establishes the environment by providing an operating system which is vital for running applications in a container. This is where the consistent environment mentioned earlier comes into play. By specifying a base layer, we ensure our app runs on the same OS and Node.js runtime, no matter where it is deployed.</p>
<p>In this instance, we are using the <a target="_blank" href="https://hub.docker.com/layers/library/node/22-alpine/images/sha256-3a4802e64ab5181c7870d6ddd8c824c2efc42873baae37d1971451668659483b">Alpine Linux</a> version of Node.js 22. It is extremely small and lightweight (about 5MB) which means faster download, build, and start times.</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">WORKDIR</span><span class="bash"> /usr/src/app</span>
</code></pre>
<h3 id="heading-workdir">WORKDIR</h3>
<p>We use this command to set the <em>working directory</em> for subsequent instructions in the file. The path specified is where the following instructions (<code>COPY</code>, <code>RUN</code>, <code>CMD</code>, etc) will be executed from, and this can either be absolute or relative. If the directory you specify does not exist, Docker will create it for you. Here, we’re setting the path to <code>/usr/src/app</code> based on the <a target="_blank" href="https://tldp.org/LDP/Linux-Filesystem-Hierarchy/html/usr.html">Linux Filesystem Hierarchy</a> according to the official Docker recommendation.</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">COPY</span><span class="bash"> package*.json ./</span>
</code></pre>
<h3 id="heading-copy">COPY</h3>
<p>This copies files and directories from your local machine into the Docker image’s filesystem. It allows you to bring in code, configuration files, and any other resources your application needs to run.</p>
<p>Syntax: <code>COPY &lt;src&gt; &lt;dest&gt;</code>.</p>
<p><code>&lt;src&gt;</code> specifies the source files or directories on your local machine, and <code>&lt;dest&gt;</code> is the destination path inside the container.</p>
<p>This is the first of two <code>COPY</code> instructions we will be using in our <code>Dockerfile</code>. Here, we are asking Docker to copy the <code>package.json</code> and <code>package-lock.json</code> files while using an asterisk (*) as a wildcard to specify both files at once.</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">RUN</span><span class="bash"> npm install</span>
</code></pre>
<h3 id="heading-run">RUN</h3>
<p>The <code>RUN</code> instruction executes commands during the image build process. It allows us to install software, run scripts, or perform other tasks inside the image while it is being built. Since we have a <code>package.json</code> file containing a list of our dependencies, we can install them by asking Docker to <em>run</em> the <code>npm install</code> command.</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">COPY</span><span class="bash"> . .</span>
</code></pre>
<h3 id="heading-copy-1">COPY</h3>
<p>The second <code>COPY</code> instruction copies the rest of the project’s files and folders (except the <code>.dockerignore</code> file) all in one go.</p>
<p>“So why do we need two separate <code>COPY</code> instructions?” you might ask. Well, we’re doing this for optimization purposes, so Docker won’t have to reinstall the dependencies every time we make a change to our codebase and rebuild the image. You can learn more about this practice <a target="_blank" href="https://maximorlov.com/a-beginners-guide-to-building-a-docker-image-of-your-nodejs-application/#:~:text=The%20reason%20we%20first%20copy,a%20Dockerfile%20represents%20a%20layer.">here</a>.</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">3000</span>
</code></pre>
<h3 id="heading-expose">EXPOSE</h3>
<p>This indicates which network the container will listen on when it is running. It does not automatically make the container externally accessible; we do that with the <code>—port</code> or <code>-p</code> flag when we run the container. What <code>EXPOSE</code> does is serve as a declaration of the specific port (3000 in this case) that we would like to publish later on.</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"node"</span>, <span class="hljs-string">"index.js"</span>]</span>
</code></pre>
<h3 id="heading-cmd">CMD</h3>
<p>We use <code>CMD</code> to specify the default command to run when we start a container from the image. A Dockerfile can only have one <code>CMD</code> instruction. If you define more than one, only the last one will take effect. Since we are starting our Express app with <code>node index.js</code>, this is the command that we pass to <code>CMD</code>.</p>
<p>After entering all of these commands, this is what your <code>Dockerfile</code> should look like:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> node:<span class="hljs-number">22</span>-alpine

<span class="hljs-keyword">WORKDIR</span><span class="bash"> /usr/src/app</span>

<span class="hljs-keyword">COPY</span><span class="bash"> package*.json ./</span>

<span class="hljs-keyword">RUN</span><span class="bash"> npm install</span>

<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>

<span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">3000</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"node"</span>, <span class="hljs-string">"index.js"</span>]</span>
</code></pre>
<p><strong>Note:</strong> When Docker executes the <code>COPY . .</code> instruction, it copies everything from the <code>express-docker-app</code> directory unless you use a <code>.dockerignore</code> file to exclude specific files or directories. Since running <code>npm install</code> automatically adds a <code>node_modules</code> folder to the image, we don’t want to overwrite it with that of our host machine.</p>
<p>Go ahead and create a <code>.dockerignore</code> file in the root directory:</p>
<pre><code class="lang-bash">touch .dockerignore
</code></pre>
<p>And add the <code>node_modules</code> folder to it:</p>
<pre><code class="lang-plaintext">node_modules/
</code></pre>
<p>Also note that the instructions used in our Dockerfile is by no means exhaustive. For the complete list of available instructions, as well as more information on those used in this tutorial, please see the <a target="_blank" href="https://docs.docker.com/reference/dockerfile/">official Dockerfile reference</a>.</p>
<h2 id="heading-building-and-running-the-docker-image">Building and running the Docker Image</h2>
<p>With your Dockerfile all set, you are ready to build your image and run your app inside a container. Run the command below from your project directory:</p>
<pre><code class="lang-bash">docker build -t express-docker-app .
</code></pre>
<p>The <code>docker build</code> part of the command creates a Docker image based on the instructions in the Dockerfile, while the <code>-t</code> flag tags the image with the name <code>express-docker-app</code> for easy identification. The dot <code>.</code> specifies the current directory we are in as the build context, which means Docker will work with the Dockerfile along with all the other files in this directory during the build process.</p>
<p>After the build is complete, you should see a message like this at the top of the build output in your terminal:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740747809509/9d1b0d75-ac9d-42bb-a2b8-10f6d4895432.png" alt="Screenshot of a terminal showing the output of a &quot;docker build&quot; command." class="image--center mx-auto" /></p>
<p>This confirms your image was built successfully and tagged as <code>express-docker-app:latest</code>.</p>
<p>Now we can run a container from the image:</p>
<pre><code class="lang-bash">docker run -p 3000:3000 express-docker-app
</code></pre>
<p>With <code>docker run</code>, we are starting a new container based on the specified image, <code>express-docker-app</code>. And, as mentioned earlier when talking about the <code>EXPOSE</code> instruction in our <code>Dockerfile</code>, the <code>-p</code> flag maps port 3000 on your local machine to port 3000 in the container and publishes it.</p>
<p>When the container starts running, you should the same message logged to your terminal again: <code>Server running on port 3000</code>.</p>
<p>As you did before, open a web browser, head to <em>http://localhost:3000</em> and again, you should see “Hello, Express App” displayed, confirming that your application is running inside a container.</p>
<p>And with that, you have successfully built and Dockerized an Express app!</p>
<h3 id="heading-helpful-commands">Helpful Commands</h3>
<p>To help you better manage your Docker containers, below are some helpful commands.</p>
<p>Display a list of images available on your local machine:</p>
<pre><code class="lang-bash">docker images
</code></pre>
<p>Display a list of active containers and their IDs:</p>
<pre><code class="lang-bash">docker ps
</code></pre>
<p>To start or stop an existing container, enter the corresponding command and replace <code>container_id</code> with the ID of the container from the previous output:</p>
<pre><code class="lang-bash">docker start|stop &lt;container_id&gt;
</code></pre>
<p>Remove a stopped container:</p>
<pre><code class="lang-bash">docker rm &lt;container_name&gt;
</code></pre>
<h2 id="heading-uploading-the-image-to-docker-hub">Uploading the Image to Docker Hub</h2>
<p>Docker provides a platform called Docker Hub for uploading Docker images and you can do this in three easy steps:</p>
<p>Type the command below and enter your credentials when prompted:</p>
<pre><code class="lang-bash">docker login
</code></pre>
<p>Tag your image with your Docker Hub username and a name for the repository:</p>
<pre><code class="lang-bash">docker tag express-docker-app &lt;your_dockerhub_username&gt;/express-docker-app:latest
</code></pre>
<p>Upload the tagged image to Docker Hub:</p>
<pre><code class="lang-bash">docker push &lt;your_dockerhub_username&gt;/express-docker-app:latest
</code></pre>
<p>Docker will push your image to the registry and make it available for public use anywhere. You can visit your Docker Hub <a target="_blank" href="https://hub.docker.com/repositories/">repository</a> to confirm the successful upload of your image.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you learned how to build and containerize an Express app using Docker. You created a Dockerfile to define your build specs, built a Docker image using that Dockerfile, and then ran your application in a Docker container. Finally, you uploaded the image to the Docker Hub registry and made it available to other developers around the world.</p>
<p>If you wish to improve your knowledge, you should consider looking into further aspects of the containerization process such as testing, debugging, network, and security.</p>
<p>I highly recommend exploring more advanced topics such as working with a multi-container setup using Docker Compose. This Digital Ocean tutorial on <a target="_blank" href="https://www.digitalocean.com/community/tutorials/how-to-integrate-mongodb-with-your-node-application">integrating MongoDB with your Node Application</a> is a great place to start.</p>
]]></content:encoded></item></channel></rss>