Simple Node.js Production Server with Caddy and PM2 Deployment
December 5, 2022•1,644 words
Tested with an Ubuntu 22.04 Droplet at DigitalOcean, 1GB/1CPU,
and a simple Express app to be deployed
This setup is ideal for small projects, as working with a container repo and managing a cluster takes more time and costs. It also only requires one VPS with little memory to run everything, though can be scaled up or horizontally as both PM2 and Caddy provide load balacing features.
Caddy is an open source web server with automatic HTTPS written in Go.
Of course it doesn't beat nginx performance-wise, but it is alot easier to configure.
The live config API also makes it easy to update the server's config without downtime.
PM2 is a production process manager for Node.js applications with a built-in load balancer.
It allows to keep applications alive forever and reload them without downtime.
It also includes a simple but powerful deployment system.
GitHub Actions can additionally be set up to automatically run the deployment after pushing to a specified branch.
Requirements:
- Ubuntu 22.04 virtual machine, at least 1GB memory recommended
- Domain with A record pointed to the server.
- Connect to the virtual machine with
ssh
after setting it up with the host (initial server setup) - Install Caddy
-sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
-curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
-curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
-sudo apt update
-sudo apt install caddy
- Open ports
sudo ufw allow proto tcp from any to any port 80,443
- Install Node.js using Node Version Manager
-curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
-source ~/.bashrc
-nvm install --lts
- In
~/.bashrc
move the nvm export to the top of the file,
(this later allows the post-setup & post-deploy commands to run when node.js is installed with nvm) - Install PM2 (both on server and local dev environment)
npm install -g pm2
- Set PM2 to run on startup (generates a startup script)
pm2 startup
- Install git, if not already (guide)
- Create SSH key for GitHub (dont provide a passphrase) (for handling multiple keys create a config file)
ssh-keygen -t ed25519 -C GitHub
cat .ssh/id_ed25519.pub
and copy the output
In the GitHub repo settings go to Deploy keys > Add deploy key > paste into key fieldssh -T git@github.com
to check the connection and add to known hosts - Create a directory for the app(s).
For serving files, make sure Caddy has permissions to the corresponding directory.
Else the user which will be running pm2 must be the owner.sudo mkdir /var/www
sudo chown -R [user]:caddy /var/www/web
orsudo chown -R [user] /var/www/api
- In the project, create a PM2 config file named
ecosystem.config.js
(docs)
- For the path value, use the previously created subdirectory.
- For the key value, use the location of the corresponding ssh key for this host.
Example:module.exports = {apps : [{name: "Example",script: './index.js',env_production: {"PORT": 3000}}],deploy : {production : {key : '../../.ssh/id_rsa',user : 'user',host : '000.000.000.00',ref : 'origin/master',repo : 'git@github.com:user/repo.git',path : '/var/www/repo','post-deploy' : 'npm install && pm2 startOrRestart ecosystem.config.js --env production'}}}; - On Windows: to prevent the "sh enoent" error, add
C:\Program Files\Git\bin
to the PATH environment variable
When running the following command for the first time appendsetup
at the endpm2 deploy ecosystem.config.js production
for later use, add it to the scripts in package.json, name it deploy-prod or something - On the server, edit the Caddyfile at
/etc/caddy/Caddyfile
and add a reverse proxy to the API and a www-redirect
(To add a file server or other configurations, view the official docs)example.com { reverse_proxy localhost:3000 } www.example.com { redir https://pilc.cc{uri} }
- Reload caddy to apply the changes
sudo systemctl reload caddy
The application should now be running, accessible on the configured domain
and can be deployed from the local dev environment withnpm run deploy-prod
To check the stats and logs of the running NodeJs app on the server usepm2 monit
- Extra: Setup GitHub Actions for completely automatic deployment when pushing to the master branch (or any other)
On the GitHub repository page, go to Actions > Set up a workflow yourself
change the file name and paste following code:name: Deploy to Productionon:push:branches: [ master ]jobs:deploy:name: Deployruns-on: ubuntu-lateststeps:- name: Check out repository codeuses: actions/checkout@v3- name: Set up SSHrun: |mkdir -p ~/.ssh/echo "$SSH_PRIVATE_KEY" > ./github_action_keysudo chmod 600 ./github_action_keyshell: bashenv:SSH_PRIVATE_KEY: ${{secrets.SSH_PRIVATE_KEY}}- name: Install PM2run: npm install -g pm2- name: Deployrun: pm2 deploy ecosystem.config.js production - Create a new SSH key locally with
ssh-keygen -t ed25519 -C GitHub
name it github_action_key and don't provide a passphrase
On the server, add the content of the generated github_action_key.pub on a new line in~/.ssh/authorized_keys
On GitHub, go to the repo Settings > Secrets > Actions > New Repository Secret
and paste the content of github_action_key and name it SSH_PRIVATE_KEY - In
ecosystem.config.js
change the key path to./github_action_key
and add this line:ssh_options: "StrictHostKeyChecking=no",
(instead of disabling StrictHostKeyChecking an entry to the known_hosts file can be added using a secret as well)
The deployment process should now be run after a push has been made to the branch specified in the Workflow config.
Sauce:
https://pm2.keymetrics.io/docs/usage/quick-start/
https://docs.github.com/en/actions
https://docs.github.com/en/authentication/connecting-to-github-with-ssh/testing-your-ssh-connection
https://dev.to/goodidea/setting-up-pm2-ci-deployments-with-github-actions-1494
https://github.com/Unitech/pm2/issues/3839#issuecomment-448275755
https://github.com/Unitech/pm2-deploy/issues/41#issuecomment-252026730