Very simple ‘git push’ workflow to deploy code on your own server

Mattias Geniar, Tuesday, December 29, 2015 - last modified: Thursday, May 18, 2017

This technique isn't new. In fact, it's far from it. Around 2013, everyone seemed to be using this (or some variant of it) to deploy simple projects. But since then, it seems to have quiet down a bit. That's a shame, because it's super easy to use.

Here's how it works.

A git-push-to-deploy scenario

Imagine being able to do just the following to push a change to production.


This boils down to just 2 commands to push your change to production (or any release cycle, really):

$ git commit -am "Commit this awesome change"
$ git push live master

This would be unthinkable in large projects (no code review, no approval process, ...), but for simple projects this is absolutely dead easy to use.

Preparations on the server

If you're reading "server" and thinking "wait a minute, I don't have a server", this probably isn't for you. You need a server (or at least: an SSH account, does not have to be root) in order for this to work. Shared hosting etc. probably won't work, unless you're given an SSH account which can execute git binaries.

Assuming you have the following directory structure for your site in your home directory:

$ ls -l

First, create a new directory that will contain your git repository data (the 'raw' data). I'll name it repo.git in this example.

$ mkdir ~/repo.git
$ cd ~/repo.git
$ git init --bare

Your server is now ready to 'host' a git repository: that's the magic behind git init --bare.

As a final touch, you need to add a post-receive hook to that repository. A post-receive hook is a simple bash-script (or any kind of executable, really) that gets called whenever this repository receives data from a git push.

We'll use this to trigger an update of your live site whenever a new change is committed to this repository.

$ vim ~/repo.git/hooks/post-receive

Copy/paste the following content in there.

git --work-tree=~/htdocs --git-dir=~/repo.git checkout -f
git --work-tree=~/htdocs --git-dir=~/repo.git pull
echo "Hooray, the new version is published!"

And make sure the file is executable.

$ chmod +x ~/repo.git/hooks/post-receive

When you first configure this, you'll need to make a first git clone manually to your htdocs directory (if this directory already exists, either move it temporarily (as a back-up) or remove it altogether).

$ git clone ~/repo.git ~/htdocs

Now you're all set to start receiving git pushes and auto-update your site.

Preparations on the client side

On the client-side, you only need to add a new git remote location. This will refer to your newly created repository.

From within the root of your git repository (the top most directory), add your new git remote.

$ git remote add live ssh://your_username@your_hostname.tld/~/repo.git

This will add a new repository to your git configuration named live which points to ssh://your_username@your_hostname.tld/~/repo.git. The endpoint may look cryptic, but it's just your SSH username @ server name or IP / path to your git repo.

If you were to list your git remotes locally, it would look like this (with different URLs, obviously).

$  git remote -v
live	ssh:// (fetch)
live	ssh:// (push)
origin (fetch)
origin (push)

For this example, I abused my string encoder/decoder tool, which you can also find on Github.

Now, whenever you do the following:

$ git commit -am "your commit message"
$ git push live master

... your server will receive the data from the git push and it will trigger its post-receive you configured above, making a checkout in your htdocs folder from the git repository.

Boom, your change is live.

Update: as was pointed out to me by @pjaspers, there's a ruby gem called git-deploy that can help with most of the setup involved.

Hi! My name is Mattias Geniar. I'm a Support Manager at Nucleus Hosting in Belgium, a general web geek & public speaker. Currently working on DNS Spy & Oh Dear!. Follow me on Twitter as @mattiasgeniar.

Share this post

Did you like this post? Will you help me share it on social media? Thanks!


Ton Kersten Tuesday, December 29, 2015 at 23:31 - Reply

Hello Mattias,

Nice article. Could you tell us what prompt you are using? Looks really good.

Michelangelo van Dam Wednesday, December 30, 2015 at 00:40 - Reply


A very nice description of how to automate your deployment workflow, I enjoyed the details you’ve given in your examples as well.

But these days sites are more then just code: they connect with databases, require image optimizations, need minifications of static assets, and so on. Therefore it’s wise to emphasize that these procedures are best used for stand-alone web applications and that these practices fit within a series of steps called a “pipeline”.

I have a presentation ready about this subject and with your permission I would love to give the talk at your office, enriched with the contents of this article (with credits).

    Mattias Geniar Wednesday, December 30, 2015 at 10:38 - Reply

    Hi Mike! :-)

    Therefore it’s wise to emphasize that these procedures are best used for stand-alone web applications and that these practices fit within a series of steps called a “pipeline”.

    You’re absolutely right: this kind of ‘push to deploy’ works best for either static sites or very simple projects, where there isn’t much build involved anyhow.

    However, you could take even this version further: nothing’s preventing you from running additional scripts on the post-receive hook. It can trigger database migrations (and if there aren’t any, it could just skip this step), minify your CSS, restart worker daemons, …

    It has its limitations, so like you mentioned it’s safe to look at a tool which offers you more variety and configuration options if your build process is complex.

    As for your presentation: I would love to see it! Perhaps we can combine it with a PHP Benelux meetup?

Šime Vidas Wednesday, December 30, 2015 at 02:02 - Reply

What if the site’s JS/CSS assets are generated via (gulp) build tasks? Would you check in the generated files (in addition to their sources)?

    Mattias Geniar Wednesday, December 30, 2015 at 10:34 - Reply

    I think this is personal preference, but I don’t mind committing the generated files in my repo. Some like it, some swear against it.

    Alternativally, you could just have the post-receive script run all gulp/grunt/composer/… buildsteps you need, so every time you push to your remote repository those steps are executed automatically (on the server, not on the client).

    However, this easily becomes a scenario where you’re hitting git‘s limitations and you’re better of using an actual build tool like Capistrano, Phing, Make, Deployer, …

Francis Kim Wednesday, December 30, 2015 at 15:19 - Reply

I’ve been looking for a similar solution for a while, I’ll definitely be trying this out at work. Thanks!

Robin Thursday, December 31, 2015 at 00:37 - Reply

Nice write up. We use basically the same method of deploying our dockerized containers to a remote server which receives the code, rebuilds (if need be) the container remotely, runs the composer/npm install et al if need be and restarts the app. And by utilizing the docker infrastructure this can be done without (noticable) downtime :)

If anyone is interested: have a look at the dokku project ( which does all this using (fairly) straight forward bash scripting and git hooks ;)

(but boils down to what you described)


David Bain Saturday, June 18, 2016 at 14:29 - Reply

Just a note: I had to swap out ‘~’ with ‘$HOME’ and then the script worked for me.

git –work-tree=$HOME/htdocs –git-dir=~$HOME/repo.git checkout -f
echo “Hooray, the new version is published!”

Mike Thursday, May 11, 2017 at 03:07 - Reply

I don’t understand why the post-receive hook is doing a git checkout instead of a git pull? A checkout doesn’t get updates where as a pull does. What am I missing? Thanks, Mike.

    Mattias Geniar Thursday, May 18, 2017 at 13:19 - Reply

    I don’t understand why the post-receive hook is doing a git checkout instead of a git pull? A checkout doesn’t get updates where as a pull does. What am I missing? Thanks, Mike.

    Seems something went wrong in the post editing, should now be fixed! The checkout resets the current git structure, so the “git pull” doesn’t give any merge conflicts.

    Post should be updated correctly now. Thx for the heads-up!

Tom Hag Thursday, November 30, 2017 at 20:23 - Reply

Hi this is rather old, but I was trying to do this for a simple git deployment setup, and have followed you guide. Everything seems to work, but somehow the post-receive hook will not fire after pushing a commit to the repo on the server. So the commit is confirmed pushed to the server repo, and so far so good, however the git hook will not fire and I have to manually pull the file from the repo to the www directory. File execution permission is also ok.

Prafulla Kumar Sahu Thursday, September 27, 2018 at 15:53 - Reply

Hello Mattias,

Thank you for such a great tutorial, I was trying to achieve the same and come across your tutorial, while I was able to do most of the things, I am accessing ssh through public key, so when I am trying to push to server, I am getting the following error,.

Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

Can you help me with this?

Gary Cartagena Tuesday, October 23, 2018 at 19:14 - Reply

I have another opinion on the use of ‘pull’ vs ‘checkout’
Doesn’t git pull, perform a a “fetch + merge” scenario? It’s two steps, although it is in the background.

why wouldn’t you use:
git checkout –force [branch, tag or commit]

Then you don’t have to do a pull (AGAIN) after already updating the directory tree


Mark Caggiano Friday, April 19, 2019 at 10:52 - Reply

Hello ! Thanks for this article !
Everything works fine, except it gives me this error:
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 301 bytes | 301.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: There is no tracking information for the current branch.
remote: Please specify which branch you want to merge with.
remote: See git-pull(1) for details.
remote: git pull
remote: If you wish to set tracking information for this branch you can do so with:
remote: git branch –set-upstream-to=/ master
remote: Hooray, the new version is published!
To ssh://
68f155e..6d3b9b3 master -> master
Branch ‘master’ set up to track remote branch ‘master’ from ‘prod’.

How can i fix it ?

Leave a Reply

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