Deploying to Multiple Environments with GitHub Actions

Recently I used GitHub Actions to facilitate automated deployments for a new app. I was impressed overall with GitHub’s new CI offering. It was fairly simple to configure, and so far it hasn’t cost anything, even with a private repo. I also use a cool little Slack integration to notify the team of new deployments.

Initially I configured the GitHub Action in a simple way. When the team pushed to master, it deployed to our dev environment. However, after the app went live, we needed a way to push to both the dev and production environments, using separate branches. To my surprise, there were no clear tutorials on the web for how to set this up. I eventually figured it out by combining information from multiple blog posts and a YouTube video.

In this post, I’ll walk you through configuring GitHub Actions to deploy to multiple self-hosted environments. This way you can push to a dev or QA environment for testing and verification of changes, and then to a production environment once everything is working well.

Installing the runner

First you will want to create a “runner.” A runner is essentially a service that listens for push events in your repo. GitHub offers two types of runners: Github-hosted runners (which run on Github’s servers), and self-hosted runners. Since we used AWS to host our app, we used self-hosted runners.

You can create a self-hosted runner in GitHub by going to your repo > Settings > Actions > Runners. Click on the New Self-hosted Runner button and it will walk you through the process of downloading and configuring the runner on your target environment.

Note: If you’d like to share a runner with multiple repos, create the runner on the Organization level instead.

Configuring the runner as a service

The instructions that GitHub provides show you how to run the service directly in the terminal.

You most likely won’t want to do this, however, since once you exit the terminal, the runner will stop. Instead, you’ll want to configure it as a background service, using these instructions. Note that all the commands in the instructions should be run within the actions-runner folder that you created when downloading the runner.

Repeat as needed for each target environment

Repeat the steps above for each environment/server you’d like to deploy to (dev, prod, etc.).

Adding tags to the runners

Once the runners are created for all environments, go to your repo > Settings > Actions > Runners. Click on each runner, and add a label with your environment name (i.e., development, production, etc.). This label will be used to match a push event to a specific environment.

Create the yaml file

Next, you’ll create a yaml file with instructions on when and how to deploy. Go to your repo page and click on the Actions tab at the top, then click on “Set up a workflow yourself.”

You’ll then be dropped into the GitHub editor where you can create your yaml file. Here is the file I used to deploy to separate dev and production environments. Our app was built using Nuxt, so the run commands use Node and pm2. You’ll want to change this of course, based on your specific build steps and requirements.

# This is a basic workflow to help you get started with Actions

name: Frontend CI

# Controls when the action will run. 
on:
  # Triggers the workflow on push or pull request events
  push:
    branches: [ dev, master ]

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  dev_deploy:
    # The type of runner that the job will run on
    runs-on: [self-hosted, development]
    # look at the ref and only run this job if the branch that triggered the workflow is "dev"
    if: endswith(github.ref, 'dev')
    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
    - uses: actions/checkout@v2
    - name: Use Node.js
      uses: actions/setup-node@v1
      with:
        node-version: '14.x'
    - run: npm install
    - run: npm run test
    - run: npm run build
    - run: pm2 start
  prod_deploy:
    # The type of runner that the job will run on
    runs-on: [self-hosted, production]
    # look at the ref and only run this job if the branch that triggered the workflow is master
    if: endswith(github.ref, 'master')
    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
    - uses: actions/checkout@v2
    - name: Use Node.js
      uses: actions/setup-node@v1
      with:
        node-version: '14.x'
    - run: npm install
    - run: npm run test
    - run: npm run build
    - run: pm2 start
  notify:
    needs: prod_deploy
    runs-on: ubuntu-latest
    steps:
    - name: Slack Notify
      uses: rtCamp/action-slack-notify@v2.1.3
    env:
      SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

A few important things to note about the above yaml:

  1. The “on” section is watching for push events to two branches: dev and master
  2. There are two deploy jobs (dev_deploy and prod_deploy).
  3. The “runs-on” section use the runner label created above to target the correct runner and environment.
  4. The “if” section uses the endswith function to apply conditional logic for the job based on the branch name from github.ref.
  5. The final job, notify, used the Slack integration mentioned above, and is totally optional. Notice the needs section, which ensures that the notification is sent for a production deployment only.

In a nutshell, the runs-on section targets the environment based on the runner label, and the if section ensures that only pushes to a specific branch get deployed to that environment. For example, a push to the dev branch will get deployed to the dev environment, via the runner with the “development” label.

Finishing up

Once the yaml file is ready, go ahead and commit it to master, and the GitHub Actions deployments will automatically start. You can view the deployments, including details on each run, in the Actions tab.

Also, the specific folder that GitHub uses for deployments on the target server is actions-runner/_work. Use that folder for hosting your app, or to troubleshoot issues in the code.

Let me know if you have any questions or problems using the comments below.

Need additional help? Hire me.

2 thoughts on “Deploying to Multiple Environments with GitHub Actions”

  1. I tried doing this, but was unable to create a second runner for my prod env. It said a runner already exists. I found a similar error online and it said it had something to do with a config directory? What did you do?

    1. I didn’t receive that error when I installed my second runner. Are you installing the second runner on a separate server, or on the same server?

Leave a comment