From Code to Deployment: Building a Free CI/CD Workflow for Node.js Application

Improving software development lifecycle with CI/CD using Node.js, CircleCI and Render.

Pavlo Kolodka
9 min readJun 5, 2023
Image created by Author

CI/CD, which stands for Continuous Integration and Continuous Delivery/Deployment, is a set of practices and principles in software development aimed at automating the process of building, testing, and deploying software applications.

It involves a combination of tools, techniques, and workflows that enable developers to deliver high-quality software more efficiently and frequently.

In this article, we will build a simple CI/CD workflow that will allow us to automatically run tests, build an application, and deploy it to a remote server.

Prerequisites:

  • Having existing code in the GitHub repository (ideally with unit tests)

TL;DR

  1. Setting up Continuous Integration (CI)
  2. Setting up Continuous Deployment (CD)
  3. Conclusion

Setting up Continuous Integration (CI)

Example of CI we will create

Continuous Integration (CI) focuses on integrating code changes from multiple developers into a shared repository regularly. It involves automating the build process and running automated tests to identify any issues or conflicts that may arise due to code changes.

By integrating code frequently, CI helps catch and resolve conflicts early, reducing the chances of introducing bugs and improving overall code stability.

There are several CI services, some of the most popular are: GitHub Actions, GitLab CI/CD, Bitbucket Pipelines, Azure Pipelines, Travis CI, Jenkins, CircleCI.

I personally prefer CircleCI for its simplicity and customizability. The main features available in CirlceCI are:

  • Free tier: CircleCI offers a free tier that provides generous limits for build minutes and active users per month. The free tier allows you to leverage CircleCI’s CI/CD platform without incurring any costs, making it an attractive option for open-source projects or small teams.
  • Multiple execution environments: CircleCI provides several runtime environments, including cloud virtual machines (VMs) and private infrastructure support. These runtime environments provide flexibility and support for different programming languages and project requirements.
  • Parallelism and caching: CircleCI allows the parallelization of jobs and provides caching mechanisms to speed up builds by storing and reusing dependencies between builds.
  • Extensive integrations: CircleCI integrates with a wide range of tools, services, and deployment platforms, enabling you to seamlessly integrate with your existing development ecosystem

Creating a CirceCI pipeline

To begin utilizing CircleCI, the first step is to create an account on their website. Once registered, you can select the appropriate project from the available options to establish the CI setup.

Next, you will be prompted to create config.yml. In this file, you will describe all the actions you need to perform. Select the last option and click “Set Up Project”.

Use the fast option to create config.yml

You will be redirected to a new page with the online editor:

CirceCI online editor

Now, let’s write a simple manifest that runs unit tests and builds the application locally.

I use TS for development, and have written some scripts to start, test, and build an application in package.json.

You can change the configuration according to your own preferences and requirements.

version: 2.1
orbs:
node: circleci/node@5.1.0

jobs:
build_and_test:
executor:
name: node/default
tag: '18.16'
steps:
- checkout
- run: node --version
- node/install-packages:
pkg-manager: npm
- run:
command: npm run test:unit
name: Run unit tests
- run:
command: npm run build
name: Build server
- run:
command: npm run start
name: Start server
background: true
- run:
command: sleep 5 && curl -I localhost:5000
name: Verify server is running

workflows:
build_and_test_app:
jobs:
- build_and_test

Let’s analyze the written manifest:

1. orbs section:

  • The node orb from the CircleCI ecosystem is being included. The circleci/node@5.1.0 orb provides predefined commands and configurations for working with Node.js projects.

2. jobs section:

  • A job named build_and_test is defined. It represents a unit of work in the pipeline.
  • The job uses the node/default executor, which is a predefined executor configuration for Node.js projects. It specifies the Node.js version to use (tag: '18.16').
  • The steps of the job are defined using the - notation.
  • checkout step: Check out the source code from the version control system. This will locate your repository and retrieve the corresponding branch of git origin. This way, you guarantee that the next steps of the pipeline will work with the most up-to-date version of the code base.
  • run: node --version step: Print the version of Node.js being used.
  • node/install-packages step: Installs project dependencies using the specified package manager (npm).
  • run: npm run test:unit step: Run the unit tests of the project.
  • run: npm run build step: Build the server application.
  • run: npm run start step: Start the server application in the background.
  • run: sleep 5 && curl -I localhost:5000 step: Waits for 5 seconds and then sends an HTTP request to the server running on localhost:5000 to verify that the server is running.

3. workflows section:

  • A workflow named build_and_test_app is defined, which represents the sequence of jobs to execute.
  • The build_and_test job is included in the workflow.

Next, you can click the “Commit and Run” button, which will create a new circleci-project-setup branch in your GitHub repository. This will start this pipeline, and you can observe it in the “Dashboad” section.

Example of the steps passed in the workflow

Additional configuration

If you need some environment variables to build your application, you can add them manually in the project settings.

Setting up environment variable in CircleCI

In the same settings under “Advanced”, I recommend enabling “Only built pull requests” to avoid unnecessary running workflow on every push to a remote repository.

“Only built pull requests” option

GitHub status check

