The more streamlined our processes, the more productive we can be. Continuous deployment is about committing small ongoing improvements to a production server in a time saving, efficient way.
Continuous deployment is especially relevant to team collaboration projects. Instead of working on separate forks of the code and spending extra time, money, and effort on integration, everyone works on the same code base so integration is seamless.
In this recipe, we'll create a deployment flow using Git as a version control tool. While this may not be Node, it can certainly boost productivity for coding, deploying, and managing Node projects.
If we're a little unfamiliar with Git, we can gain insight from Github's help documents, http://help.github.com.
We'll need Git installed on both our server and desktop systems, instructions for different systems can be found here http://book.git-scm.com/2_installing_git.html. If we're using Linux with the apt-get
package manager we can do:
sudo apt-get install git git-core
If we are installing Git for the first time, we'll have to set the personal information configuration settings as follows:
git config --global user.name "Dave Clements"
git config --global user.email "[email protected]"
We'll be using our login
app, which we deployed to our server in the first recipe. So let's SSH into our server and enter the /var/www/login
directory.
ssh -l root nodecookbook.com -t "cd /var/www/login; bash"
Since we'll not be running our app as root, we'll keep things simple and change the listening port in login/app.js
to 8000:
app.listen(8000);
Once we've logged in to our server and installed Git (see Getting ready) in the login
folder, we say the following:
git init
git add *
git commit -m "initial commit"
Next, we create a bare repository (it has a record of all the changes but no actual working files) which we'll be pushing changes to. This helps to keep things consistent.
We'll call the bare repository repo
, because this is the repository we'll be pushing our changes to and we'll create it within the login
folder:
mkdir repo
echo repo > .gitignore
cd repo
git --bare init
Next. we hook up our bare repo
to the login
app repository, and push all the commits to repo
.
cd ..
git remote add repo ./repo
git push repo master
Now we'll write a Git hook that instructs the login
repository to pull any changes from the bare repo
repository, then restarts our login
app whenever repo
is updated via a remote Git push.
cd repo/hooks
touch post-update
chmod +x post-update
nano post-update
With the file open in nano
, we write the following code:
#!/bin/sh cd /root/login env -i git pull repo master exec forever restart /root/login/app.js
Saving our hook with Ctrl + O, then exit with Ctrl + X.
If we ever make Git commits to the login
repository, the two repositories could go out of sync. To fix this, we create another hook for the login
repository:
#!/bin/sh git push repo
We store this in login/.git/hooks/post-commit
, ensuring it has been made executable using chmod +x post-commit
.
We'll be making commits to the repo
remotely via the SSH protocol. Ideally, we want to create a system user just for Git interactions.
useradd git
passwd git #set a password
mkdir /home/git
chown git /home/git
We've also created home directories for the git
user to make it easy for forever
to store logs and PID files. We'll need to make git
the owner of the login
app, allowing us to manage it using Git through SSH:
cd /var/www
chown -R git login
Finally (for the server-side setup), we log in as the git
user and start our app using forever
.
su git
forever start /var/www/login/app.js
Assuming our server is hosted at nodecookbook.com
, we could now access the login
app at http://nodecookbook.com:8000
.
Back on our desktop, we clone the repo
repository:
git clone ssh://[email protected]/var/www/login/repo
This will give us a repo
directory, containing all the generated files perfectly matching our original login
folder. We can then enter the repo
folder and make a change to our code (say, altering the port in app.js)
.
app.listen(9000);
Then we commit the change and push to our server.
git commit -a -m "changed port"
git push
On the server side, our app should have automatically restarted, resulting in our app now being hosted from http://nodecookbook.com:9000
instead of http://nodecookbook.com:8000
.
We created two Git repositories. The first is the login
app itself. When we ran git init
, a .git
directory was added to the login
folder. git add *
adds all of the files in the folder and commit -m "initial commit"
plants our additions into Git's version control system. So now our entire code base is recognized by Git.
The second is repo
, which is a created with the --bare
flag. This is a sort of skeleton repository providing all of the expected Git functionality, but lacking the actual files (it has no working tree).
While it may seem overly complex to use two repositories, it actually simplifies things greatly. Since Git does not allow pushes to a branch that is currently checked in, we would have to create a separate dummy branch so we can checkout of the master and into the dummy branch. This creates problems with the Git hooks and restarting our app. The hooks try to start the app for the wrong branch. The branches can also quickly become out of sync, and the hooks only add fuel to the fire.
As repo
is within the login
directory, we create a .gitignore
file telling Git to disregard this subdirectory. Even though login
and repo
are on the same server, we add repo
as a remote
repository. This puts some necessary distance between the repositories and allows us to later use our first Git hook to cause login
to pull changes from repo
. A Git push from repo
to login
wouldn't cause login
to update its working directory, whereas pulling from repo
into login
does initiate a merge.
After our remote add
, we perform an initial push from the master branch (login) to repo
, now they're singing off the same hymn sheet.
Then we created our hooks.
Git hooks are executable files which reside in the repository's hook
folder. There are a variety of available hooks (already in the folder, but suffixed with .sample)
. We used two: post-update
and post-commit
. One executes after an update (for example, once changes have been pulled and integrated into repo)
, and one after a commit.
The first hook, login/repo/hooks/post-update
, essentially provides our continuous deployment functionality. It changes its working directory from repo
to login
using cd
, and commands a git pull
. The git pull
command is prefixed with env -i
. This prevents problems with certain Git functionality that would otherwise execute the Git commands on behalf of repo
no matter what directory we sent our hook script to. Git utilizes a $GIT_DIR
environment variable to lock us in to the repository that the hook is called from. env -i
deals with this by telling git pull
to ignore (-i
) all environment variables.
Having updated the working directory, our hook then goes on to call forever restart
, thus causing our app to reinitialize with the committed changes in place.
Our second hook is little more than a polyfill to ensure code base consistency in the event that commits are made directly to the login
repository. Making commits directly to the login
directory won't update the working tree, nor will it cause our app to restart but the code between login
and repo
will at least maintain synchronicity.
For the sake of damage limitation (if we were ever compromised), we create a specific account for handling Git updates over SSH, giving it a home directory, taking ownership of the login
app and executing the primary initialization of our app.
Once the server is configured it's plain sailing. After cloning the repo
repository to our local development environment, we simply make a change, add and commit that change, then push to the server.
The server receives our push request, updates repo
, initiates the post-update
hook which makes login
pull the changes from repo
, after which the post-update
hook uses forever
to restart app.js
, and thus we have a continuous deployment work flow.
We can potentially have as many clones from as many locations as we like, so this method lends itself well to geographically-independent team collaboration projects both large and small.
We could avoid uploading modules by using npm install
in the post-update hook. Also, Git hooks don't have to be written in shell script, we can write them in Node!
Some Node modules are written in pure JavaScript, others have C or C++ bindings. Those with C or C++ bindings have to be built from source — a task which is system specific. Unless our live server environment is identical to our development environment, we shouldn't simply push code build for one system onto another.
Further, to save on transfer bandwidth and have faster synchronizations, we could have our Git hooks install all modules (native bindings and JavaScript) and have Git ignore the node_modules
folder entirely.
So in our local repository, let's do the following:
echo node_modules >> .gitignore
Then we'll change the post-update
hook in our bare remote repository (login/repo/hooks) to:
#!/bin/sh
cd /root/login
env -i git pull repo master && npm rebuild && npm install
exec forever restart /root/login/app.js
We've added&& npm rebuild && npm install
to the git pull
line (using&&
to ensure they benefit from the env -i
command).
Now if we added a module to package.json
, and did a git commit -a
followed by git push
, our local repo
would push the package.json
to the remote repo
. This would trigger the post-update
hook to pull changes into the main login
repository, and follow this up with an npm rebuild
(to rebuild any C / C++ dependencies) and an npm install
(to install any new modules).
Continuous deployment is an extension of continuous integration which generally carries the expectation that a thorough test suite is run against any code changes for quality assurance.
Our login
app (being a basic demonstration site) doesn't have a test suite (for info on test suites, seeChapter 9, Writing Your Own Node Modules), but we can still write a hook that executes any tests that could be added to login
in the future.
What's more, we can write it in Node, which has the added bonus of functioning cross platform (on Windows, for example).
#!/usr/bin/env node var npm = require("npm"); npm.load(function (err) { if (err) { throw err; } npm.commands.test(function (err) { if (err) { process.exit(1); } }); });
We would place this code on the server into login/repo/hooks/pre-commit
and make it executable (chmod +x pre-commit)
.
The first line sets node
as the scripts interpreter directive (much as #!/bin/sh
sets the sh
shell for shell scripts). Now we're in Node country.
We use npm
programmability, to load the package.json
file for our app, and then run the test script (if any is specified).
We then add the following to our package.json
file:
{
"name": "application-name"
, "version": "0.0.1"
, "private": true
, "dependencies": {
"express": "2.5.5"
, "jade": ">= 0.0.1"
},
"scripts": {
"test": "node test"
},
"devDependencies": {"npm": "1.1.18"}
}
Then do the following:
npm -d install
Now whenever we push to repo
, any changes will only be committed if they pass the tests. As long as we have a well-written test suite, this is a great way to maintain good code.
For our scripts.test
property, we used node test
(as In Chapter 9, Writing Your Own Node Modules). However, there are more advanced test frameworks available to us, such as Mocha http://visionmedia.github.com/mocha/.
This Node Git hook is adapted (with permission) from a gist by Domenic Denicola, which can be found at https://gist.github.com/2238951.
18.220.160.216