Implement Node.js application deployment with Git hooks and PM2 clustering

Intermediate 45 min May 04, 2026 125 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Set up automated Node.js application deployment using Git hooks for continuous integration, combined with PM2 clustering for high availability and load distribution across multiple CPU cores.

Prerequisites

  • Server with sudo access
  • Basic Git knowledge
  • Node.js application to deploy

What this solves

This tutorial creates an automated deployment pipeline for Node.js applications using Git hooks combined with PM2 process management for production clustering. You'll set up Git-based continuous deployment where pushing code automatically restarts your application, plus PM2 clustering to distribute load across CPU cores for high availability.

Step-by-step installation

Install Node.js 20

Install the latest LTS version of Node.js using the NodeSource repository for better package management.

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs build-essential
node --version
npm --version
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
sudo dnf install -y nodejs npm gcc-c++ make
node --version
npm --version

Install PM2 process manager

PM2 provides clustering, monitoring, and process management for Node.js applications in production environments.

sudo npm install -g pm2
pm2 --version

Create application directory structure

Set up the directory structure for your application with separate folders for the Git repository and the deployed application.

sudo mkdir -p /var/www/myapp
sudo mkdir -p /var/git/myapp.git
sudo useradd -r -s /bin/bash -d /var/www/myapp nodeapp
sudo chown -R nodeapp:nodeapp /var/www/myapp
sudo chown -R nodeapp:nodeapp /var/git/myapp.git

Initialize bare Git repository

Create a bare Git repository that will receive pushed code and trigger deployments via post-receive hooks.

sudo -u nodeapp git init --bare /var/git/myapp.git
sudo -u nodeapp git config --global user.name "Deployment System"
sudo -u nodeapp git config --global user.email "deploy@example.com"

Create post-receive Git hook

Set up the Git hook that automatically deploys your application when code is pushed to the repository.

#!/bin/bash

Configuration

APP_DIR="/var/www/myapp" GIT_DIR="/var/git/myapp.git" BRANCH="main" PM2_APP_NAME="myapp"

Change to application directory

cd $APP_DIR

Check out the latest code

echo "Deploying application to $APP_DIR" git --git-dir=$GIT_DIR --work-tree=$APP_DIR checkout -f $BRANCH

Install dependencies

echo "Installing dependencies..." npm ci --only=production

Run any build steps if needed

if [ -f "package.json" ] && grep -q '"build":' package.json; then echo "Running build..." npm run build fi

Restart PM2 application

echo "Restarting application with PM2..." if pm2 describe $PM2_APP_NAME > /dev/null 2>&1; then pm2 restart $PM2_APP_NAME else pm2 start ecosystem.config.js fi

Save PM2 configuration

pm2 save echo "Deployment completed successfully!"

Make Git hook executable

Set the correct permissions for the Git hook script and ensure proper ownership.

sudo chmod +x /var/git/myapp.git/hooks/post-receive
sudo chown nodeapp:nodeapp /var/git/myapp.git/hooks/post-receive

Create PM2 ecosystem configuration

Configure PM2 clustering settings to utilize all available CPU cores with automatic restart and monitoring.

module.exports = {
  apps: [{
    name: 'myapp',
    script: './app.js',
    instances: 'max',
    exec_mode: 'cluster',
    autorestart: true,
    watch: false,
    max_memory_restart: '1G',
    env: {
      NODE_ENV: 'production',
      PORT: 3000
    },
    error_file: '/var/log/myapp/err.log',
    out_file: '/var/log/myapp/out.log',
    log_file: '/var/log/myapp/combined.log',
    time: true,
    max_restarts: 10,
    min_uptime: '10s',
    kill_timeout: 5000,
    restart_delay: 1000
  }]
};

Create log directory

Set up logging directory with proper permissions for PM2 to write application logs.

sudo mkdir -p /var/log/myapp
sudo chown nodeapp:nodeapp /var/log/myapp
sudo chmod 755 /var/log/myapp

Create sample Node.js application

Create a basic Express.js application to test the deployment pipeline and clustering setup.

const express = require('express');
const cluster = require('cluster');
const os = require('os');

const app = express();
const port = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.json({
    message: 'Hello from Node.js deployment!',
    pid: process.pid,
    hostname: os.hostname(),
    timestamp: new Date().toISOString(),
    uptime: process.uptime()
  });
});

app.get('/health', (req, res) => {
  res.status(200).json({ status: 'healthy', pid: process.pid });
});

app.listen(port, () => {
  console.log(Server running on port ${port}, PID: ${process.pid});
});

process.on('SIGTERM', () => {
  console.log('SIGTERM received, shutting down gracefully');
  process.exit(0);
});

Create package.json

Define application dependencies and scripts for the deployment process.

{
  "name": "myapp-deployment",
  "version": "1.0.0",
  "description": "Node.js application with automated deployment",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js",
    "test": "echo \"No tests specified\""
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}

Install application dependencies

Install the required Node.js packages as the application user.

sudo -u nodeapp bash -c "cd /var/www/myapp && npm install"

Configure PM2 startup

Set up PM2 to automatically start your application on system boot with proper user permissions.

sudo -u nodeapp pm2 start /var/www/myapp/ecosystem.config.js
sudo -u nodeapp pm2 save
sudo pm2 startup systemd -u nodeapp --hp /var/www/myapp
sudo systemctl enable pm2-nodeapp