To protect your branch from skipping tests, you need to enable “Require status checks to pass before merging” and select your created workflow as a status check in the corresponding GitHub branch.

“Require status checks to pass before merging” flag

That’s it, we’ve set up a simple CI configuration to run unit tests and build the app at each pull request and new commit in the default branch (the master branch in my case).

Setting up Continuous Deployment (CD)

Example of CD we will create

As I mentioned before, CD stands for Continuous Deployment or Continuous Delivery.

  • Continuous Deployment refers to an automated process where changes to the codebase are automatically deployed to production or a production-like environment as soon as they pass the necessary tests and quality checks. With continuous deployment, every successful change is immediately released to end-users without manual intervention.
  • Continuous Delivery, on the other hand, is an approach where changes to the codebase are continuously prepared and made ready for deployment. While the process is automated, the decision to actually deploy the changes to production is typically done manually. Continuous Delivery ensures that software can be reliably and consistently delivered to production, but the deployment itself is triggered by a human decision.
Source: AWS continuous dilivery

Thus, the only difference is the final deployment decision, which in the case of Continuous Deployment is automatic, while in the case of Continuous Delivery you have to make the decision manually.

Choosing a cloud platform account

There are many cloud service providers that can be easily integrated into CD workflow, such as AWS (Amazon Web Services), Google Cloud, Microsoft Azure, Heroku.

Until recently, Heroku was the best option because it offered a free level for hosting projects. Now, overwhelmingly, there are no platforms that offer a permanent free level.

But I recently found a cloud service called Render (not a sponsor) that offers different solutions as well as a free level for your own personal projects.

Render has support for various environments for different programming languages as well as Docker support. I’ll choose the last one.

Using a Docker environment for the deployment process offers several advantages over using native support like a Node.js environment directly.

Docker provides a consistent and isolated runtime environment, ensuring that the application behaves consistently across different systems regardless of the underlying infrastructure or operating system.

Render cloud can automatically detect a Dockerfile.

You can use my sample to create your own Dockerfile if you have not already done so.

FROM node:18

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

RUN npm run build

EXPOSE 5000

CMD [ "node", "./dist/src/index.js" ]

Creating a CD pipeline using Render

All you have to do to get started is create a new account and then select “Create a new Web Service”. Then you can connect via GitHub or simply paste in a link to your repository.

Render will automatically start the deployment process, and you can see the result in the “Logs” tab.

To set up a continuous deployment, we will do the following:

  • After merging the pull request into the “master” branch, the CD workflow will be started.
  • We will run tests and build the application again to make sure the new changes are integrated correctly.
  • Only after that, we will run the Render Webhook to start the new deployment.

Updated config.yml

version: 2.1
orbs:
node: circleci/node@5.1.0

jobs:
build_and_test:
executor:
name: node/default
tag: '18.16'
steps:
- checkout
- run: node --version
- node/install-packages:
pkg-manager: npm
- run:
command: npm run test:unit
name: Run unit tests
- run:
command: npm run build
name: Build server
- run:
command: npm run start
name: Start server
background: true
- run:
command: sleep 5 && curl -I localhost:5000
name: Verify server is running
deploy:
machine:
image: ubuntu-2004:current
resource_class: medium
steps:
- run:
name: Deploy API to Render
command: |
response=$(curl -s -w "%{http_code}" -o response.txt $DEPLOY_URI)
response_code=${response:(-3)}
if [ $response_code -eq 200 ]; then
echo "Deployment successful!"
cat response.txt # Print the response body
else
echo "Deployment failed with response code: $response_code"
cat response.txt # Print the response body
exit 1
fi

workflows:
build_and_test_app:
jobs:
- build_and_test
- deploy:
requires:
- build_and_test
filters:
branches:
only: master

We have added a new job called deploy .

In this job, we spin up a new virtual machine to create an HTTP request for deployment. $DEPLOY_URI is an environment variable that you need to register in the current CircleCI project.

You can grab it from the Render cloud under “Setting” in the “Deploy hook” section.

  • If the request status code is 200, we will output “Deployment successful!” and the corresponding body.
  • Otherwise, we will output “Deployment failed with response code: $response_code” with the response body, where $response_code is the HTTP status code.

We have also added to the build_and_test_app workflow a deploy job call only if the current branch is master and if the previous job succeeded.

Conclusion

CI/CD has become a cornerstone of efficient and reliable software development. Its ability to automate key processes, ensure code quality, and enable frequent and reliable deployments makes it an indispensable approach for modern development teams.

In this article, we went through the entire process of setting up a basic CI/CD pipeline using CirlceCI and Render to test and deploy a Node.js web server completely free.

Definitely, this configuration is not complete, and there is a lot that could be added and improved.

For example, testing the application in a pre-production environment with end-to-end tests and a database. Also, logging, monitoring, automatic code security checks, easy rollback and many other useful things. I plan to write a second part in which I will try to implement all of these ideas.

If you have any questions or suggestions, feel free to write a comment. I would also appreciate it if you clap for this article and follow me.
Stay tuned!

Photo by Aaron Burden on Unsplash

--

--

Pavlo Kolodka

Passionate Software Engineer with a love for learning and sharing interesting things.