Budget continuous deployment with git hooks and Docker

I’m too cheap to pay for CI/CD for a small personal project like this one, so I decided to do it myself. The “production” server is in my home, not exposed to the internet, and the only users are my family members.

On the production server I created a bare repository.

mkdir myrepo.git
cd myrepo.git
git init --bare --shared

We don’t care about merge conflicts in a production deployment, so I set

git config receive.denyNonFastForwards false

On the development machine, I added this as a remote and tested pushing with

git remote add production myuser@myhost:myrepo.git
git push production master

I wanted my post-receive hook to be version-controlled, but git doesn’t support tracking files which are actually inside the .git directory. Git also doesn’t play well with symlinks, and it doesn’t preserve permissions when checking files out. This means scripts will need to be called with bash script-name instead of ./script-name.

I created a script in the root of the repository, under version control. I found some info here on how to detect the name of the branch that was pushed.

My script simply checks the files out into a temporary directory, checks that the branch that was pushed was indeed deploy/production and, if so, it calls another script which handles building the new Docker images, running the tests, creating and starting the new containers.

#!/bin/bash
# post-receive.sh

set -euxo pipefail

WORKDIR=~/deploy_temp
ENV=.env.production

echo "Determining branch"
if ! [ -t 0 ]
then
  read -a ref
fi
BRANCH=$(sed 's,refs/heads/,,' <<< "${ref[2]}")

GIT_WORK_TREE="$WORKDIR" git checkout -f "$BRANCH"

if [[ "$BRANCH" == "deploy/production" ]]
then
  pushd "$WORKDIR"
  
  bash ./build_and_run.sh "$ENV"
fi

To make this script run, I created a hook file in the production remote git repository, myrepo.git/hooks/post-receive which simply puts Bash into strict mode and then calls the main script as root. (We need to be root, otherwise we will be denied access to the Docker socket.)

#!/bin/bash
set -euxo pipefail

sudo /bin/bash ../../deploy_temp/post-receive.sh

I made the hook file executable.

chmod +x myrepo.git/hooks/post-receive

Unfortunately, because of how the script works, it has to be present on the remote before the push, if it is to run after the push. This won’t be a problem usually, but when first setting up the hook or making changes to it, I have to copy it over manually before pushing. I do this with

scp post-receive.sh myuser@myhost:deploy_temp/post-receive.sh

However, because the meaty parts of the deployment are handled afterwards, in build_and_run.sh, I can push changes to that file and they will take effect on that push, without needing to do any manual copying. Awesome!

After initially putting the hook in place, I can make a production deployment by running the following command from my development machine.

git push production master:deploy/production -f

Leave a Reply

Your email address will not be published. Required fields are marked *