Create deployment rollback script

Add a rollback mechanism to quickly revert to the previous deployment if issues occur.

#!/bin/bash

APP_DIR="/var/www/myapp"
GIT_DIR="/var/git/myapp.git"
PM2_APP_NAME="myapp"

echo "Rolling back to previous commit..."
cd $APP_DIR

Get the previous commit hash

PREV_COMMIT=$(git --git-dir=$GIT_DIR log --format="%H" -n 2 | tail -1) if [ -z "$PREV_COMMIT" ]; then echo "Error: No previous commit found" exit 1 fi

Checkout previous commit

git --git-dir=$GIT_DIR --work-tree=$APP_DIR checkout -f $PREV_COMMIT

Reinstall dependencies

npm ci --only=production

Restart application

pm2 restart $PM2_APP_NAME echo "Rollback completed to commit: $PREV_COMMIT"

Make rollback script executable

Set proper permissions for the rollback script.

sudo chmod +x /var/www/myapp/rollback.sh
sudo chown nodeapp:nodeapp /var/www/myapp/rollback.sh

Configure reverse proxy with NGINX

Set up NGINX to proxy requests to your PM2-managed Node.js cluster for better performance and SSL termination.

sudo apt install -y nginx
sudo dnf install -y nginx
upstream nodejs_backend {
    server 127.0.0.1:3000;
    keepalive 32;
}

server {
    listen 80;
    server_name example.com www.example.com;
    
    access_log /var/log/nginx/myapp_access.log;
    error_log /var/log/nginx/myapp_error.log;
    
    location / {
        proxy_pass http://nodejs_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $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_cache_bypass $http_upgrade;
        proxy_redirect off;
    }
    
    location /health {
        proxy_pass http://nodejs_backend/health;
        access_log off;
    }
}

Enable NGINX configuration

Activate the NGINX configuration and restart the service.

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
sudo systemctl enable nginx

Set up local Git repository for deployment

Create a local development repository and configure it to push to your deployment server.

mkdir ~/myapp-dev
cd ~/myapp-dev
git init
cp /var/www/myapp/app.js .
cp /var/www/myapp/package.json .
cp /var/www/myapp/ecosystem.config.js .
git add .
git commit -m "Initial commit"
git remote add production nodeapp@your-server-ip:/var/git/myapp.git

Configure monitoring and health checks

Create health check script

Set up automated health monitoring to ensure your application cluster is responding correctly.

#!/bin/bash

APP_URL="http://localhost:3000/health"
PM2_APP_NAME="myapp"
MAX_RETRIES=3
RETRY_COUNT=0

while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
    if curl -f -s $APP_URL > /dev/null; then
        echo "Health check passed"
        exit 0
    fi
    
    RETRY_COUNT=$((RETRY_COUNT + 1))
    echo "Health check failed, retry $RETRY_COUNT/$MAX_RETRIES"
    sleep 5
done

echo "Health check failed after $MAX_RETRIES attempts, restarting PM2"
pm2 restart $PM2_APP_NAME
exit 1

Schedule health checks with cron

Add automated health checks to run every 5 minutes to ensure high availability.

sudo chmod +x /var/www/myapp/healthcheck.sh
sudo chown nodeapp:nodeapp /var/www/myapp/healthcheck.sh
sudo -u nodeapp crontab -l > /tmp/nodeapp_cron 2>/dev/null || true
echo "/5    * /var/www/myapp/healthcheck.sh >> /var/log/myapp/healthcheck.log 2>&1" >> /tmp/nodeapp_cron
sudo -u nodeapp crontab /tmp/nodeapp_cron
rm /tmp/nodeapp_cron

Verify your setup

# Check PM2 processes
sudo -u nodeapp pm2 list
sudo -u nodeapp pm2 monit

Test application endpoint

curl http://localhost:3000/ curl http://localhost:3000/health

Check NGINX status

sudo systemctl status nginx curl -I http://localhost/

View application logs

sudo -u nodeapp pm2 logs myapp --lines 50

Test deployment (from development directory)

cd ~/myapp-dev echo "console.log('Deployment test');" >> app.js git add . git commit -m "Test deployment" git push production main

Common issues

SymptomCauseFix
Git push fails with permission deniedSSH key not configuredAdd your SSH key: ssh-copy-id nodeapp@server-ip
PM2 processes exit immediatelyApplication crashes on startupCheck logs: pm2 logs myapp and fix application errors
Deployment hook doesn't runHook not executable or wrong permissionschmod +x /var/git/myapp.git/hooks/post-receive
npm install fails during deploymentMissing build tools or permissionsInstall build-essential and check directory permissions
Application not accessible via NGINXNGINX configuration or upstream issueCheck nginx -t and verify PM2 is listening on port 3000
PM2 doesn't restart on bootStartup script not configuredRun pm2 startup and follow the instructions

Next steps

Running this in production?

Want this handled for you? Setting up PM2 clustering once is straightforward. Keeping it patched, monitored, backed up and tuned across environments is the harder part. See how we run infrastructure like this for European SaaS and e-commerce teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed devops services for businesses that depend on uptime. From initial setup to ongoing operations.