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 just see the script 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.