Deploying with a post-receive Git Hook
Deploying through CI is great, but sometimes you just need to push code to a server with a single git push. The post-receive hook in a bare repository handles this without extra dependencies: push to the server, and the hook automatically checks out files into the working directory.
Schema: bare repo as a deploy trigger
The logic is straightforward:
- A bare repository is created on the server (e.g.,
/srv/deploy/app.git). - The developer adds it as a remote and runs
git push origin main. - Git receives the data and triggers
hooks/post-receive. - The hook script runs
git --work-tree=/var/www/app --git-dir=/srv/deploy/app.git checkout -f main.
A bare repository has no working directory. That is why post-receive must explicitly pass --work-tree so checkout knows where to write files.
This is not CI — it is a direct trigger at the Git level. No pipelines, artifacts, or queues. It fits small services, VPS instances, and internal tools well.
Creating a bare repository on the server
SSH into the server and create the repo:
The resulting structure is standard: hooks/, objects/, refs/, HEAD, config. Default hooks live in /srv/deploy/app.git/hooks/ with a .sample suffix — rename them or create your own.
Make sure /srv/deploy is owned by the user you are pushing as. Otherwise, write permissions on objects/ will be denied.
Writing the post-receive hook
Create /srv/deploy/app.git/hooks/post-receive:
Key elements:
| Element | Purpose |
|---|---|
set -euo pipefail | Script exits on any error; does not continue with bad data |
while read oldrev newrev refname | The hook passes three arguments per line for each updated ref |
refs/heads/$BRANCH | Checks that the pushed ref is the target branch |
checkout -f | Force-synchronizes the working tree with the reference |
If you need to restart a service after deployment, append systemctl restart app or supervisorctl reload app to the end of the script. The hook runs in the context of the Git user, so verify it has permission to restart.
Setting up permissions and access
The most common issue is permissions. The Git user you push as must be able to write to WORK_TREE and execute commands from the hook.
Setup options:
- Single user: Git user and web user are the same identity. Simply
chown -R deploy:deploy /srv/deploy /var/www/app. - Group: Add the Git user to the web server group.
usermod -aG www-data deploy, thenchmod -R g+w /var/www/app. - SSH key: Push over SSH; the key authenticates as the correct user.
Do not forget to make the hook executable:
Pushing from the client and verifying
On the developer machine, add the remote and push:
On the server, the hook log will show:
Verify files on the server:
If the push fails with Permission denied, check the owner of hooks/post-receive and the target directory. If checkout does not write files, confirm that WORK_TREE points to an existing directory and the user has write access to it.
That is it. No additional tools — just Git and shell. For high-load production, this is obviously not a replacement for a proper CI pipeline, but for quick deploys to one or two servers it works reliably without unnecessary complexity.