Skip to content

Instantly share code, notes, and snippets.

@sarahhenderson
Last active January 29, 2019 20:11
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save sarahhenderson/f520601f9b47c62602b5 to your computer and use it in GitHub Desktop.
Save sarahhenderson/f520601f9b47c62602b5 to your computer and use it in GitHub Desktop.
Single command deploy for an Angular/Node web application to Ubuntu EC2 instance

Easy deployment with Grunt and Git

Angular/Node.js web application to Ubuntu EC2 instance

Objective

Deploy entire site to server using a single command grunt deploy

Assumptions

  1. Your web application exists on your local machine in a git repo
  2. You can log into your server via ssh without a password
  3. You are using grunt for your build process
  4. Your application is able to run on your server (i.e. node, npm, git and any other necessaries are installed already). I assume you have a working app already, and this gist is just about automating the deployment of it.

This will be true for most people working from bases like MEAN.js, MEAN.io or Yeoman. Information about why I did it this way is at the end.

Process

grunt deploy should:

  1. Build our client code (concat, ngmin, uglify, less etc)
  2. Compile the server code (if necessary) from CoffeeScript to JavaScript
  3. Put all the deployable code (and any other config or resources) in a single folder
  4. Tag that code with the current version number
  5. Push that code to the server

Our server should:

  1. Check out the latest version of the code
  2. Put it in a folder marked with the tag number and date
  3. Run npm install to update any dependencies in our server code
  4. Update a symbolic link so the website now points to our new code
  5. Restart our node process

Server side setup

I have a configuration shortcut set up in my ~/.ssh folder so that I can access my site via ssh with just ssh mysite. The .ssh config specified the .pem file (for AWS) or ssh key (for other hosts) as well as the hostname and the username. This saves me a lot of typing.

The site I am working on is referred to as mysite. The user I am logged in as shows up as ubuntu, as this is the default on Amazon AWS EC2 Ubuntu instances. I keep my web files in my ubuntu user's home directory. You can put them in /var/www or /srv/www or whatever you prefer.

This is my setup:

ssh mysite
mkdir mysite && cd mysite
mkdir site # site files will be stored here
mkdir repo && cd repo
mkdir site.git && cd site.git
git init --bare # bare is important
git symbolic-ref HEAD refs/heads/release # I only push from a branch called master
scp ./post-receive.sh mysite:/home/ubuntu/mysite/repo/site.git/hooks/post-receive
chmod +x hooks/post-receive
#!/bin/bash
read oldrev newrev refname
function exit_with_error() {
echo "MySite ERROR: $1"
exit 1
}
tagline=`dirname $refname`
tag=`basename $refname`
if [ "$tagline" != "refs/tags" ]; then
exit_with_error "Site will only be deployed if you push a tag"
fi
timestamp=`date +"%Y-%m-%d"`
release="/home/ubuntu/mysite/releases/$tag--$timestamp"
echo " "
echo "Deploying mysite version $tag to $release"
mkdir $release || exit_with_error "Unable to create directory $release"
git --work-tree $release --git-dir=/home/ubuntu/mysite/repo/site.git checkout $tag -f
if [ "$?" == "1" ]; then
exit_with_error "Git checkout failed"
fi
echo "Checked out latest files"
cd $release
if npm install --loglevel "error"; then
echo "Updated npm dependencies"
ln -sfn $release /home/ubuntu/mysite/site
echo "Relinked website"
sudo service mysite restart # pre-configured upstart service
echo "Restarted mysite"
#sudo service nginx restart
#echo "Deployment complete"
else
exit_with_error "Unable to update npm dependencies. Check npm_debug.log"
fi

Client side setup

Add a release task to Gruntfile.js that will take your compiled site code and any other deployment files and copy them into release. I used standard grunt tasks for concat, ngmin, uglify, less etc, based on ngBoilerplate and MEAN.js.
One file that is necessary is your package.json file. This must be in your deployment in order to install your node dependencies on the server. My release folder contains package.json and three folders: client, server and conf. conf just has my configurations for nginx, upstart and monit.

Starting in your repo:

mkdir release && cd release
git init
checkout -b release
grunt release
git add --all
git commit -m "Initial commit"
git remote add deploy mysite-aws:/home/ubuntu/mysite/repo/site.git
git push deploy release

Before you next check in your source, you should add the release folder to your .gitignore and make sure you build process will not delete release or the .git folder inside it. This is a one off, but if you create new sites a lot, you could automate this with grunt-shell as well.

For the deployment, I used grunt-bump and grunt-shell. This is my deploy task:

   grunt.registerTask "deploy", [
      	"bump:patch"
   	"build"
   	"compile"
   	"copy:release:
	"shell:deploy"
   ]

bump:patch increments the version number while the other tasks do all the bundling and minification and copies to my release folder. The real magic happens in shell:deploy.

npm install grunt-shell --save-dev
shell:
deploy:
command: [
'cd ./<%= release_dir %>'
'git add --all'
'git commit -m "Release <%= pkg.version %>"'
'git tag "<%= pkg.version %>"'
'git push deploy "<%= pkg.version %>"'
].join(' && ')

End Result

The final result is a single grunt task that will bundle and minify, copy the files to the server, unpack them and update any dependencies, restart the node process and make the new files live on the web server.

A couple of notes:

  • The deploy repo exists only on your deployment machine and the live server. If you wanted to preserve it elsewhere, you could add another remote and push to that as well. Whenever you deploy to the webserver, you should also check in your source and tag it with the current version as well. The grunt-bump task by default actually automatically tags and commits your repo, so you can always checkout that tag and recompile from it if you needed to.
  • Each deploy creates a separate folder on the server containing your site code, and it is labelled with the version and date. The location where your webserver looks for your files is symlinked to the latest version. If you need to rollback, just reset the symlink to the previous version and restart your node process - takes just a second. You can purge old versions periodically if needed.

Background

I read a lot of articles about automating deployment (and tried a number of different ways), but they all didn't work for me for one reason or another.

Simple post-receive hook

There are many tutorials that will show you how to set up a bare repo on the server that you can push your development repo to. Most tutorial authors assume that whatever is checked into git is what you make available on your webserver. The post-receive hook is a simple git checkout, with no build steps. If I pushed my source code to the server, I would have to run my entire build process on the server. This would mean intalling all the grunt devDependencies, running all the concat, ngmin, uglify, less, minify and coffee compilations. That didn't seem like a great idea, since I have all that already set up on my local development environment. My server is supposed to be a production server, not a build server.

Other options involve having a separate build server or continuous integration server that automatically builds pushed source and can then deploy it to the server(s). However, this seemed like overkill for my single-developer small project. I'm just running on the free micro EC2 instance, so I don't want to spin up other servers that will cost me money just to build and deploy.

A number of sites pointed to tools like Capistrano and Fabric for deployment (also Chef and Puppet, although those seem more oriented towards provisioning servers rather than dev deployments). But I didn't really want the spend a lot of time learning a (to me) completely new language just to deploy. I'd rather just suck it up and use FTP in that case, or just enter all the deployment sequence commands manually.

I possibly wouldn't need grunt if my git-fu was better. I came across some talk about having separate branches and subtrees in which you could store compiled code and you could checkout to them and commit from there, but I was never quite able to make it all work. Since I was already using Grunt, this solution works for me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment