An uncomplicated deploy script that doesn’t use Git, but still keeps your upstream Git repo up-to-date with the live server.
I’ve been using this simple script to deploy a few side projects for a couple of years now. There are brilliant services like Vercel or Netlify that let you deploy static sites super easily. But this one is a 100% DIY solution, which assumes you have a VPS (like DigitalOcean etc.) with an ssh access.
Requirements for the "simple" deploy script
Basically a dumb script that deploys to a simple server, in my case it only
assumes that
rsync
works to the target machine. If you deploy to an AWS bucket, you can change the
upload step on the script.
No Git: the actual transporting of the files doesn’t happen via Git. This way Git is not needed to be installed onto the server, nor have any hooks listening. Which enables to roll with a really simple, perhaps rootless server setup, etc.
Build locally: if the build step happens locally, it also doesn’t expect anything from the server.
Keep Git and the server in sync: the problem with having the build step run locally, is that the deploy script can be run before the new code has been pushed upstream (Github, GitLab...), and then your server isn’t up-to-date with the remote repo. But this can be solved with a bit of locally run Git smartness.
The script
I’ll divide the explanation into 5 parts (or jump to the full script):
- Setup
- Tools
- Handle Git
- Transport the files
- Run the functions
Setting things up
#!/usr/bin/env bash
set -o errexit
set -o pipefail
set -o nounset
# set -o xtrace
readonly PROJECT_NAME='example'
readonly TARGET_PATH='/var/www/example.com/public_html'
readonly SOURCE_PATH='public/'
readonly SITE_URL='https://example.com'
readonly SERVER="${1:-}"First, it sets on the
unofficial Bash strict mode,
in this case written in longhand syntax. It could also be written as
set -euo pipefail. It tells Bash to: 1) stop executing the script if an error
happens, 2) Error and stop the script if an undefined variable is being
called, 3) prevent errors in a pipeline from being masked.
Second: configure the deployment script, paths and names.
Tools
These totally optional helper functions are only for printing different colored messages to the terminal as you deploy. More on the colors in Bash.
green() {
echo "\\033[32m${1}\\033[m"
}
red() {
echo "\\033[31m${1}\\033[m"
}
yellow() {
echo "\\033[33m${1}\\033[m"
}Handle Git
This is the most interesting bit of the code I think. It uses diffing and other Git wizardry to figure out if the local Git tree is clean and up-to-date with the upstream one.
die_if_wrong_project() {
local CURRENT_PROJECT=$(basename "$(git rev-parse --show-toplevel)")
if [ "$CURRENT_PROJECT" != "$PROJECT_NAME" ]; then
red "\\n😱 wrong project, this script is configured to be used in a project named ${PROJECT_NAME}\\n"
exit 1
fi
}
die_if_dirty() {
local CHANGED_FILES=$(git diff-index --name-only HEAD --)
if [ "$CHANGED_FILES" != "" ]; then
red "\\n😱 the repository is not clean:\\n ${CHANGED_FILES}\\n"
exit 1
fi
}
die_if_untracked_files() {
local UNTRACKED=$(git ls-files --exclude-standard --others)
if [ "$UNTRACKED" != "" ]; then
red "\\n😱 there are untracked files:\\n ${UNTRACKED}\\n"
exit 1
fi
}
die_if_ahead_of_remote() {
local UPSTREAM="@{u}"
local LOCAL=$(git rev-parse @)
local REMOTE=$(git rev-parse "$UPSTREAM")
if [ "$LOCAL" != "$REMOTE" ]; then
red "\\n😱 the local branch is not up to date with the remote, please do a git push\\n"
exit 1
fi
}Explanations:
die_if_wrong_project()This is just a precaution, since we’re doing some
rmstuff, better check if this is the right Git project so it doesn’t reek havoc if run in a wrong directory.die_if_dirty()This function will exit with the script if the Git tree is dirty, i.e.: there are uncommitted changes.
die_if_untracked_files()This function will exit the script if there are untracked files loitering around.
die_if_ahead_of_remote()Exits the script if you forgot to push changes to the upstream branch.
Move the files
The function that physically moves the files over to the server using rsync:
move_files_to_server() {
yellow "File transfer started, might take a moment."
rsync -az --info=progress2 --exclude=".*/" --chmod=+rwx --delete "${SOURCE_PATH}" "${SERVER}":"${TARGET_PATH}"
green "Files moved to ${SERVER}:${TARGET_PATH}."
}Here’s what the rsync does:
-a,--archiveThis is equivalent to
-rlptgoD. It is a quick way of saying you want recursion and want to preserve almost everything (with -H being a notable omission).-z,--compressWith this option,
rsynccompresses the file data as it is sent to the destination machine, which reduces the amount of data being transmitted.--info=progress2This option outputs statistics based on the whole transfer, rather than individual files. Note that this only applies to rsync versions 3.0 or newer.
See here how to update rsync
if you’re running an old version.
--exclude=".*/"This excludes dotfiles, for example
.env, which is super important!--chmod=+rwx- Make the transferred files readable.
--deleteThis tells rsync to delete extraneous files from the receiving side (ones that aren’t on the sending side), but only for the directories that are being synchronized. Meaning: it will keep the directories synchronized.
Run the functions and build
Then lastly, run all the functions and the build step. In this case I have the build script as an npm script. First run the sanity check, then build, then move the files:
yellow "Started the deploy process, may take a moment..."
die_if_wrong_project
die_if_dirty
die_if_untracked_files
die_if_ahead_of_remote
npm run build
move_files_to_server
green "Deployd to: ${SITE_URL}. Have a nice day!"The whole deploy script
Below is the wle script. You need to of course configure it to fit your project.
Run it like so:
$ sh deploy.sh [email protected]Or put it in your projects package.json along with the other scripts your
project needs:
{
"scripts": {
"deploy": "sh bin/deploy.sh [email protected]",
"develop": "gatsby develop",
"clean": "gatsby clean",
"build": "gatsby build",
"serve": "gatsby serve"
}
}The whole deploy.js script:
#!/usr/bin/env bash
set -o errexit
set -o pipefail
set -o nounset
# set -o xtrace
readonly PROJECT_NAME='example'
readonly TARGET_PATH='/var/www/example.com/public_html'
readonly SOURCE_PATH='public/'
readonly SITE_URL='https://example.com'
readonly SERVER="${1:-}"
green() {
echo "\\033[32m${1}\\033[m"
}
red() {
echo "\\033[31m${1}\\033[m"
}
yellow() {
echo "\\033[33m${1}\\033[m"
}
# Since we're doing some rm stuff, better check if this is the right Git project
# so it don't reek havoc if run in a wrong directory
die_if_wrong_project() {
local CURRENT_PROJECT=$(basename "$(git rev-parse --show-toplevel)")
if [ "$CURRENT_PROJECT" != "$PROJECT_NAME" ]; then
red "\\n😱 wrong project, this script is configured to be used in a project named ${PROJECT_NAME}\\n"
exit 1
fi
}
die_if_dirty() {
local CHANGED_FILES=$(git diff-index --name-only HEAD --)
if [ "$CHANGED_FILES" != "" ]; then
red "\\n😱 the repository is not clean:\\n ${CHANGED_FILES}\\n"
exit 1
fi
}
die_if_untracked_files() {
local UNTRACKED=$(git ls-files --exclude-standard --others)
if [ "$UNTRACKED" != "" ]; then
red "\\n😱 there are untracked files:\\n ${UNTRACKED}\\n"
exit 1
fi
}
die_if_ahead_of_remote() {
local UPSTREAM="@{u}"
local LOCAL=$(git rev-parse @)
local REMOTE=$(git rev-parse "$UPSTREAM")
if [ "$LOCAL" != "$REMOTE" ]; then
red "\\n😱 the local branch is not up to date with the remote, please do a git push\\n"
exit 1
fi
}
move_files_to_server() {
yellow "File transfer started, might take a moment."
rsync -az --info=progress2 --exclude=".*/" --chmod=+rwx --delete "${SOURCE_PATH}" "${SERVER}":"${TARGET_PATH}"
green "Files moved to ${SERVER}:${TARGET_PATH}."
}
yellow "Started the deploy process, may take a moment..."
die_if_wrong_project
die_if_dirty
die_if_untracked_files
die_if_ahead_of_remote
npm run build
move_files_to_server
green "Deployd to: ${SITE_URL}. Have a nice day!"Possible improvements
The script could take a version number as the second arg, and then create a tag based on that, so rollbacks would be possible by checking out the previous tag.
It could have a --force option that ignores the Git stuff.
The script could also be configured with args or a config file, it could read in
package.json etc.