Simple deployment strategy using only git + ssh + vpn

Claudio Cicali
4 min readMay 10, 2016


This is a strategy I use to deploy an application of mine in the following context:

  • There is a staging environment and a production environment
  • The environments are set up on two different servers
  • The servers reside in a VPC, hence their consoles are only accessible via a VPN
  • There is a build step which will be run on the servers before the actual deployment will take place

The idea is to create a repository on each server, set them as two new `remotes` and then just push to them to deploy, using the `post-receive` git hook which is run after the push has terminated.

Setting up users (remote)

For the sake of simplicity, we’ll consider using a `deployer` user with sudo powers as the owner of the repository and the deployed directory.

Setting up the repositories (remote)

I decided to put my remote repositories as subdirectories of `/var/git`.

cd /var/git
sudo mkdir MyProject
sudo chown deployer: MyProject
cd MyProject
git init --bare

A “bare” repository is a repository containing only the git metadata and not the actual files. The actual files will be downloaded by the post-receive hook and put in the directory we want to deploy from (or to serve directly from, if you don’t have a build step).

As it is, the repository is already ready to `receive` pushes. Now we need to decide what to do after we’ve received data from a push (the `post-receive` hook).

If you enter the `hooks` directory in the repository, you’ll see that there’re already some of them present. Not the `post-receive` though, which we need to create. Something like this is already a good start:

#!/usr/bin/env bashgit --work-tree=/tmp/MyProject --git-dir=/var/git/MyProject checkout -f

Once the repository is refreshed by a push, the script will use git itself to checkout the current branch (probably `master`) in /tmp.

Setting up the development environment (local)

It’s now to send our project files to the new remote. This is just a matter of entering your local git repository for the project and running something like:

cd ~/Workspace/MyProject
git remote add staging ssh://deployer@staging-server/var/git/MyProject
git remote add production ssh://deployer@production-server/var/git/MyProject

As mentioned before I am behind a VPN, hence I need to use a private IP to access the server (set in /etc/hosts with the name of `staging-server`, in the example).

The problem at this point is that I also need to specify a particular ssh private key for each one of the servers, and this is not possible directly from the git command line. There are 2 or 3 workarounds which involve some creative usage of the git environment variables, but I definitely suggest to use the ssh config file itself.

cd ~/.ssh
vi config

The content of the file can be something like this:

Host staging-server
HostName staging-server
Identityfile /home/claudioc/.ssh/staging.pem
Host production-server
HostName production-server
IdentityFile /home/claudioc/.ssh/production.pem

Note the the `host` key can be anything you like while the `hostName` key must resolve to an IP address.

Now issuing any git command with the ssh:// schema will use the correct key selecting it by the name of the server you want to connect to. Sweet.

You can now try to push to our remote server (not Github!) and see what happens.

git push staging master

Take a look into your server now: the directory `/tmp/MyProject` should now contain all the files of the repository.

From this point on, what to do with those files is up to you.

This is what I do, for example:

  • once the files are in checked out, I run `npm install` and `npm prune`
  • once the npm install is finished, I run my tests with `npm test`
  • if all the test pass, I run `gulp build`
  • if the build is successful, I will `rsync` the `/dist` directory in the directory which is served by the web server

Bonus (bullet) points

  • stdout output from the `post-receive` hook is sent back to the console where the “git push” has been started from. This means that you are able to see what’s going on during the deployment (and use `echo` statements to make it even more verbose)
  • the user with which the `post-receive` script is run is the owner of the script itself. Keep this in mind when you are setting up the directories and which commands to run from the script
  • to be able to run the post-receive script even if there is nothing to push, an interesting option is to delete the master branch on the server (from inside the post-receive script itself: `rm -rf /var/git/MyProject/refs/heads/master` (it will add more transfer time, of course)
  • in this simple setup there isn’t a robust and automated rollback strategy. My suggestion is to always tag your repo before pushing. If you find problems with the current version, “rolling back” to the previous tag would be just a matter of:
cd ~/Workspace/MyProject
git reset MyPreviousTag --hard
git push -f staging master

And then, after the deploy:

git pull origin master

Repeat everything for “production” where you read “staging” and you’ll be set.

Further readings



Claudio Cicali

My specialties are software engineering, fintech and pointless random rants. I live in Berlin.