Using git push remains one of my favorite ways of deploying software. It’s simple, effective, and you can stretch it significantly until you need more complex workflows.
I’m not referring to using git push to trigger a Github action which builds and deploys software. I’m talking about using git push web main to deploy your main branch to a server that you’ve named web.
I learned this workflow from Josef Strzibny’s excellent book Deployment from Scratch, which I’ve adapted somewhat.
This note supposes you have SSH access to a server that has git installed. Let’s assume that said server is already configured as a host in your machine’s SSH configuration file:
Creating a remote repository
I keep an Ansible playbook that automates provisioning this workflow. It should be easy to derive an equivalent bash script if you’re not using Ansible.
The way this works is that you keep a bare git repository in the server where you want to deploy software. A bare repository is a repository that does not have a working directory. It does not push or pull. Anyone with access and permission to the server and the directory where git repository is created will be able to push to it to deploy.
My convention is to create a directory for the project at hand in the /srv/ directory. Inside, I will create two directories: a git directory where the bare repository lives, and a source directory where the source-controlled project files live.
Then, you configure a post-receive script in the git repository hook’s directory. This script will check out the code that was pushed to the main branch into the source directory.
You could do other git operations here like obtaining the current HEAD hash if you’re using that somewhere in your application code.
Finally, you trigger a deployment script that also lives in the project root.
This deployment script, in turn, takes care of whatever is necessary to build and release the pushed code. For example, you could use it generate a new version of a website that uses Hugo:
It’s important to note that the current working directory for that script will be /srv/project/git/hooks, so you may need to change directory or use absolute paths as I showed in the example above.
Another important consideration is that the remote repository gets updated even if post-receive exits because of an error. It is recommended, particularly in the deploy script, to use set -e so that the script stops if any command exits with an non-zero status. If an error ocurrs you’ll know right away because the stdout and stderr of post-receive is piped back to the client that pushed.
I also recommend writing the deploy.sh script such that it is not coupled to a particular push. It should work with what’s in the source directory. In other words, I should be able to use the same script to manually deploy the application if I have a reason to.
Here are a couple of uses for this workflow, some of which I’ve done:
- Build a new binary of a Go program using go build and replace the process
- Build a new image of a Docker container using docker build or docker compose build1 and replace the running containers
- Restart a Node.js server
If you’re using PHP, or serving plain HTML files, you may even get away with not having a deploy.sh script given that the hook updates the source files.
Adding the remote and pushing code
At this point all you need to do is create a new SSH remote for the repository in your machine:
The first web above is the name of the remote, which is arbitrary. The second web matches the name of the host we defined configured in our SSH configuration file.
And finally, you push to it.
Because this workflow is version-controlled, reverting or jumping to a specific version is just another git operation.
I should emphasize that this workflow is pinned to the main branch. Any change that you do must be reflected in that branch in order to get it live. That said, you can push other branches to this remote as it is a regular git repository. However, note the following difference.
Doing git push web my-feature-branch will not deploy a new version with the contents of that branch. The post-receive hook above always checks out the contents of main.
You can either merge my-feature-branch to main in your machine and then push to it, or push directly from the branch to the remote:
You can force push with -f if the remote warns you about discrepancies.