Simple deployment strategy using only git + ssh + vpn

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.

Update from the future self: this is obviously quite an old setup which I only leave here for the sake of archival purposes.

This is a strategy I have used 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

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 bash

git --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 time to try sending 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

How To Set Up Automatic Deployment with Git with a VPS | DigitalOcean

Setting up Push-to-Deploy with git

Written on May 10, 2016 by Claudio Cicali.

Originally published on Medium