Simple Node.js Production Server with Caddy and PM2 Deployment

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.

  1. Connect to the virtual machine with ssh after setting it up with the host (initial server setup
  2. 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
  3. Open ports
    sudo ufw allow proto tcp from any to any port 80,443
  4. 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
  5. 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)
  6. Install PM2 (both on server and local dev environment)
    npm install -g pm2
  7. Set PM2 to run on startup (generates a startup script)
    pm2 startup
  8. Install git, if not already (guide)
  9. 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 field
    ssh -T git@github.com to check the connection and add to known hosts
  10. 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
    or sudo chown -R [user] /var/www/api
  11. 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'
        }
      }
    };
  12. 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 append setup at the end
    pm2 deploy ecosystem.config.js production
    for later use, add it to the scripts in package.json, name it deploy-prod or something
  13. On the server, edit the Caddyfile at /etc/caddy/Caddyfile and add a reverse proxy to the API and a www-redirect
    example.com {
            reverse_proxy localhost:3000
    }
    
    www.example.com {
            redir https://pilc.cc{uri}
    }
    (To add a file server or other configurations, view the official docs)
  14. 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 with npm run deploy-prod
    To check the stats and logs of the running NodeJs app on the server use pm2 monit

  15. 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 Production
    on:
      push:
        branches: [ master ]
    jobs:
      deploy:
        name: Deploy
        runs-on: ubuntu-latest
        steps:
        - name: Check out repository code
          uses: actions/checkout@v3
        - name: Set up SSH
          run: |
            mkdir -p ~/.ssh/
            echo "$SSH_PRIVATE_KEY" > ./github_action_key
            sudo chmod 600 ./github_action_key
          shell: bash
          env:
            SSH_PRIVATE_KEY: ${{secrets.SSH_PRIVATE_KEY}}
        - name: Install PM2
          run: npm install -g pm2
        - name: Deploy
          run: pm2 deploy ecosystem.config.js production
  16. 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

  17. 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://caddyserver.com/docs/

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


You'll only receive email when they publish something new.

More from Cili's Notes
All posts