Skip to content

Instantly share code, notes, and snippets.

@FTKhanFT
Last active January 26, 2025 18:14
Show Gist options
  • Save FTKhanFT/b87ef1bcfff51814b45815ad708fdcc1 to your computer and use it in GitHub Desktop.
Save FTKhanFT/b87ef1bcfff51814b45815ad708fdcc1 to your computer and use it in GitHub Desktop.
Deploy NodeJs/TS/Any Js framework app in ubuntu using pm2. Also supports Prisma ORM. Uses git url or project directory. Uses ecosystem.config.js file in the project directory to handle the pm2 management.
#!/bin/bash
# Author: Tanvir Ahmed Khan
# License: MIT License
# Github: https://github.com/ftkhanft
# Website: https://rrad.ltd
# Before running this make sure you have the ecosystem.config.js file in your project directory you cloned or in the git repo that you want to deploy. Referance file added below this script.
# Use the setup script added below this script or directly run in terminal:
# curl -s -L https://gist.githubusercontent.com/FTKhanFT/b87ef1bcfff51814b45815ad708fdcc1/raw/4f47ef626f2865b601cb81d628e4c89f04db1604/setup_script.sh | bash
# Then you will see a deploy.sh script in the current directory which you can use to deploy.
# Colors for console outputs
ERROR='\033[0;31m'
WARNING='\033[1;33m'
SUCCESS='\033[0;32m'
PURPLE='\033[0;35m'
NC='\033[0m'
# Function to display usage
usage() {
echo "Usage: ./deploy.sh"
echo "Options:"
echo " --dir reletive/path Specify directory to work on."
echo " --f To force deployment."
echo " --gu git_username Specify the Git username."
echo " --gp git_password Specify the Git password or access token."
echo " --prisma true/false Enable or disable Prisma commands (true/false)."
echo " --git-url git_url Specify the Git repository URL."
echo " --pm2-id id Specify the PM2 process ID to stop/restart. If id is not provided, will try to use ecosystem.config.js file. If none is found, will do nothing."
exit 1
}
stopPm2(){
# Check if PM2 is installed
if ! command -v pm2 &> /dev/null; then
echo -e "${PURPLE}[Deploy.sh]:${NC} PM2 is not installed. Skipping stop process..."
return
fi
# Stop PM2 processes based on options
if [[ -n "$PM2_ID" ]]; then
echo -e "${PURPLE}[Deploy.sh]:${NC} Stopping PM2 process with ID: $PM2_ID..."
pm2 stop "$PM2_ID"
elif [[ -f "ecosystem.config.js" ]]; then
echo -e "${PURPLE}[Deploy.sh]:${NC} Stopping PM2 processes using ecosystem.config.js..."
pm2 stop ecosystem.config.js
else
echo -e "${PURPLE}[Deploy.sh]:${NC} PM2 id or ecosystem.config.js not found. Skipping pm2 stop process..."
fi
}
restartPm2(){
# Check if PM2 is installed
if ! command -v pm2 &> /dev/null; then
echo -e "${PURPLE}[Deploy.sh]:${NC} PM2 is not installed. Skipping restart process..."
return
fi
# Restart PM2 processes based on options
if [[ -n "$PM2_ID" ]]; then
echo -e "${PURPLE}[Deploy.sh]:${NC} Restarting PM2 process with ID: $PM2_ID..."
pm2 restart "$PM2_ID" --time --update-env || { echo -e "${ERROR}PM2 restart failed"; exit 1; }
pm2 save
elif [[ -f "ecosystem.config.js" ]]; then
echo -e "${PURPLE}[Deploy.sh]:${NC} Restarting PM2 processes using ecosystem.config.js..."
pm2 restart ecosystem.config.js --time --update-env || { echo -e "${PURPLE}[Deploy.sh]:${NC} ${ERROR}PM2 restart failed"; exit 1; }
pm2 save
else
echo -e "${PURPLE}[Deploy.sh]:${NC} PM2 id or ecosystem.config.js not found. Skipping pm2 restart process..."
fi
}
transformRepo(){
local REMOTE_URL="$1" # Use the passed argument as the repository URL
# Determine the correct repository URL based on SSH configuration
if $SSH_CONFIGURED; then
echo -e "${PURPLE}[Deploy.sh]:${NC} SSH is configured. Using SSH URL."
AUTH_REPO_URL=$(echo "$REMOTE_URL" | sed "s#https://github.com/#[email protected]:#")
else
echo -e "${PURPLE}[Deploy.sh]:${NC} SSH is not configured. Using HTTPS."
if [[ "$REMOTE_URL" == [email protected]:* ]]; then
echo -e "${PURPLE}[Deploy.sh]:${NC} ${ERROR}Repository url is an SSH URL but ssh with github is not configured. Exiting..."
exit 1
fi
# Prompt for username and password if not provided
GIT_USERNAME=${GIT_USERNAME:-$DEPLOY_GIT_USERNAME}
GIT_PASSWORD=${GIT_PASSWORD:-$DEPLOY_GIT_PASSWORD}
if [[ -z "$GIT_USERNAME" ]]; then
read -p "Enter your Git username: " GIT_USERNAME
fi
if [[ -z "$GIT_PASSWORD" ]]; then
read -s -p "Enter your Git password: " GIT_PASSWORD
echo
fi
# Set AUTH_REPO_URL based on GIT_USERNAME and GIT_PASSWORD
if [[ -z "$GIT_USERNAME" || -z "$GIT_PASSWORD" ]]; then
AUTH_REPO_URL="$REMOTE_URL"
else
AUTH_REPO_URL=$(echo "$REMOTE_URL" | sed "s#https://#https://$GIT_USERNAME:$GIT_PASSWORD@#")
fi
fi
}
installNodeJs(){
# Check if npm is installed, if not install it
if ! command -v npm &> /dev/null; then
echo -e "${PURPLE}[Deploy.sh]:${NC} ${WARNING}NodeJs is not installed. Installing NodeJs 18.x${NC}"
sudo apt update
curl -sL https://deb.nodesource.com/setup_18.x -o nodesource_setup.sh
sudo bash nodesource_setup.sh
sudo apt install nodejs || { echo -e "${PURPLE}[Deploy.sh]:${NC} Failed to install npm"; exit 1; }
rm nodesource_setup.sh
else
echo -e "${PURPLE}[Deploy.sh]:${NC} ${SUCCESS}NodeJs already installed.${NC}"
fi
}
installPm2(){
# Check if package.json exists
if [[ ! -f "package.json" ]]; then
echo -e "${PURPLE}[Deploy.sh]:${NC} package.json does not exist. Skipping pm2 install..."
return
fi
installNodeJs
# Check if PM2_ID exists or ecosystem.config.js file exists
if [[ -z "$PM2_ID" && ! -f "ecosystem.config.js" ]]; then
echo -e "${PURPLE}[Deploy.sh]:${NC} Neither PM2_ID nor ecosystem.config.js found. Skipping PM2 installation..."
return
fi
# Check if PM2 is installed, if not install it
if ! command -v pm2 &> /dev/null; then
echo -e "${PURPLE}[Deploy.sh]:${NC} PM2 is not installed. Installing PM2..."
npm install pm2@latest -g || { echo -e "${PURPLE}[Deploy.sh]:${NC} Failed to install PM2"; exit 1; }
# Run PM2 startup command with current username
CURRENT_USER=$(whoami)
echo -e "${PURPLE}[Deploy.sh]:${NC} Setting up PM2 to run on startup..."
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u "$CURRENT_USER" --hp /home/"$CURRENT_USER"
else
echo -e "${PURPLE}[Deploy.sh]:${NC} pm2 already installed..."
fi
}
runNpmInstall(){
# Check if package.json exists
if [[ ! -f "package.json" ]]; then
echo -e "${PURPLE}[Deploy.sh]:${NC} package.json does not exist. Skipping npm install..."
return
fi
# Check if GIT_URL is provided
if [[ -n "$GIT_URL" ]]; then
echo -e "${PURPLE}[Deploy.sh]:${NC} GIT_URL is provided. Installing npm dependencies..."
npm install || { echo -e "${PURPLE}[Deploy.sh]:${NC} ${ERROR}Install dependency failed"; exit 1; }
else
# Check if package.json has changed from the previous commit or FORCED is false
if [[ "$FORCED" == true ]] || git diff --name-only HEAD^ HEAD | grep -q "^package.json$"; then
npm install || { echo -e "${PURPLE}[Deploy.sh]:${NC} ${ERROR}Install dependency failed"; exit 1; }
else
echo -e "${PURPLE}[Deploy.sh]:${NC} No changes in package.json. Skipping npm install..."
fi
fi
}
is_stash_required() {
if [[ -n $(git status --porcelain) ]]; then
echo -e "${PURPLE}[Deploy.sh]:${NC} There are local changes checking if need to stash..."
PULL_DRY_OUTPUT=$(git pull "$AUTH_REPO_URL" --dry-run 2>&1)
if echo "$PULL_DRY_OUTPUT" | grep -q "CONFLICT"; then
echo -e "${ERROR}------------Dry Run Output------------\n$PULL_DRY_OUTPUT\n------------Dry Run Output------------${NC}"
return 0 # Stashing is required
else
return 1 # Stashing is not required
fi
else
echo -e "${PURPLE}[Deploy.sh]:${NC} There are no local changes."
return 1 # No uncommitted changes, stashing not required
fi
}
# Default values
PRISMA=false
FORCED=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--dir)
TARGET_DIR="$2"
shift
shift
;;
--git-url)
GIT_URL="$2"
shift
shift
;;
--gu)
GIT_USERNAME="$2"
shift
shift
;;
--gp)
GIT_PASSWORD="$2"
shift
shift
;;
--pm2-id)
PM2_ID="$2"
shift
shift
;;
--prisma)
PRISMA="$2"
shift
shift
;;
--f)
FORCED=true
shift
;;
*)
echo -e "${PURPLE}[Deploy.sh]:${NC} Unknown argument: $1"
usage
;;
esac
done
# Validate the input arguments
if [[ -z "$TARGET_DIR" && -z "$GIT_URL" ]]; then
echo -e "${PURPLE}[Deploy.sh]:${NC} ${ERROR}Error: Either target directory or Git URL is required.${NC}"
usage
fi
if [[ -n "$TARGET_DIR" && -n "$GIT_URL" ]]; then
echo -e "${PURPLE}[Deploy.sh]:${NC} ${ERROR}Error: Please provide either target directory or Git URL, not both.${NC}"
usage
fi
# Check if SSH is configured
echo -e "${PURPLE}[Deploy.sh]:${NC} Checking if SSH is set up for GitHub..."
SSH_CONFIGURED=false
if ssh -T [email protected] 2>&1 | grep -q "successfully authenticated"; then
SSH_CONFIGURED=true
fi
# If GIT_URL is provided, check SSH and clone the repository
if [[ -n "$GIT_URL" ]]; then
echo -e "${PURPLE}[Deploy.sh]:${NC} Using Git URL: $GIT_URL"
transformRepo "$GIT_URL"
echo -e "${PURPLE}[Deploy.sh]:${NC} Cloning repository from Git URL: $AUTH_REPO_URL"
git clone "$AUTH_REPO_URL" || { echo -e "${PURPLE}[Deploy.sh]:${NC} ${ERROR}Failed to clone repository"; exit 1; }
TARGET_DIR=$(basename "$GIT_URL" .git)
# Navigate to the target directory
echo -e "${PURPLE}[Deploy.sh]:${NC} Navigating to target directory: $TARGET_DIR"
cd "$TARGET_DIR" || { echo -e "${PURPLE}[Deploy.sh]:${NC} ${ERROR}Failed to navigate to directory: $TARGET_DIR"; exit 1; }
else
echo -e "${PURPLE}[Deploy.sh]:${NC} Using $TARGET_DIR"
# Navigate to the target directory
echo -e "${PURPLE}[Deploy.sh]:${NC} Navigating to target directory: $TARGET_DIR"
cd "$TARGET_DIR" || { echo -e "${PURPLE}[Deploy.sh]:${NC} ${ERROR}Failed to navigate to directory: $TARGET_DIR"; exit 1; }
if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
# Get the Git repository URL
REPO_URL=$(git remote get-url origin)
else
echo -e "${PURPLE}[Deploy.sh]:${NC} ${WARNING}$TARGET_DIR is not a Git repository.${NC}"
fi
if [ -z "$REPO_URL" ]; then
echo -e "${PURPLE}[Deploy.sh]:${NC} Failed to get remote repository URL. Skipping git pull.."
else
transformRepo "$REPO_URL"
# Stash local changes
if is_stash_required; then
echo -e "${PURPLE}[Deploy.sh]:${NC} Stashing is required before pulling."
echo -e "${PURPLE}[Deploy.sh]:${NC} ${WARNING}Stashing local changes...${NC}"
git stash || { echo -e "${PURPLE}[Deploy.sh]:${NC} ${ERROR}Failed to stash changes"; exit 1; }
else
echo -e "${PURPLE}[Deploy.sh]:${NC} ${SUCCESS}No need to stash local changes.${NC}"
fi
# Pull latest code from git
echo -e "${PURPLE}[Deploy.sh]:${NC} Pulling latest code from git..."
PULL_OUTPUT=$(git pull "$AUTH_REPO_URL" 2>&1)
if echo "$PULL_OUTPUT" | grep -q "Already up to date."; then
echo -e "${PURPLE}[Deploy.sh]:${NC} No updates available. Repository is already up to date."
echo -e "${WARNING}------------Git pull Output------------\n$PULL_OUTPUT\n------------Git pull Output------------${NC}"
if [[ "$FORCED" == false ]]; then
echo -e "${PURPLE}[Deploy.sh]:${NC} ${SUCCESS}Deployment Stopped."
exit 0;
else
echo -e "${PURPLE}[Deploy.sh]:${NC} Redeploying..."
fi
elif echo "$PULL_OUTPUT" | grep -q "Updating [0-9a-f]\{7\}\.\.[0-9a-f]\{7\}"; then
echo -e "${PURPLE}[Deploy.sh]:${NC} Git pull successful."
echo -e "${SUCCESS}------------Git pull Output------------\n$PULL_OUTPUT\n------------Git pull Output------------${NC}"
echo -e "${PURPLE}[Deploy.sh]:${NC} Updated code. Deploying..."
elif [[ $? -ne 0 ]]; then
echo -e "${PURPLE}[Deploy.sh]:${NC} Git pull failed"
echo -e "${ERROR}------------Git pull Output------------\n$PULL_OUTPUT\n------------Git pull Output------------${NC}"
while true; do
read -p "Do you want to proceed with the deployment anyway? (y/n): " choice
case "$choice" in
y|Y ) echo -e "${PURPLE}[Deploy.sh]:${NC} Proceeding with the deployment..."; break;;
n|N ) exit 1;;
* ) echo -e "${PURPLE}[Deploy.sh]:${NC} ${ERROR}Invalid choice. Please enter 'y' or 'n'.${NC}";;
esac
done
else
echo -e "${PURPLE}[Deploy.sh]:${NC} ${SUCCESS}Git pull successful.${NC}"
fi
fi
installPm2
stopPm2
fi
runNpmInstall
# Run Prisma commands if required
if [[ "$PRISMA" == "true" ]]; then
# Check if Prisma is listed in package.json
if grep -q '"@prisma/client":' package.json && npm list @prisma/client &> /dev/null; then
echo -e "${PURPLE}[Deploy.sh]:${NC} Running Prisma commands..."
npx prisma generate || { echo -e "${PURPLE}[Deploy.sh]:${NC} ${ERROR}Prisma generate failed"; exit 1; }
npx prisma db push || { echo -e "${PURPLE}[Deploy.sh]:${NC} ${ERROR}Prisma db push failed"; exit 1; }
else
echo -e "${PURPLE}[Deploy.sh]:${NC} Prisma is not installed or not listed in package.json. Skipping Prisma commands."
fi
fi
# Build the project
if [ ! -f package.json ]; then
echo -e "${PURPLE}[Deploy.sh]:${NC} package.json not found. Skipping build step..."
elif grep -q '"build":' package.json; then
echo -e "${PURPLE}[Deploy.sh]:${NC} Building the project..."
npm run build || { echo -e "${PURPLE}[Deploy.sh]:${NC} ${ERROR}Build failed"; exit 1; }
else
echo -e "${PURPLE}[Deploy.sh]:${NC} Build command not found in package.json. Skipping build step..."
fi
restartPm2
echo -e "${PURPLE}[Deploy.sh]:${NC} ${SUCCESS}Deployment complete!"
// This is a demo ecosystem file that runs "npm run start" when pm2 starts this. Modify it as per need.
// PM2 Doc: https://pm2.io/docs/runtime/reference/ecosystem-file/
module.exports = {
apps: [
{
name: "Awesome API",
script: "npm",
args: "start",
namespace: "backend",
env: {
NODE_ENV: "production",
},
},
],
};
# Run this script in the root directory where you want to clone your projects from git or already cloned project directory exists.
# This will clone the deployment script from the gist and make it executable.
# Export git credentials so that you don't have to pass them as argument everytime.
# Uncomment the fields you want to setup.
# or run this directly from the url and the menualy set the credentials.
# curl -s -L https://gist.githubusercontent.com/FTKhanFT/b87ef1bcfff51814b45815ad708fdcc1/raw/4f47ef626f2865b601cb81d628e4c89f04db1604/setup_script.sh | bash
# Git Username
# export DEPLOY_GIT_USERNAME=
# Git personal access token
# export DEPLOY_GIT_PASSWORD=
GIT_URL=https://gist.github.com/b87ef1bcfff51814b45815ad708fdcc1.git
git clone "$GIT_URL"
TARGET_DIR=$(basename "$GIT_URL" .git)
mv "$TARGET_DIR/deploy.sh" .
rm -rf "$TARGET_DIR"
chmod +x deploy.sh
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment