Bash Shell Script for Deploying your Application to the Web

How do you deploy your code? Do you have a script to automate the deployment of your web application?

In this article I will show you how to write a shell script for deployment. I present a bash shell script for deploying to Linux Server. This unix bash shell script can be run on your test, stage or production server to deploy your web application stored in a git repository hosted on github.

Jump here if you want to and skip the explanation.

One step to consider in your setup is how to trigger your script. In this particular implementation I use a github action. Which, in this case, triggers the execution of a bash shell script on the server when ever we make a change to main branch. You can read more about this our github action setup here.

Once you have this sorted out, you can concentrate on the shell script for deployment.

There are actually a few options to choose from and this can vary massively depending on your application stack. There are a few libraries which could help with deployment of various applications. A few such deployment applications are:

  • Robo https://robo.li/getting-started/
  • Deployer https://deployer.org/

Bash Shell Script

In this article, we consider a simple deployment using a bash shell script. I have put together a basic shell script to deploy a basic web application. This script can be used and adapted for any application stack.

The deployment script has two main components. A list of declared functions, and a list of steps to execute these functions. There is also a config file which can contain server specific settings.

I provide an in-depth explanation of each function in the following sections.

Init

The function in the file is the func_init. This function is run to create the output the file logging and also setup parameters from external file.

# Function init()
#  loads config file with specifc env variables
#  env='local', 'test', 'stage', 'prod'
func_init(){    
    configFile='config'
    if [ -f "$configFile" ]; then
        source $configFile;

        #enable logging
        func_createLog;
        echo $ENV

        #Load settings
        env=$ENV;
        gitrepo=$GIT_REPO;
        gitbranch=$GIT_BRANCH
        gitssh=$GIT_SSH

        dir_git=$DIR_GIT
        
        dir_build=$DIR_BUILD
        dir_live=$DIR_LIVE

    else
        echo "Config file not found [$configFile]" >&2
        echo "Exiting!!!" >&2
        exit 1
    fi
}

The output logging file is an import step. The function that created the log file, also allows the output steps of the script to be output to std.out as well as to file. This means we can see the progress of the deployment script on the command line, if we invoke this script manually.

# Function createLog()
#  Create a log for output from this script to file.
func_createLog(){
    #Redirect output to logfile   
    dateNow=$(date '+%Y-%m-%d_%H-%M-%S')
    exec > >(tee -i $DIR_LOG/DeployLog_$dateNow.log)
    exec 2>&1
}

The next important function which is used thought the script is a function to check the exit signal of the previously issued command. This is used when we issue a command that is critical to the deployment, and would be catastrophic if there is a problem, or something goes wrong.

That is why in this automated deployment script we check check the exit signal after each main command. If there is a problem, we exit, and throw an error. This is essentially exiting the script with a signal 1 (exit 1). If there is no problem detected, then we output OK and proceed.

# Function checkCmdStatus()
#  check the status of previously executed command
func_checkCmdStatus(){
    if [ $? -eq 0 ]
    then
        echo "--OK $1"
    else
        echo "Failure: command failed $1" >&2
        echo "Exiting!!!" >&2
        func_pingGithub false;
        exit 1
    fi
}

The first set of output from this script is initiated using the startup function. The output is simple the set of environment and config variables that are going to be used in the remaining of the script. This function is also responsible for setting the start time of the script.

# Function startup()
#  outputs basic system level settings
func_startup(){
    start=`date +%s`
    echo "start  : "$(date)
    echo "env    : "$env
    echo "whoami : "$(whoami)
    echo "pwd    : "$(pwd)
    echo "uptime : "$(uptime -p)
    echo "Settings"
    echo "gitrepo: "$gitrepo
    echo "git    : "$dir_git
    echo "build  : "$dir_build
    echo "live   : "$dir_live
}

Github code

The function called func_updateCode is responsbile for grabbing the latest code from github. it does this by first fetching the lastest update from github for the branch you have set in the config file. This branch is typically main, but could be test, stage or prod based on your setup. The next command update the files and finally we clean up any files that have been left of the file system from previous build, or activities not related to the gitrepo.

# Function updateCode()
#  gets latest version of code from github
func_updateCode(){
    echo "-UpdateCode"
    echo "  repo      = "$gitrepo
    echo "  gitbranch = "$gitbranch
    echo "  gitssh    = "$gitssh
    echo "  dir       = "$dir_git
    cd $dir_git

    GIT_SSH_COMMAND="ssh -i $gitssh" git fetch origin $gitbranch
    func_checkCmdStatus "git fetch origin"

    GIT_SSH_COMMAND="ssh -i $gitssh" git reset --hard origin/$gitbranch
    func_checkCmdStatus "git reset"

    GIT_SSH_COMMAND="ssh -i $gitssh" git clean -fdx
    func_checkCmdStatus "git clean"

    echo "-Done"
}

We also have functions for backing up, and seeding data. These will depend on your application, and what data setup you have.

The update live function essentially copies the code from your local git repo to the live directory.

# Function update Live
#  copy code from build dir to live
func_updateLive(){
    source=$dir_git"/www/"
    destination=$dir_live

    echo "source : "$source
    echo "dest   : "$destination

    rsync -rv --progress --stats \
        $source $destination

    func_checkCmdStatus "rsync"
}

Depending on your setup, you might choose to exclude certain files from the implementation. These are simply added as arguments to the rsync command.

--exclude=dir_git".git" \
--exclude=dir_git".github" \
--exclude=dir_git"deploy" \
--exclude=dir_git"docker" \

Tear down

Once the script has completed, there are a few functions that can be run. The first is the end function which calculates the script runtime. This script will also call the func_pingGithub function. Depending on your implementation, you could optionally chose to ping the github sever with the success (or failure) of your deployment. I use a github action to execute this deployment script in an automated fashion, which means this function is not necessary for this particular configuration.

# Function end()
#  Outputs script execution time
func_end(){
    end=`date +%s`
    runtime=$((end-start))
    hours=$((runtime / 3600)); 
    minutes=$(( (runtime % 3600) / 60 )); 
    seconds=$(( (runtime % 3600) % 60 ));
    echo "Runtime: $hours:$minutes:$seconds (hh:mm:ss)"
    func_pingGithub true;
}
# Function ping github
#  ping github with success/failed deployment
func_pingGithub(){
    if [ "$1" = true ] ; then
        echo "deployment success";
    elif [ "$1" = false ] ; then
        echo "deployment failed";
    else
        echo "deployment failed";
    fi
}

Main

All this is put together and called in the order as defined below. We start with our initial setup func_init;. We then output our start up variables and start our script timer func_startup;. Once we have completed these first steps, it is now time to grab the latest version of our code from github func_updateCode;. We can then issue and application specific build commands func_goLive;, and then run the go live function. The go live function firsts enables maintenance mode, then copies our code to the live directory, and refresh cache, and then bring the site back up to life. The script then ends with outputting the execution time func_end;.

# Main
func_init;
func_startup;
func_updateCode;
func_build;
func_goLive;
func_end;

Full Deployment Bash Script

Here is the complete shell script with all components as explained above.

#!/bin/bash
# 
# deploy.sh
# @version 0.1
#
# Running instructions
# > ./deploy.sh
#


# Function init()
#  loads config file with specifc env variables
#  env='local', 'test', 'stage', 'prod'
func_init(){    
    configFile='config'
    if [ -f "$configFile" ]; then
        source $configFile;

        #enable logging
        func_createLog;
        echo $ENV

        #Load settings
        env=$ENV;
        gitrepo=$GIT_REPO;
        gitbranch=$GIT_BRANCH
        gitssh=$GIT_SSH

        dir_git=$DIR_GIT
        
        dir_build=$DIR_BUILD
        dir_live=$DIR_LIVE

    else
        echo "Config file not found [$configFile]" >&2
        echo "Exiting!!!" >&2
        exit 1
    fi
}

# Function createLog()
#  Create a log for output from this script to file.
func_createLog(){
    #Redirect output to logfile   
    dateNow=$(date '+%Y-%m-%d_%H-%M-%S')
    exec > >(tee -i $DIR_LOG/DeployLog_$dateNow.log)
    exec 2>&1
}

# Function checkCmdStatus()
#  check the status of previously executed command
#    [ $? -eq 0 ] && echo "Command was successful" || echo "FAILED!!!"
func_checkCmdStatus(){
    if [ $? -eq 0 ]
    then
        echo "--OK $1"
    else
        echo "Failure: command failed $1" >&2
        echo "Exiting!!!" >&2
        func_pingGithub false;
        exit 1
    fi    
}


# Function startup()
#  outputs basic system level settings
func_startup(){
    start=`date +%s`
    echo "start  : "$(date)
    echo "env    : "$env
    echo "whoami : "$(whoami)
    echo "pwd    : "$(pwd)
    echo "uptime : "$(uptime -p)
    echo "Settings"
    echo "gitrepo: "$gitrepo  
    echo "git    : "$dir_git  
    echo "build  : "$dir_build
    echo "live   : "$dir_live
}

# Function end()
#  Outputs script execution time
func_end(){
    end=`date +%s`
    runtime=$((end-start))
    hours=$((runtime / 3600)); 
    minutes=$(( (runtime % 3600) / 60 )); 
    seconds=$(( (runtime % 3600) % 60 ));
    echo "Runtime: $hours:$minutes:$seconds (hh:mm:ss)"
    func_pingGithub true;
}

# Function ping github
#  ping github with success/failed deployment
func_pingGithub(){
    if [ "$1" = true ] ; then
        echo "deployment success";
    elif [ "$1" = false ] ; then
        echo "deployment failed";
    else    
        echo "deployment failed";
    fi
}

# Function updateCode()
#  gets latest version of code from github
func_updateCode(){
    echo "-UpdateCode"
    echo "  repo      = "$gitrepo
    echo "  gitbranch = "$gitbranch  
    echo "  gitssh    = "$gitssh     
    echo "  dir       = "$dir_git
    cd $dir_git

    GIT_SSH_COMMAND="ssh -i $gitssh" git fetch origin $gitbranch
    func_checkCmdStatus "git fetch origin"

    GIT_SSH_COMMAND="ssh -i $gitssh" git reset --hard origin/$gitbranch
    func_checkCmdStatus "git reset"

    GIT_SSH_COMMAND="ssh -i $gitssh" git clean -fdx
    func_checkCmdStatus "git clean"

    echo "-Done"
}

# Function build()
#  executes application build process.
func_build(){
    echo "-Build"
    cd $dir_build
    composer install
    func_checkCmdStatus "composer install"   
    echo "-Done"
}

# Function enable/disable maintenance mode
func_maintenanceMode(){
    if [ "$1" = true ] ; then
        echo "enable maintenance mode";
    elif [ "$1" = false ] ; then
        echo "disable maintenance mode";
    else    
        echo "maintenance mode option not recognised";
    fi
}

# Function back up live
#  used for roll back in the event of failure
func_backupLive(){
    echo "do nothing";
}

# Function seed data
#  populate database with seed data.
func_dataSeed(){
    echo "do nothing";
}

# Function restore data
#  
func_dataRestore(){
    echo "do nothing";
}

# Function clearCache
func_clearCache(){
    echo "-clearCache"
    cd $dir_live    
    composer dump-autoload    
    func_checkCmdStatus "composer dump-autoload" 
    echo "-Done"    
}

# Function update Live
#  copy code from build dir to live
func_updateLive(){
    source=$dir_git"/www/"
    destination=$dir_live

    echo "source : "$source
    echo "dest   : "$destination

    rsync -rv --progress --stats \
        $source $destination

    func_checkCmdStatus "rsync"
}

# Function goLive()
#  copy code to live directory
func_goLive(){
    echo "-GoLive"

    if [ $env = "local" ]; then
        echo "-local"
        func_maintenanceMode true;        
        #func_updateLive;
        func_dataSeed;        
        func_maintenanceMode false;          
    elif [ $env = "test" ]; then
        echo "-test"
        func_maintenanceMode true;
        func_dataSeed;
        func_updateLive;
        func_clearCache;
        func_maintenanceMode false;         
    elif [ $env = "stage" ]; then
        echo "-stage"  
        func_maintenanceMode true;
        func_dataRestore;
        func_updateLive;
        func_maintenanceMode false;         
    elif [ $env = "prod" ]; then
        echo "-prod"
        func_maintenanceMode true;
        func_backupLive;
        func_updateLive;
        func_maintenanceMode false;         
    else
        echo "Failure: ENV not set" >&2
        echo "Exiting!!!" >&2
        exit 1
    fi

    echo "-Done"
}


#
# Main
func_init;
func_startup;
func_updateCode;
func_build;
func_goLive;
func_end;

Here is the config file with some example inputs

ENV='local'
DIR_LOG="/deploy"
GIT_REPO="git@github.com:USERNAME/REPO.git"
GIT_BRANCH="svr-local"
GIT_SSH="/home/username/.ssh/mdmt-gh-ro"
DIR_GIT="/home/username/gitrepo/repo"
DIR_BUILD="/home/username/gitrepo/repo/www"
DIR_LIVE="/var/www/html"

How do I invoke this bash shell script?

You could trigger this script manually, use web or command line ping.

In my particular implementation, I use a github aciton to trigger this script. The github action I use is as follows: (you can read more about the github action setup here).

name: deploy svr-test

on:
  push:
    branches:
      - svr-test


jobs:

  # Test
	
  # Build

  # Deploy
  deploy:
    runs-on: ubuntu-18.04 
    steps:
    - name: Install SSH Key
      uses: shimataro/ssh-key-action@v2.1.0
      with:
        key: ${ { secrets.SSH_KEY_SVRTEST } }
        known_hosts: ${ { secrets.KNOWN_HOSTS_SVRTEST } }
    - name: Execute test Script
      run: ssh username@server.com "cd $-Deploy && pwd && ./deploy.sh"

You can read more about how I have used devops in my previous projects.

If you have any questions or comments you can contact me.