DEV Community

Joseph Thomas
Joseph Thomas

Posted on

Setting up PM2 CI deployments with Github Actions

I spent the afternoon wrestling with getting some Github actions set up for CI using pm2. 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:

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:

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)

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. :)

Top comments (7)

Collapse
 
arpitvasani profile image
Arpit Vasani

Thanks for making this. PM2 docs is very short on several big commands/switches. Had to made several changes in my case but very useful tutorial. πŸ‘πŸ‘

not sure you faced this or not but in my case github was throwing an error when using your yml which is

name: CI - Master
  on:
    push:
      branches: [ main ] 
Enter fullscreen mode Exit fullscreen mode

What ended up working was

name: CI - Master
on:  # no indentation needed in this section 
  push:
    branches: [ main ]
Enter fullscreen mode Exit fullscreen mode
Collapse
 
nikhilkamboj796 profile image
nikhilkamboj796

Great blog. But I have one issue with my deployment. So, I have used root privileges to use pm2 to start my process on 80 PORT because with normal user I can't access 80 PORT, right, but when I use Github Actions yml to pull, create build and then try to restart pm2 using script then it is not able to access the pm2 as at this time it is only using normal user access not the root one, so how do I fix this? Please help me.

Collapse
 
bar22775192 profile image
Bar • Edited

Great solution!
In your workflow config file, you forgot to write the key file in the ~/.ssh/ directory
echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy.key
(I added the /ssh/ that was missing)

Collapse
 
ryanblunden profile image
Ryan Blunden

Excellent article and great to see the use of GitHub Secrets for securing app secrets!

I recently created documentation on how to improve the secrets management for PM2 using the Doppler secrets manager at docs.doppler.com/docs/pm2 and wanted to share it here as a more secure alternative to .env files.

Collapse
 
victorioberra profile image
Victorio Berra

Could this be used in place of the custom script that configures SSH? github.com/marketplace/actions/ssh...

Collapse
 
arthurfedotiev profile image
Arthur-Fedotiev

This is a treasure, thank you a lot!
Guys, the question to everyone... I had to add one more intermediary step:
pm2 deploy ecosystem.config.js setup
Is this not necessary in most of the cases?

Collapse
 
gekh profile image
German Khokhlov

Add deploy.env to your .gitignore file.

I think you meant deploy.key