Hosting NextJS on a private server using PM2 and Github webhooks as CI/CD

Hosting NextJS on a private server using PM2 and Github webhooks as CI/CD

This article shows you how can host your Next.js site on a (virtual private) server with Nginx and a CI/CD pipeline via PM2 and Github Webhooks.

Setup your server

First of all you need an server with root access. I strongly recommend to have a look at the guide "Initial Server Setup with Ubuntu 18.04" from the DigitalOcean community which will lead you through the process of:

  1. Logging in and set up root user access to your server with SSH
  2. Creating a new user
  3. Granting Administrative Privileges to the new user
  4. Setting up a basic firewall
  5. Giving your regular user access to the server with SSH key authentication.

After you have done that you can continue by installing all necessary dependencies on your server.

Install Node.js

Again there is an guide by DigitalOcean which will help you installing Node.js using PPA.

After completing

  • install Node.js, NPM and
  • the "build-essential package"

you will have to change npm's default directory.

  • Create a .npm-global directory and set the path to this directory for node_modules:
cd ~
mkdir ~/.npm-global
npm config set prefix '~/.npm-global'
  • Create (or modify) a ~/.profile and add the following line:
sudo nano ~/.profile
# set PATH so global node modules install without permission issues
export PATH=~/.npm-global/bin:$PATH

Now you have to update your system variables:

source ~/.profile

Now you should be able to check your installed Node.js version with:

node -v

Install git

Check if git is already installed with:

git --version

If it isn't installed yet you can install it with

sudo apt install git

and configure Git with

git config --global user.name "Your Name"
git config --global user.email "youremail@domain.com"

After git is installed and configured you can deploy your project by cloning it from Github.

Deploy from Github

It is important that you are loggin in as non-root user for the following steps.

cd ~
git clone https://github.com/your-name/your-project-repo.git path

Create a .env on the server if you are using one locally and copy/paste your content.

After you have deployed your project (optionally with environment variables) you can install all dependencies and build your Next.js site with:

cd ./my-project/
npm install
NODE_ENV=production npm run build

Now you should have a copy of your local project/Next.js site on your remote server.

Next you are going to setup PM2 which will be used to keep your site alive and restart it after every reboot.

Setup PM2

You can install PM2 with:

npm install pm2@latest -g

You will need to create/configure an ecosystem.config.js file which will restart the default Next.js server.

cd ~
pm2 init
sudo nano ecosystem.config.js

Copy/paste the template and replace the content.

module.exports = {
  apps: [
    {
      name: 'next-site',
      cwd: ' /home/your-name/my-nextjs-project',
      script: 'npm',
      args: 'start',
      env: {
        NEXT_PUBLIC_...: 'NEXT_PUBLIC_...',
      },
    },
    // optionally a second project
],};

With

cd ~
pm2 start ecosystem.config.js

you can start your server which will run on the Port 1337.

You can always check the status with:

pm2 status next-site

After the server reboots this PM2 should be always automatically be restarted. For that you are going to need a small Startup script which you can also copy/paste.

  • Generate and configure a startup script to launch PM2:
cd ~
pm2 startup systemd

[PM2] Init System found: systemd
[PM2] To setup the Startup Script, copy/paste the following command:
**sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u your-name --hp /home/your-name**
  • Copy/paste the generated command:
**sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u your-name --hp /home/your-name**

[PM2] Init System found: systemd
Platform systemd

. . .


[PM2] [v] Command successfully executed.
+---------------------------------------+
[PM2] Freeze a process list on reboot via:
   $ pm2 save

[PM2] Remove init script via:
   $ pm2 unstartup systemd
  • And save the new PM2 process list and environments. Then Start the service with systemctl.
pm2 save

[PM2] Saving current process list...
[PM2] Successfully saved in /home/your-name/.pm2/dump.pm2

If you reboot your server now with sudo reboot the script should be automatically restart your Next.js site. Give it a try!

Setup Github Webhook

One thing missing now is an continuos integration and continuos delivery (CI/CD) pipeline which you will setup using Github webhooks.

Therefore you need to create a new Webhook in your repository.

The following articles provide additional information to the steps below:

You need to create a server script which will do something if it is triggered by the Github webhook.

cd ~
mkdir NodeWebHooks
cd NodeWebHooks
sudo nano webhook.js

