Deploying Your NodeJS Code to a Server Every Time You Push with Github Actions

Deploying Your NodeJS Code to a Server Every Time You Push with Github Actions

Deploying Your NodeJS Code to a Server Every Time You Push with Github Actions

Since they were introduced in late 2019, GitHub Actions have completely changed the ways in which developers handle workflow automation. GitHub Actions allow you to automate tedious repetitive workflows like installing dependencies (npm ci), running tests (npm test), scanning for vulnerabilities (npm audit) and deploying to a server.

Manual deployment should be a thing of the past. A lot of developers simply SSH into their servers, git pull and restart the app. This process is prone to run into a myriad of errors, though, not to mention the fact that it’s slow and repetitive. We can remedy that (for free!) with GitHub Actions.

You can think of an action as a third party library that you can insert somewhere in your pipeline to perform certain tasks. There’s an action for almost anything you can think of – from setting up NodeJs to sending text messages. Individual actions are used inside workflows.

Workflows are triggered by events. An event is almost anything that happens in your git repository – a push, commit, merge, etc. Essentially, any GitHub webhook event will trigger a workflow.

Finally, workflows run on separate Github-owned servers. These servers are referred to as runners. You can host your own runners, but GitHub’s hosted ones will do just fine for our purposes.

Let’s see this in action (no pun intended).

Creating a workflow

A workflow is a custom automated process consisting of jobs that are run in steps. Inside these steps, you can use actions or run bash commands within your package. This way, you can build, test, scan, package and deploy any project on Github.

A workflow is configured using YAML syntax and saved in the ‘.github/actions’ folder. The directory structure should look like this:

So, say we wanted to write a simple command that prints “Hello World” every time we push to the server.

First, we need to specify the name of the workflow

Since we only need our workflow to be triggered when a push is made to our git repository, we need to specify it as such.

Every workflow must contain at least one job. We only have one job in this case so that shouldn’t be a problem.

We’ll name our job “echo.”

We also need to specify the operating system our job will run on. The latest version of Ubuntu will do just fine.

Lastly, we need to define a series of steps that will run sequentially inside our job.

This job has two steps:

  1. Checkout the source code from our git repo using “actions/checkout@v2.” Notice that this is just an action, not a command. You can also leave out the ‘name’ property if you like.
  2. Run “Hello World” on the runner. ‘run’ accepts any valid bash commands.

This is the output we get from Github once you push to the repo.

Output from Github

Output from Github

With the fundamentals out of the way, let’s apply the same principles to see how we can deploy a simple express app every time we push to the master branch.

Deploying to an SSH server

If you don’t have a project up and running already, you can bootstrap one using the command

Before we proceed, let’s lay out the groundwork. Here is how the application is supposed to work:

  1. The developer pushes to a github repo
  2. A deploy workflow is triggered.
  3. The task runner connects to the SSH server.
  4. On the SSH server, the task runner clones the repo.
  5. Dependencies are installed.
  6. Start (or restart) any relevant processes.

You might be curious as to why there isn’t a step for checking out the repo like in the previous section.

We don’t need to checkout or clone the repo because we don’t run any direct commands on it from the runner. We simply connect to the SSH server and run all our commands there.

This is a direct consequence of the lack of potentially resource-intensive tasks such as running tests, creating coverage reports, static code analysis and transpilation. Projects based on frameworks such as React and Typescript (that need a build step) will probably have a different-looking flow.

This is how the above pseudocode looks when represented as a workflow YAML file.

Some steps are unique to this application because we are running it for the first time. Typically, for example, you won’t need to create new folders or clone your repo.

A more typical script would look like this:

In order to log into our server, we have to provide the relevant credentials. These are defined as repository or organization-level secrets.

Creating secrets

To create a secret, head over to the “Settings” tab of your Github repo and click on “Secrets.” Here are the above SSH secrets set up in our dummy repository:

SSH Secrets Set up in Our Dummy Repository

SSH Secrets Set up in Our Dummy Repository

To learn more about how secrets are encrypted, how they are passed to your workflow and the limits imposed on them, follow this Github guide.

Since this is SSH, you’re free to use a private/public key pair if you prefer. Save it as a secret in your repository and supply it to your workflow file in the same manner as above.

If you’d like to see the code on Github (you can’t see the secrets, though.)

Github link:


There are a couple of issues you may run into while attempting to run this tutorial.

npm: command not found – this error is as a result of how commands are run once the task runner connects to your server. If you encounter this add NodeJS to your PATH.

If Node is already included in your path and you still experience this, you will have to run all node-related commands explicitly. If you use NVM, for instance, you will need to run ~/.nvm/versions/node/v12.14.0/bin/npm install instead of regular old ‘npm install’. Additionally, commands such as ‘pm2’ will need to be re-written.

/usr/bin/env: ‘node’: No such file or directory – you will either run into this error because of PM2 or installing Node using a package manager like apt.

For nvm users: ln -s .nvm/versions/node/v<your node version>/bin/node
For apt users: ln -s /usr/bin/nodejs /usr/bin/node

Be careful with this approach. If you ever change your node version, you will also need to change these commands to reflect the change.


Hopefully, this article serves as a great introduction to the world of automated development practices. Modifying your pipeline to automate boring repetitive bits gives developers time to concentrate on more important tasks at hand, besides greatly reducing human interaction with sensitive bits of the system such as deployment.

Lastly, we didn’t get to showcase a lot of impressive things that Github Actions can do in this tutorial, but you’d do well to learn how to integrate a whole CI/CD pipeline just within Github itself.

About the author

Stay Informed

It's important to keep up
with industry - subscribe!

Stay Informed

Looks good!
Please enter the correct name.
Please enter the correct email.
Looks good!

Related articles

Setting Up Automated Semantic Versioning For Your NodeJS Project

Every project with a significant amount of dependencies should implement semantic versioning in order to avoid preventable issues later on. This post ...

Create simple POS with React.js, Node.js, and MongoDB #10: CRUD Supplier

In this chapter, we are going to continue to implement CRUD operation for the Supplier information of a grocery ...


Protecting Your API from Brute Forcing By Rate Limiting in NodeJS

Brute forcing is the most common cybersecurity attack. To avoid facing downtime and potentially leaking user credentials, rate limiting should be ...

No comments yet

Sign in

Forgot password?

Or use a social network account


By Signing In \ Signing Up, you agree to our privacy policy

Password recovery

You can also try to

Or use a social network account


By Signing In \ Signing Up, you agree to our privacy policy