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
- Copy the contents of the public file:
cat gh_rsa.pub | pbcopy
(or if you don't havepbcopy
, justcat
and then copy from the terminal output). - SSH into your remote server:
ssh username@SERVER_IP
- 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:
- 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.
-
- 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'sauthorized_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 theenv
command to load the file we created.
6. Cleanup & security
- Add
deploy.env
to your.gitignore
file. - Delete the
gh_rsa
andgh_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)
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
What ended up working was
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.
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)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.
Could this be used in place of the custom script that configures SSH? github.com/marketplace/actions/ssh...
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?
I think you meant deploy.key