The script is going to create a server running on Port 8100. (Your Github webhook should be of course sending the webhook to something like http://server-ip:8100.)

If it gets triggered by a webhook it will

  • go into your repo ~/my-nextjs-project/,
  • pull the latest commits,
  • install all dependencies,
  • build a new version of the site and
  • restart the server via the PM2 script.
const secret = "your-secret-key";
const repo = "~/my-nextjs-project/";

const http = require('http');
const crypto = require('crypto');
const exec = require('child_process').exec;

const BUILD_CMD = 'npm install && NODE_ENV=production npm run build';
const PM2_CMD = 'pm2 restart next-site';

http.createServer(function (req, res) {
    req.on('data', function(chunk) {
        let sig = "sha1=" + crypto.createHmac('sha1', secret).update(chunk.toString()).digest('hex');

        if (req.headers['x-hub-signature'] == sig) {
            exec('cd ' + repo + ` && git pull && npm install && ${BUILD_CMD} && ${PM2_CMD}`);
        }
    });

    res.end();
}).listen(8100);

You will need to allow communication on Port 8100 with:

sudo ufw allow 8100/tcp
sudo ufw enable

Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup

Earlier you setup PM2 to restart the services (your Next.js site) whenever the server reboots or is started. You will now do the same for the webhook script.

  • Run echo $PATH and copy the output for use in the next step.
echo $PATH

/home/your-name/.npm-global/bin:/home/your-name/bin:/home/your-name/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
  • Create a webhook.service file:
cd ~
sudo nano /etc/systemd/system/webhook.service
  • In the editor, copy/paste the following script, but make sure to replace your-name in two places with your username. Earlier, you ran echo $PATH, copy this to the Environment=PATH= variable, then save and exit:
[Unit]
Description=Github webhook
After=network.target

[Service]
Environment=PATH=your_path
Type=simple
User=your-name
ExecStart=/usr/bin/nodejs /home/your-name/NodeWebHooks/webhook.js
Restart=on-failure

[Install]
WantedBy=multi-user.target
  • Enable and start the new service so it starts when the system boots:
sudo systemctl enable webhook.service
sudo systemctl start webhook
  • Check the status of the webhook:
sudo systemctl status webhook

You can test your webhook with these instructions.

The Next.js server is now running on your-ip:3000 and you implemented a CI/CD pipeline via PM2 and Github Webhooks but you still can't access your website via a domain because you need to configure a webserver like Nginx.

Configure Nginx

I am using Cloudflare to manage DNS for my domains but you can do this with every other provider also.

  • Create two A Records which will point your-domain.com and www.your-domain.com to the IP-adress of your server.

After that you will need to configure Nginx.

The following instructions are based on How To Install Nginx on Ubuntu 18.04 [Quickstart].

  • Update your local package index:
sudo apt update
  • install Nginx:
sudo apt install nginx
  • and adjust the Firewall:
sudo ufw allow 'Nginx Full'
sudo ufw delete allow 'Nginx HTTP'

You should now be able to see the Nginx landing page on:

http://your_server_ip

Setting up Server Blocks

  • Create the directory for your-domain.com, using the -p flag to create any necessary parent directories:
sudo mkdir -p /var/www/your-domain.com/html
  • Assign ownership of the directory:
sudo chown -R $USER:$USER /var/www/your-domain.com/html
  • The permissions of your web roots should be correct if you haven’t modified your umask value, but you can make sure by typing:
sudo chmod -R 755 /var/www/example.com
  • Make a new server block at /etc/nginx/sites-available/your-domain.com:
sudo nano /etc/nginx/sites-available/example.com
  • Copy/Paste the following nextjs-nginx configuration and update the server_name sections:
server {
    # Listen HTTP
    listen 80;
    listen [::]:80;

    server_name your-domain.com www.your-domain.com;

    # Redirect HTTP to HTTPS
    return 301 https://$host$request_uri;
}

server {
    # Listen HTTP
    listen 443 ssl;
    listen [::]:443 ssl;

    server_name your-domain.com www.your-domain.com;

    # SSL config
    include snippets/self-signed.conf;
    include snippets/ssl-params.conf;

    # Proxy Config
    location / {
        proxy_pass http://localhost:3000
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_pass_request_headers on;
    }
    location ~ /.well-known {
       allow all;
    }
}

Save the file and close it when you are finished.

  • Enable the file by creating a link from it to the sites-enabled directory:
sudo ln -s /etc/nginx/sites-available/your-domain.com /etc/nginx/sites-enabled/
  • Test for syntax errors:
sudo nginx -t
  • and finally enable the changes:
sudo systemctl restart nginx

Nginx should now be serving content on your domain name. That means if you have a look at http://your-domain.com you should see your Next.js site.

In the end should deny traffic to Port 3000 with:

cd ~
sudo ufw deny 3000

This guide is also using parts of Strapi Deployment on DigitalOcean which helped me a lot setting up Strapi and Next.js on a server in a proper way.

First published September 13, 2020

    0 Webmentions

    Have you published a response to this? Send me a webmention by letting me know the URL.

    Found no Webmentions yet. Be the first!

    About The Author

    Max
    Max

    Geospatial Developer

    Hi, I'm Max (he/him). I am a geospatial developer, author and cyclist from Rosenheim, Germany. Support me

    0 Virtual Thanks Sent.

    Continue Reading