Setting up PM2 CI deployments with Github Actions
Tags: pm2, github, ci
Published: 2020-08-26 04:59:11

---
title: Setting up PM2 CI deployments with Github Actions
published: true
description: 
tags: pm2, github, ci
---

I spent the afternoon wrestling with getting some Github actions set up for CI using [pm2](https://pm2.keymetrics.io/). I had some issues & confusion around SSH issues, and couldn't find anything online, so I wanted to post this here --- even if it's just for me to refer to later. In particular, I was dealing with `Host key verification failed.` when attempting to deploy from the Github action.

If you're not familiar with PM2, it's a process manager that will run your node.js apps, allowing you to stop, restart, and view logs.

I'm deploying to a DigitalOcean droplet, but the following should be the same for any other VPS. Here are a few tutorials you can follow to recreate the same setup:

- [Set up a new Ubuntu droplet & sudo user](https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-18-04)
- [Set up Node.js & serve with Nginx](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-18-04)
- [Set up PM2](https://www.digitalocean.com/community/tutorials/how-to-use-pm2-to-setup-a-node-js-production-environment-on-an-ubuntu-vps)

Once you have your server set up and your repository hosted on Github, follow these steps:

#### 1. Set up PM2 configuration

Create or update `ecosystem.config.js`. Mine looks like this:

```js
module.exports = {
  apps: [
    {
      name: 'myapp-api',
      script: 'yarn start:api',
      time: true,
      instances: 1,
      autorestart: true,
      max_restarts: 50,
      watch: false,
      max_memory_restart: '1G',
      env: {
        PORT: 3000,
        DATABASE_ADDRESS: process.env.DATABASE_ADDRESS
      },
    },
  ],
  deploy: {
    production: {
      user: 'username',
      host: '165.232.50.103',
      key: 'deploy.key',
      ref: 'origin/main',
      repo: 'https://github.com/username/myapp',
      path: '/home/username/myapp',
      'post-deploy':
        'yarn install && yarn build && pm2 reload ecosystem.config.js --env production && pm2 save && git checkout yarn.lock',
      env: {
        NODE_ENV: 'production',
        DATABASE_ADDRESS: process.env.DATABASE_ADDRESS
      },
    },
  },
}
```

Make sure you aren't missing the `deploy.production.key` value, and that your `ref` matches the branch you want to deploy from on github. (I've renamed my `master` branch to `main`, see [npx no-masters](https://github.com/good-idea/no-masters))

#### 2. Create a SSH key pair

To get things to work, we need to be able to ssh into our remote server from the machine running the github action. Our next step is to create some new SSH keys. (You could use ones you already have, but it's safer to create new ones just for this project).

On your local machine, run:

`ssh-keygen -t rsa -b 4096 -C "username@SERVER_IP" -q -N ""`

When prompted about the file location, enter `gh_rsa`, or anything else you'd like - we will delete these files after getting the information we need from them.

#### 3. Remote server setup

1. Copy the contents of the public file: `cat gh_rsa.pub | pbcopy` (or if you don't have `pbcopy`, just `cat` and then copy from the terminal output).
2. SSH into your remote server: `ssh username@SERVER_IP`
3. Add the public key to your user's `authorized_keys`:

  `echo "ssh-rsa AAAA....YOUR_PUBLIC_KEY..." >> ~/.ssh/authorized_keys`

#### 4. Github Secrets setup

Next, we'll add some secrets to the Github repo for use in the action. Go to your repo's Settings > Secrets pane, then add two new keys:

1. Secret key:
  - `cat gh_rsa | pbcopy` to copy the private key to your clipboard
  - In Github, create a new secret named `SSH_PRIVATE_KEY` and paste in the contents.
2. Known hosts:
  - `ssh-keyscan SERVER_IP > pbcopy`
  - In Github, create a new secret named `SSH_KNOWN_HOSTS` and paste in the contents.

#### 5. Github Action configuration

Lastly, on your local machine, create or update the file `.github/workflows/main.yml` (or whatever file you are using for your action):


```
name: CI - Master
  on:
    push:
      branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      #
      # ... your other steps, such as running tests, etc...
      #
      - name: Set up SSH
        run: |
          mkdir -p ~/.ssh/
          echo "$SSH_PRIVATE_KEY" > ./deploy.key
          sudo chmod 600 ./deploy.key
          echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
        shell: bash
        env:
          SSH_PRIVATE_KEY: ${{secrets.SSH_PRIVATE_KEY}}
          SSH_KNOWN_HOSTS: ${{secrets.SSH_KNOWN_HOSTS}}

      # (optional - only needed if your config uses environment variables)
      - name: Create env file
        run: |
          touch .env
          echo DATABASE_ADDRESS=${{ secrets.DATABASE_ADDRESS }} >> .env

      - name: Install PM2
        run: npm i pm2

      - name: Deploy
        run: env $(cat .env | grep -v \"#\" | xargs) pm2 deploy ecosystem.config.js staging
        # Or alternately, put this deploy script in your package.json's scripts and run it using yarn/npm:
        # run: yarn deploy
```

The key steps here are:

- **Set up SSH**: this creates the `deploy.key` file, so when pm2 deploys, it matches the public key that we added to the server's `authorized_keys`.
- **Create Env File**: If your PM2 configuration uses environment variables, we need to use our Github secrets to populate `process.env` with these variables. In our "Deploy" step, we use the `env` command to load the file we created.

#### 6. Cleanup & security

1. Add `deploy.env` to your `.gitignore` file.
2. Delete the `gh_rsa` and `gh_rsa.pub` files that we created. Most importantly: if you created your SSH keys within your project's directory, *be sure to not commit them*.

### Conclusion

That should do it! If you have any issues, please leave a comment and I'll get back as soon as I can. :)