diff --git a/index.js b/index.js
index 3a3d3a4989..ba3a23f38f 100644
--- a/index.js
+++ b/index.js
@@ -43,10 +43,7 @@ var start = function (callback) {
       freeport(function(err, mongoPort) {
         console.log('MongoDB: ' + mongoPort);
         console.log('webPort: ' + webPort);
-        child_process.exec('kill $(ps aux -e | grep PURPOSE=KITEMATIC | awk \'{print $2}\')', function (error, stdout, stderr) {
-          console.log(error);
-          console.log(stdout);
-          console.log(stderr);
+        child_process.exec('kill $(ps aux -e | grep PURPOSE=KITEMATIC | awk \'{print $2}\') && rm ' + path.join(dataPath, 'mongod.lock'), function (error, stdout, stderr) {
           var mongoChild = child_process.spawn(path.join(process.cwd(), 'resources', 'mongod'), ['--bind_ip', '127.0.0.1', '--dbpath', dataPath, '--port', mongoPort, '--unixSocketPrefix', dataPath], {
             env: {
               PURPOSE: 'KITEMATIC'
@@ -54,7 +51,12 @@ var start = function (callback) {
           });
           var started = false;
           mongoChild.stdout.setEncoding('utf8');
+          mongoChild.stderr.setEncoding('utf8');
+          mongoChild.stderr.on('data', function (data) {
+            console.log(data);
+          });
           mongoChild.stdout.on('data', function (data) {
+            console.log(data);
             if (data.indexOf('waiting for connections on port ' + mongoPort)) {
               if (!started) {
                 started = true;
diff --git a/meteor/.jshintrc b/meteor/.jshintrc
index e2c8f682e4..ab5b799ac1 100755
--- a/meteor/.jshintrc
+++ b/meteor/.jshintrc
@@ -128,7 +128,7 @@
       // Packages
       "Fiber": true,
       "moment": true,
-      "Docker": true,
+      "Dockerode": true,
       "byline": true,
       "fs": true,
       "zlib": true,
@@ -142,6 +142,9 @@
       "chokidar": true,
       "docker": true,
       "async": true,
+      "child_process": true,
+      "convert": true,
+      "Convert": true,
 
       // Collections
       "SimpleSchema": false,
@@ -161,8 +164,13 @@
       "SetupController": true,
 
       // Server and Client
+      "Docker": true,
+      "Util": true,
+      "Boot2Docker": true,
+      "Installer": true,
+      "VirtualBox": true,
+      
       "boot2dockerexec": true,
-      "getBinDir": true,
       "getBoot2DockerIp": true,
       "getBoot2DockerState": true,
       "getBoot2DockerDiskUsage": true,
@@ -170,7 +178,6 @@
       "getBoot2DockerInfo": true,
       "boot2DockerVMExists": true,
       "eraseBoot2DockerVMFiles": true,
-      "getHomePath": true,
       "initBoot2Docker": true,
       "isVirtualBoxInstalled": true,
       "upgradeBoot2Docker": true,
@@ -191,46 +198,17 @@
       "fixInterval": true,
       "stopFixInterval": true,
       "runSetup": true,
-      "removeBindFolder": true,
       "removeAppWatcher": true,
       "addAppWatcher": true,
       "resolveWatchers": true,
-      "recoverApps": true,
-      "restartApp": true,
-      "deleteApp": true,
-      "deleteFolder": true,
-      "loadKiteVolumes": true,
-      "getAppLogs": true,
-      "hasDockerfile": true,
-      "runContainer": true,
-      "runContainerSync": true,
-      "restartContainer": true,
-      "restartContainerSync": true,
-      "createTarFile": true,
-      "createTarFileSync": true,
-      "getImageData": true,
-      "getImageDataSync": true,
-      "removeImage": true,
-      "removeImageSync": true,
-      "deleteImage": true,
       "checkDefaultImages": true,
       "resolveDefaultImages": true,
       "checkDefaultContainers": true,
       "resolveDefaultContainers": true,
       "killAndRemoveContainers": true,
-      "deleteImageSync": true,
       "upContainers": true,
       "reloadDefaultContainers": true,
       "removeImages": true,
-      "pullImageFromDockerfile": true,
-      "buildImage": true,
-      "getImageMetaData": true,
-      "getImageJSON": true,
-      "rebuildImage": true,
-      "saveImageFolderSync": true,
-      "rebuildImageSync": true,
-      "saveImageFolder": true,
-      "copyFolder": true,
 
       // Forms
       "showFormErrors": true,
@@ -239,11 +217,6 @@
       "FormSchema": true,
       "showFormSuccess": true,
       "resetForm": true,
-      "removeContainer": true,
-      "removeContainerSync": true,
-      "deleteAppSync": true,
-      "getContainerData": true,
-      "getContainerDataSync": true,
 
       // Testing
       "require": false,
diff --git a/meteor/client/lib/boot2docker.js b/meteor/client/lib/boot2docker.js
index 8e939a2dc3..589b85b67d 100644
--- a/meteor/client/lib/boot2docker.js
+++ b/meteor/client/lib/boot2docker.js
@@ -1,14 +1,54 @@
 var exec = require('exec');
 var path = require('path');
 
-boot2dockerexec = function (command, callback) {
-  exec(path.join(getBinDir(), 'boot2docker') + ' ' + command, function(err, stdout) {
-    callback(err, stdout);
+Boot2Docker = {};
+
+Boot2Docker.REQUIRED_IP = '192.168.60.103';
+
+Boot2Docker.exec = function (command, callback) {
+  exec(path.join(Util.getBinDir(), 'boot2docker') + ' ' + command, function(err, stdout, stderr) {
+    callback(err, stdout, stderr);
   });
 };
 
-getBoot2DockerIp = function (callback) {
-  boot2dockerexec('ip', function (err, stdout) {
+Boot2Docker.exists = function (callback) {
+  this.exec('info', function (err) {
+    if (err) {
+      callback(null, false);
+    } else {
+      callback(null, true);
+    }
+  });
+};
+
+Boot2Docker.stop = function (callback) {
+  this.exec('stop', function (err, stdout) {
+    if (err) {
+      callback(err);
+    } else {
+      callback(null);
+    }
+  });
+};
+
+Boot2Docker.erase = function (callback) {
+  var VMFileLocation = path.join(Util.getHomePath(), 'VirtualBox\\ VMs/boot2docker-vm');
+  exec('rm -rf ' + VMFileLocation, function (err) {
+    callback(err);
+  });
+};
+
+Boot2Docker.upgrade = function (callback) {
+  var self = this;
+  self.stop(function (err) {
+    self.exec('upgrade', function (err, stdout) {
+      callback(err);
+    });
+  });
+};
+
+Boot2Docker.ip = function (callback) {
+  this.exec('ip', function (err, stdout) {
     if (err) {
       callback(err, null);
     } else {
@@ -17,12 +57,54 @@ getBoot2DockerIp = function (callback) {
   });
 };
 
-getBoot2DockerState = function (callback) {
-  boot2dockerexec(' info', function (err, stdout) {
-    if (err) {
-      callback(err, null);
+Boot2Docker.setIp = function (ifname, ip, callback) {
+  this.exec('ssh "sudo ifconfig ' + ifname + ' ' + ip + ' netmask 255.255.255.0"', function (err, stdout) {
+    callback(err);
+  });
+};
+
+Boot2Docker.init = function (callback) {
+  this.exec('init', function (err) {
+    callback(err);
+  });
+};
+
+Boot2Docker.start = function (callback) {
+  var self = this;
+  self.exists(function (err, exists) {
+    if (!exists) {
+      callback('Cannot start if the boot2docker VM doesn\'t exist');
       return;
     }
+    self.exec('up -v', function (err, stdout) {
+      // Sometimes boot2docker returns an error code even though it's working / waiting, so treat that as
+      // Success as well
+      if (!err || (err.indexOf('Waiting for VM to be started') !== -1 || err.indexOf('..........') !== -1)) {
+        self.correct(function (err) {
+          if (err) { callback(err); return; }
+          self.injectUtilities(function (err) {
+            callback(err);
+          });
+        });
+      } else {
+        callback(err);
+      }
+    });
+  });
+};
+
+Boot2Docker.correct = function (callback) {
+  Boot2Docker.setIp('eth2', Boot2Docker.REQUIRED_IP, function(err) {
+    if (err) { callback(err); return; }
+    VirtualBox.removeDHCP(function (err) {
+      callback();
+    });
+  });
+};
+
+Boot2Docker.state = function (callback) {
+  this.exec('info', function (err, stdout, stderr) {
+    if (err) { callback(err, null); return; }
     try {
       var info = JSON.parse(stdout);
       callback(null, info.State);
@@ -32,8 +114,8 @@ getBoot2DockerState = function (callback) {
   });
 };
 
-getBoot2DockerDiskUsage = function (callback) {
-  boot2dockerexec('ssh "df"', function (err, stdout) {
+Boot2Docker.diskUsage = function (callback) {
+  this.exec('ssh "df"', function (err, stdout) {
     if (err) {
       callback(err, null);
       return;
@@ -61,8 +143,8 @@ getBoot2DockerDiskUsage = function (callback) {
   });
 };
 
-getBoot2DockerMemoryUsage = function (callback) {
-  boot2dockerexec('ssh "free -m"', function (err, stdout) {
+Boot2Docker.memoryUsage = function (callback) {
+  this.exec('ssh "free -m"', function (err, stdout) {
     if (err) {
       callback(err, null);
       return;
@@ -92,178 +174,105 @@ getBoot2DockerMemoryUsage = function (callback) {
   });
 };
 
-getBoot2DockerInfo = function (callback) {
-  boot2dockerexec('ssh "sudo ifconfig eth1 192.168.59.103 netmask 255.255.255.0"', function (err, stdout) {
-    exec('VBoxManage dhcpserver remove --netname HostInterfaceNetworking-vboxnet0', function (err, stdout) {
-      getBoot2DockerState(function (err, state) {
+Boot2Docker.stats = function (callback) {
+  var self = this;
+  self.state(function (err, state) {
+    if (err) { callback(err, null); return; }
+    if (state === 'poweroff') {
+      callback(null, {state: state});
+      return;
+    }
+    self.memoryUsage(function (err, mem) {
       if (err) {
-        callback(err, null);
+        callback(null, {state: state});
         return;
       }
-      if (state === 'poweroff') {
-        callback(null, {state: state});
-      } else {
-        getBoot2DockerMemoryUsage(function (err, mem) {
-          if (err) { callback(null, {state: state}); }
-          getBoot2DockerDiskUsage(function (err, disk) {
-            if (err) { callback(null, {state: state}); }
-            callback(null, {
-              state: state,
-              memory: mem,
-              disk: disk
-            });
-          });
-        });
-      }
-    });
-    });
-  });
-};
-
-boot2DockerVMExists = function (callback) {
-  boot2dockerexec('info', function (err) {
-    if (err) {
-      callback(null, false);
-    } else {
-      callback(null, true);
-    }
-  });
-};
-
-eraseBoot2DockerVMFiles = function (callback) {
-  var VMFileLocation = path.join(getHomePath(), 'VirtualBox\\ VMs/boot2docker-vm');
-  exec('rm -rf ' + VMFileLocation, function (err) {
-    callback(err);
-  });
-};
-
-initBoot2Docker = function (callback) {
-  isVirtualBoxInstalled(function (err, installed) {
-    if (err) {
-      callback(err);
-      return;
-    }
-    if (installed) {
-      boot2dockerexec('init', function (err) {
-        console.log(err);
+      self.diskUsage(function (err, disk) {
         if (err) {
-          if (err.indexOf('exit status 1') !== -1) {
-            eraseBoot2DockerVMFiles(function () {
-              boot2dockerexec('init', function (err) {
-                callback(err);
-              });
-            });
-          } else {
-            callback(err);
-          }
-        } else {
-          callback();
+          callback(null, {state: state, memory: mem});
+          return;
         }
+        callback(null, {
+          state: state,
+          memory: mem,
+          disk: disk
+        });
       });
+    });
+  });
+};
+
+/**
+ * Get the VM's version.
+ * Node that this only works if the VM is up and running.
+ */
+Boot2Docker.vmVersion = function (callback) {
+  this.exec('ssh "cat /etc/version', function (err, stdout, stderr) {
+    if (err) {
+      callback(err);
+      return;
     } else {
-      callback(new Error('initBoot2Docker called but VirtualBox isn\'t installed.'));
+      callback(null, stdout);
     }
   });
 };
 
-upgradeBoot2Docker = function (callback) {
-  boot2dockerexec('upgrade', function (err, stdout) {
-    console.log(stdout);
+Boot2Docker.version = function (callback) {
+  this.exec('version', function (err, stdout, stderr) {
+    if (err) {
+      callback(err);
+      return;
+    }
+    var match = stdout.match(/Client version: v(\d\.\d\.\d)/);
+    if (!match || match.length < 2) {
+      callback('Could not parse the boot2docker cli version.');
+    } else {
+      callback(null, match[1]);
+    }
+  });
+};
+
+Boot2Docker.injectUtilities = function (callback) {
+  exec('/bin/cat ' + path.join(Util.getBinDir(), 'kite-binaries.tar.gz') + ' | ' +  path.join(Util.getBinDir(), 'boot2docker') + ' ssh "tar zx -C /usr/local/bin"', function (err, stdout) {
     callback(err);
   });
 };
 
-installBoot2DockerAddons = function (callback) {
-  exec('/bin/cat ' + path.join(getBinDir(), 'kite-binaries.tar.gz') + ' | ' +  path.join(getBinDir(), 'boot2docker') + ' ssh "tar zx -C /usr/local/bin"', function (err, stdout) {
-    console.log(stdout);
-    callback(err);
-  });
-  boot2dockerexec('ssh "sudo ifconfig eth1 192.168.59.103 netmask 255.255.255.0"', function (err, stdout) {});
-  exec('VBoxManage dhcpserver remove --netname HostInterfaceNetworking-vboxnet0', function (err, stdout) {});
-};
-
-startBoot2Docker = function (callback) {
-  isVirtualBoxInstalled(function (err, installed) {
-    if (err) {
-      callback(err);
-      return;
-    }
-    if (installed) {
-      boot2DockerVMExists(function (err, exists) {
-        if (exists) {
-          boot2dockerexec('up -v', function (err, stdout) {
-            console.log(err);
-            console.log(stdout);
-            if (err) {
-              if (err.indexOf('Waiting for VM to be started') !== -1 || err.indexOf('..........') !== -1) {
-                installBoot2DockerAddons(function (err) {
-                  callback(err);
-                });
-              } else {
-                callback(err);
-              }
-            } else {
-              installBoot2DockerAddons(function (err) {
-                callback(err);
-              });
-            }
-          });
-        } else {
-          callback(new Error('startBoot2Docker called but boot2docker-vm doesn\'t exist.'));
-        }
-      });
-    } else {
-      callback(new Error('startBoot2Docker called but VirtualBox isn\'t installed.'));
-    }
-  });
-};
-
-stopBoot2Docker = function (callback) {
-  boot2dockerexec('stop', function (err, stdout) {
-    console.log(stdout);
-    console.log(err);
-    if (err) {
-      callback(err);
-      return;
-    }
-    callback(null);
-  });
-};
-
-checkBoot2DockerVM = function (callback) {
-  boot2DockerVMExists(function (err) {
+Boot2Docker.check = function (callback) {
+  var self = this;
+  self.exists(function (err, exists) {
     if (err) {
       callback(err);
       return;
     } else {
-      getBoot2DockerState(function (err, state) {
+      self.state(function (err, state) {
         if (state !== 'running') {
           callback('boot2docker not running');
         } else {
-          callback();
+          self.correct(function (err) {
+            callback(err);
+          });
         }
       });
     }
   });
 };
 
-// Make sure the VM exists, is up and is running.
-resolveBoot2DockerVM = function (callback) {
-  boot2DockerVMExists(function (err, exists) {
-
+Boot2Docker.resolve = function (callback) {
+  var self = this;
+  self.exists(function (err, exists) {
     // If somehow the boot2docker VM doesn't exist anymor then re-create it.
     if (!exists) {
-      initBoot2Docker(function () {
-        startBoot2Docker(function (err) {
+      self.init(function () {
+        self.start(function (err) {
           callback(err);
         });
       });
     } else {
-
       // If it exists but it's not running.. restart it.
-      getBoot2DockerState(function (err, state) {
+      self.state(function (err, state) {
         if (state !== 'running') {
-          startBoot2Docker(function (err) {
+          self.start(function (err) {
             callback(err);
           });
         } else {
diff --git a/meteor/client/lib/installer.js b/meteor/client/lib/installer.js
new file mode 100644
index 0000000000..459ed06f7b
--- /dev/null
+++ b/meteor/client/lib/installer.js
@@ -0,0 +1,160 @@
+var async = require('async');
+
+Installer = {};
+
+Installer.CURRENT_VERSION = '0.0.2';
+
+Installer.isUpToDate = function () {
+  return !!Installs.findOne({version: Installer.CURRENT_VERSION});
+};
+
+/**
+ * Install steps. A step is a function that accepts a function (err) callback and returns once that step is complete.keys:
+ * - run: Function that runs the installation step and calls the callback with an error if failed.
+ * - pastMessage: Message to show after step completion
+ * - message: Message to show while step is running
+ * - imperativeMessage: Message to show before running
+ */
+Installer.steps = [
+  {
+    run: function (callback) {
+      var installed = VirtualBox.installed();
+      if (!installed) {
+        VirtualBox.install(function (err) {
+          callback(err);
+        });
+      } else {
+        // Version 4.3.12 is required.
+        VirtualBox.version(function (err, installedVersion) {
+          if (err) {
+            callback(err);
+            return;
+          }
+          var needsUpdate = Util.compareVersions(installedVersion, VirtualBox.REQUIRED_VERSION) < 0;
+          if (needsUpdate) {
+            VirtualBox.install(function (err) {
+              callback(err);
+            });
+          } else {
+            callback();
+          }
+        });
+      }
+    },
+    pastMessage: 'VirtualBox installed',
+    message: 'Installing VirtualBox',
+    futureMessage: 'Install VirtualBox if necessary'
+  },
+
+  // Initialize Boot2Docker if necessary.
+  {
+    run: function (callback) {
+      Boot2Docker.exists(function (err, exists) {
+        if (err) { callback(err); return; }
+        if (!exists) {
+          Boot2Docker.init(function (err) {
+            callback(err);
+          });
+        } else {
+          Boot2Docker.stop(function (err) {
+            if (err) { callback(err); return; }
+            Boot2Docker.upgrade(function (err) {
+              callback(err);
+            });
+          });
+        }
+      });
+    },
+    pastMessage: 'Setup the Boot2Docker VM (if required)',
+    message: 'Setting up the Boot2Docker VM',
+    futureMessage: 'Set up the Boot2Docker VM(if required)'
+  },
+
+  {
+    run: function (callback) {
+      VirtualBox.addCustomHostAdapter('boot2docker-vm', function (err, ifname) {
+        callback(err);
+      });
+    },
+    pastMessage: 'Added custom host adapter to the Boot2Docker VM',
+    message: 'Adding custom host adapter to the Boot2Docker VM',
+    futureMessage: 'Add custom host adapter to the Boot2Docker VM'
+  },
+
+  // Start the Kitematic VM
+  {
+    run: function (callback) {
+      Boot2Docker.state(function (err, state) {
+        if (err) { callback(err); return; }
+        if (state !== 'running') {
+          console.log('starting');
+          Boot2Docker.start(function (err) {
+            callback(err);
+          });
+        } else {
+          Boot2Docker.setIp('eth2', Boot2Docker.REQUIRED_IP, function(err) {
+            callback(err);
+          });
+        }
+      });
+    },
+    pastMessage: 'Started the Boot2Docker VM',
+    message: 'Starting the Boot2Docker VM',
+    futureMessage: 'Start the Kitematic VM',
+  },
+
+  {
+    run: function (callback) {
+      VirtualBox.setupRouting('boot2docker-vm', function (err, ifname) {
+        callback(err);
+      });
+    },
+    pastMessage: 'Container routing set up',
+    message: 'Setting up container routing (root required)',
+    futureMessage: 'Set up container routing to VM (root required)'
+  },
+
+  // Set up the default Kitematic images
+  {
+    run: function (callback) {
+      Meteor.call('reloadDefaultContainers', function (err) {
+        callback(err);
+      });
+    },
+    pastMessage: 'Started the Boot2Docker VM',
+    message: 'Setting up the default Kitematic images...',
+    subMessage: '(This may take a few minutes)',
+    futureMessage: 'Set up the default Kitematic images',
+  }
+];
+
+Installer.run = function (callback) {
+  var currentStep = 0;
+  Session.set('currentInstallStep', currentStep);
+  Session.set('numberOfInstallSteps', this.steps.length);
+  async.eachSeries(this.steps, function (step, callback) {
+    console.log('Performing step ' + currentStep);
+    step.run(function (err) {
+      if (err) {
+        callback(err);
+      } else {
+        currentStep += 1;
+        Session.set('currentInstallStep', currentStep);
+        callback();
+      }
+    });
+  }, function (err) {
+    if (err) {
+      // if any of the steps fail
+      console.log('Kitematic setup failed at step ' + currentStep);
+      console.log(err);
+      Session.set('failedStep', currentStep);
+      Session.set('failedError', err);
+      callback(err);
+    } else {
+      // Setup Finished
+      console.log('Setup finished.');
+      callback();
+    }
+  });
+};
diff --git a/meteor/client/lib/requires.js b/meteor/client/lib/requires.js
new file mode 100644
index 0000000000..2b261be406
--- /dev/null
+++ b/meteor/client/lib/requires.js
@@ -0,0 +1,2 @@
+path = require('path');
+fs = require('fs');
diff --git a/meteor/client/lib/router.js b/meteor/client/lib/router.js
index 0e9f2ecb6e..6e8168efd6 100755
--- a/meteor/client/lib/router.js
+++ b/meteor/client/lib/router.js
@@ -48,12 +48,16 @@ Router.map(function () {
     controller: 'SetupController',
     action: function () {
       if (this.ready()) {
-        var install = Installs.findOne();
-        if (!install) {
-          console.log('No installs detected, running installer again.');
-          this.redirect('/setup/intro');
+        if (!Installer.isUpToDate()) {
+          if (!Installs.findOne()) {
+            console.log('No installs detected, running installer again.');
+            this.redirect('/setup/intro');
+          } else {
+            // There's an install but it's lower than the current version, re-run as an 'update'.
+            Session.set('isUpdating', true);
+            this.redirect('/setup/intro');
+          }
         } else {
-          startFixInterval();
           this.redirect('/apps');
         }
       }
diff --git a/meteor/client/lib/sync.js b/meteor/client/lib/sync.js
index 4dbbcace5d..cd2468ef2d 100644
--- a/meteor/client/lib/sync.js
+++ b/meteor/client/lib/sync.js
@@ -15,7 +15,7 @@ removeAppWatcher = function (id) {
 
 addAppWatcher = function (app) {
   removeAppWatcher(app._id);
-  var appPath = path.join(path.join(getHomePath(), 'Kitematic'), app.name);
+  var appPath = path.join(path.join(Util.getHomePath(), 'Kitematic'), app.name);
   var vmDir = path.join('/var/lib/docker/binds', app.name);
   var vmPath = 'ssh://docker@localhost:2022/' + vmDir;
   var watcher = chokidar.watch(appPath, {ignored: /.*\.DS_Store/});
@@ -30,7 +30,7 @@ addAppWatcher = function (app) {
     syncing = true;
     var errorPattern = /The\sfile\s(.*)\son\shost/g;
     var archiveErrorPattern = /Archive\s(.*)\son\shost\s.*\sshould\sbe\sDELETED/g;
-    var cmd = path.join(getBinDir(), 'unison');
+    var cmd = path.join(Util.getBinDir(), 'unison');
     var args = [
       cmd,
       vmPath,
@@ -46,7 +46,7 @@ addAppWatcher = function (app) {
       'Name\ {*.tmp,*.unison,*.swp,*.pyc,.DS_Store}',
       '-auto',
       '-sshargs',
-      '-o\ UserKnownHostsFile=/dev/null\ -o\ StrictHostKeyChecking=no\ -o PreferredAuthentications=publickey\ -i\ ' + path.join(getHomePath(), '.ssh/id_boot2docker')
+      '-o\ UserKnownHostsFile=/dev/null\ -o\ StrictHostKeyChecking=no\ -o PreferredAuthentications=publickey\ -i\ ' + path.join(Util.getHomePath(), '.ssh/id_boot2docker')
     ];
 
     if (!fs.existsSync(appPath)) {
@@ -70,12 +70,12 @@ addAppWatcher = function (app) {
         if (err.indexOf('The archive file is missing on some hosts') !== -1) {
           var results = archiveErrorPattern.exec(err);
           var location = results[1].replace(' ', '\\ ');
-          var fullLocation = path.join(getHomePath(), 'Library/Application\\ Support/Unison', location);
+          var fullLocation = path.join(Util.getHomePath(), 'Library/Application\\ Support/Unison', location);
           var cmd = '/bin/rm -rf ' + fullLocation;
           exec(cmd, function () {});
         }
       } catch (e) {
-        // console.error(e);
+        //console.error(e);
       }
       syncing = false;
       if (willSyncAgain) {
@@ -118,4 +118,4 @@ resolveWatchers = function (callback) {
   });
 
   callback();
-};
\ No newline at end of file
+};
diff --git a/meteor/client/lib/utilities.js b/meteor/client/lib/utilities.js
index c3fe268798..3315ad4040 100755
--- a/meteor/client/lib/utilities.js
+++ b/meteor/client/lib/utilities.js
@@ -1,17 +1,3 @@
-var path = require('path');
-
-getBinDir = function () {
-  if (process.env.NODE_ENV === 'development') {
-    return path.join(path.join(process.env.PWD, '..'), 'resources');
-  } else {
-    return path.join(process.cwd(), 'resources');
-  }
-};
-
-getHomePath = function () {
-  return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
-};
-
 showFormErrors = function ($form, errors) {
   for (var name in errors) {
     if (errors.hasOwnProperty(name)) {
@@ -31,7 +17,7 @@ clearFormErrors = function ($form) {
   $form.find('.form-group.has-error .help-block.error').remove();
   $form.find('.form-group.has-error').removeClass('has-error');
 };
- 
+
 resetForm = function ($form) {
   $form.find('input').val('');
 };
@@ -41,4 +27,4 @@ trackLink = function (trackLabel) {
     console.log(trackLabel);
     ga('send', 'event', 'link', 'click', trackLabel);
   }
-};
\ No newline at end of file
+};
diff --git a/meteor/client/lib/virtualbox.js b/meteor/client/lib/virtualbox.js
index 6d362492a9..4368f5c72f 100644
--- a/meteor/client/lib/virtualbox.js
+++ b/meteor/client/lib/virtualbox.js
@@ -1,55 +1,158 @@
 var fs = require('fs');
-var child_process = require('child_process');
+var exec = require('exec');
 var path = require('path');
 
-isVirtualBoxInstalled = function (callback) {
-  fs.exists('/usr/bin/VBoxManage', function (exists) {
-    callback(null, exists);
+VirtualBox = {};
+
+VirtualBox.REQUIRED_VERSION = '4.3.12';
+VirtualBox.INCLUDED_VERSION = '4.3.12';
+VirtualBox.INSTALLER_FILENAME = 'virtualbox-4.3.12.pkg';
+
+// Info for the hostonly interface we add to the VM.
+VirtualBox.HOSTONLY_HOSTIP = '192.168.60.3';
+VirtualBox.HOSTONLY_NETWORKMASK = '255.255.255.0';
+
+VirtualBox.installed = function () {
+  return fs.existsSync('/usr/bin/VBoxManage');
+};
+
+VirtualBox.exec = function (command, callback) {
+  exec('/usr/bin/VBoxManage ' + command, function (error, stdout, stderr) {
+    callback(error, stdout, stderr);
   });
 };
 
-isResolverSetup = function (callback) {
-  fs.readFile('/etc/resolver/dev', {
-    encoding: 'utf8'
-  }, function (err, data) {
+VirtualBox.install = function (callback) {
+  // -W waits for the process to close before finishing.
+  exec('open -W ' + path.join(Util.getBinDir(), this.INSTALLER_FILENAME), function (error, stdout, stderr) {
+    if (error) {
+      callback(error);
+      return;
+    }
+    callback(null);
+  });
+};
+
+VirtualBox.version = function (callback) {
+  if (!this.installed()) {
+    callback('VirtualBox not installed.');
+    return;
+  }
+  this.exec('-v', function (err, stdout, stderr) {
     if (err) {
-      callback(err, false);
-    } else {
-      if (data.indexOf('nameserver 172.17.42.1') !== -1) {
-        callback(null, true);
-      } else {
-        callback(null, false);
+      callback(err);
+      return;
+    }
+    // Output is x.x.xryyyyyy
+    var match = stdout.match(/(\d+\.\d+\.\d+).*/);
+    if (!match || match.length < 2) {
+      callback('VBoxManage -v output format not recognized.');
+      return;
+    }
+    callback(null, match[1]);
+  });
+};
+
+VirtualBox.hostOnlyIfs = function (callback) {
+  this.exec('list hostonlyifs', function (err, stdout, stderr) {
+    if (err) {
+      callback(err);
+      return;
+    }
+    var lines = stdout.split('\n');
+    var hostOnlyIfs = {};
+    var currentIf = null;
+    _.each(lines, function (line) {
+      if (!line.length) {
+        return;
       }
-    }
+      var pieces = line.split(':');
+      var key = pieces[0].trim();
+      var value = pieces[1] ? pieces[1].trim() : null;
+      if (key === 'Name') {
+        currentIf = value;
+        hostOnlyIfs[value] = {};
+      }
+      hostOnlyIfs[currentIf][key] = value; 
+    });
+    callback(null, hostOnlyIfs);
   });
 };
 
-setupResolver = function (callback) {
-  var installFile = path.join(getBinDir(), 'install');
-  var cocoaSudo = path.join(getBinDir(), 'cocoasudo');
-  var execCommand = cocoaSudo + ' --prompt="Kitematic Setup wants to make changes. Type your password to allow this." ' + installFile;
-  child_process.exec(execCommand, function (error, stdout, stderr) {
-    console.log(stdout);
-    if (error) {
-      console.log(error);
-      callback(error);
+VirtualBox.hostOnlyAdapters = function (vm, callback) {
+  this.exec('showvminfo ' + vm + ' --machinereadable', function (err, stdout, stderr) {
+    if (err) {
+      callback(err);
       return;
     }
-    console.log('Virtualbox Installation & Resolver config complete.');
-    callback();
-  });
-};
-
-setupVirtualBox = function (callback) {
-  child_process.exec('open -W ' + path.join(getBinDir(), 'virtualbox-4.3.12.pkg'), function (error, stdout, stderr) {
-    console.log(stdout);
-    if (error) {
-      console.log(error);
-      callback(error);
-      return;
+    var matches = stdout.match(/(hostonlyadapter\d+)="(vboxnet\d+)"/g);
+    if (!matches.length) {
+      callback(null, {});
+    } else {
+      var objs = {};
+      _.each(matches, function (match) {
+        var pieces = match.split('=');
+        objs[pieces[0]] = pieces[1].replace(/"/g, '');
+      });
+      callback(null, objs);
     }
-    console.log('Virtualbox Installation running.');
-    callback();
   });
 };
 
+VirtualBox.hostOnlyAdapter = function (callback) {
+  var self = this;
+  self.hostOnlyIfs(function (err, ifs) {
+    var iface = _.findWhere(_.toArray(ifs), {IPAddress: VirtualBox.HOSTONLY_HOSTIP});
+    if (!iface) {
+      self.exec('hostonlyif create', function (err, stdout, stderr) {
+        var match = stdout.match(/Interface '(vboxnet\d+)' was successfully created/);
+        if (!match) {
+          callback('Could not parse output of hostonlyif create');
+          return;
+        }
+        self.exec('hostonlyif ipconfig ' + match[1] + ' --ip ' + VirtualBox.HOSTONLY_HOSTIP + ' --netmask ' + VirtualBox.HOSTONLY_NETWORKMASK, function(err, stdout, stderr) {
+          if (err) { callback(err); return; }
+          callback(null, match[1]);
+        });
+      });
+    } else {
+      callback(null, iface.Name);
+    }
+  });
+};
+
+VirtualBox.addCustomHostAdapter = function (vm, callback) {
+  var self = this;
+  self.hostOnlyAdapter(function (err, ifname) {
+    if (err) { callback(err); return; }
+    self.exec('modifyvm ' + vm + ' --nic3 hostonly --nictype3 virtio --cableconnected3 on --hostonlyadapter3 ' + ifname, function (err, stdout, stderr) {
+      callback(err, ifname);
+    });
+  });
+};
+
+VirtualBox.setupRouting = function (vm, callback) {
+  // Get the host only adapter or create it if it doesn't exist
+  this.addCustomHostAdapter(vm, function (err, ifname) {
+    var installFile = path.join(Util.getBinDir(), 'install');
+    var cocoaSudo = path.join(Util.getBinDir(), 'cocoasudo');
+    var execCommand = cocoaSudo + ' --prompt="Kitematic needs your password to allow routing *.kite requests to containers." ' + installFile;
+    exec(execCommand, {env: {IFNAME: ifname, GATEWAY: Boot2Docker.REQUIRED_IP}}, function (error, stdout, stderr) {
+      if (error) {
+        callback(error);
+        return;
+      }
+      callback();
+    });
+  });
+};
+
+VirtualBox.removeDHCP = function (callback) {
+  var self = this;
+  self.hostOnlyAdapter(function (err, ifname) {
+    if (err) { callback(err); return; }
+    self.exec('dhcpserver remove --ifname ' + ifname, function (err, stdout, stderr) {
+      callback(err);
+    });
+  });
+};
diff --git a/meteor/client/main.js b/meteor/client/main.js
index 7abe7d59a1..429201d295 100755
--- a/meteor/client/main.js
+++ b/meteor/client/main.js
@@ -68,24 +68,11 @@ Meteor.call('getDockerHost', function (err, host) {
   Session.set('dockerHost', host);
 });
 
-updateBoot2DockerInfo = function () {
-  getBoot2DockerInfo(function (err, info) {
-    if (err) {
-      return;
-    }
-    Session.set('boot2dockerState', info.state);
-    if (info.state !== 'poweroff' && info.memory && info.disk) {
-      Session.set('boot2dockerMemoryUsage', info.memory);
-      Session.set('boot2dockerDiskUsage', info.disk);
-    }
-  });
-};
-
 fixBoot2DockerVM = function (callback) {
-  checkBoot2DockerVM(function (err) {
+  Boot2Docker.check(function (err) {
     if (err) {
       Session.set('available', false);
-      resolveBoot2DockerVM(function (err) {
+      Boot2Docker.resolve(function (err) {
         if (err) {
           callback(err);
         } else {
@@ -138,28 +125,41 @@ fixDefaultContainers = function (callback) {
 };
 
 Meteor.setInterval(function () {
-  updateBoot2DockerInfo();
+  Boot2Docker.exists(function (err, exists) {
+    if (err) { console.log(err); return; }
+    if (exists) {
+      Boot2Docker.state(function (err, state) {
+        if (err) { console.log(err); return; }
+        Session.set('boot2dockerState', state);
+        if (state === 'running') {
+          Boot2Docker.stats(function (err, stats) {
+            if (err) { console.log(err); return; }
+            if (stats.state !== 'poweroff' && stats.memory && stats.disk) {
+              Session.set('boot2dockerMemoryUsage', stats.memory);
+              Session.set('boot2dockerDiskUsage', stats.disk);
+            }
+          });
+        }
+      });
+    }
+  });
 }, 5000);
 
-fixInterval = null;
-startFixInterval = function () {
-  stopFixInterval();
-  fixInterval = Meteor.setInterval(function () {
+Meteor.setInterval(function () {
+  if (Installer.isUpToDate()) {
     resolveWatchers(function () {});
-    fixBoot2DockerVM(function (err) {
-      if (err) { console.log(err); return; }
-      // Meteor.call('recoverApps');
-      fixDefaultImages(function (err) {
+    if (!Session.get('boot2dockerOff')) {
+      fixBoot2DockerVM(function (err) {
         if (err) { console.log(err); return; }
-        fixDefaultContainers(function (err) {
-          if (err) { console.log(err); }
+        Meteor.call('recoverApps');
+        fixDefaultImages(function (err) {
+          if (err) { console.log(err); return; }
+          fixDefaultContainers(function (err) {
+            if (err) { console.log(err); }
+          });
         });
       });
-    });
-  }, 5000);
-};
+    }
+  }
+}, 5000);
 
-stopFixInterval = function () {
-  Meteor.clearInterval(fixInterval);
-  fixInterval = null;
-};
\ No newline at end of file
diff --git a/meteor/client/views/dashboard/apps/dashboard-single-app.js b/meteor/client/views/dashboard/apps/dashboard-single-app.js
index acbd198e79..5d32c4818f 100755
--- a/meteor/client/views/dashboard/apps/dashboard-single-app.js
+++ b/meteor/client/views/dashboard/apps/dashboard-single-app.js
@@ -21,8 +21,8 @@ Template.dashboard_single_app.events({
   },
   'click .btn-terminal': function () {
     var app = this;
-    var cmd = path.join(getBinDir(), 'boot2docker') + ' ssh -t "sudo docker-enter ' + app.docker.Id + '"';
-    var terminalCmd = path.join(getBinDir(),  'terminal') + ' ' + cmd;
+    var cmd = path.join(Util.getBinDir(), 'boot2docker') + ' ssh -t "sudo docker-enter ' + app.docker.Id + '"';
+    var terminalCmd = path.join(Util.getBinDir(),  'terminal') + ' ' + cmd;
     var exec = require('child_process').exec;
     console.log(terminalCmd);
     exec(terminalCmd, function (err, stdout) {
diff --git a/meteor/client/views/dashboard/settings/dashboard-settings.js b/meteor/client/views/dashboard/settings/dashboard-settings.js
index cf12f91c27..1a2ef4b7bd 100644
--- a/meteor/client/views/dashboard/settings/dashboard-settings.js
+++ b/meteor/client/views/dashboard/settings/dashboard-settings.js
@@ -3,18 +3,22 @@ Template.dashboard_settings.events({
     var $btn = $(e.currentTarget);
     $btn.html('Starting Boot2Docker...');
     $btn.attr("disabled", "disabled");
-    startFixInterval();
-    startBoot2Docker(function (err) {
-      if (err) { console.error(err); }
+    Session.set('boot2dockerOff', false);
+    Boot2Docker.start(function (err) {
+      if (err) {
+        console.log(err);
+      }
     });
   },
   'click .btn-stop-boot2docker': function (e) {
     var $btn = $(e.currentTarget);
     $btn.html('Stopping Boot2Docker...');
     $btn.attr("disabled", "disabled");
-    stopFixInterval();
-    stopBoot2Docker(function (err) {
-      if (err) { console.error(err); }
+    Session.set('boot2dockerOff', true);
+    Boot2Docker.stop(function (err) {
+      if (err) {
+        console.log(err);
+      }
     });
   }
 });
diff --git a/meteor/client/views/dashboard/setup/setup-install.js b/meteor/client/views/dashboard/setup/setup-install.js
index 004ac755cf..d5774fbbd7 100644
--- a/meteor/client/views/dashboard/setup/setup-install.js
+++ b/meteor/client/views/dashboard/setup/setup-install.js
@@ -1,142 +1,13 @@
-var async = require('async');
-
-// Install steps. A step is a function that accepts a function (err) callback and returns once that step is complete.
-// keys:
-// - install: Function that runs the installation step and calls the callback with an error if failed.
-// - pastMessage: Message to show after step completion
-// - message: Message to show while step is running
-// - imperativeMessage: Message to show before running
-var steps = [
-
-  // Set up VirtualBox
-  {
-    install: function (callback) {
-      isVirtualBoxInstalled(function (err, virtualBoxInstalled) {
-        var installedYet = false;
-        if (!virtualBoxInstalled) {
-          setupVirtualBox(function (err) {
-            callback(err);
-          });
-        } else {
-          callback();
-        }
-      });
-    },
-    pastMessage: 'VirtualBox installed',
-    message: 'Installing VirtualBox',
-    futureMessage: 'Install VirtualBox if necessary'
-  },
-
-  // Set up the routing.
-  {
-    install: function (callback) {
-      setupResolver(function (err) {
-        callback(err);
-      });
-    },
-    pastMessage: 'Container routing set up (root required).',
-    message: 'Setting up container routing (root required).',
-    subMessage: '(This may take a few minutes)',
-    futureMessage: 'Set up container routing to VM (root required).'
-  },
-
-  // Set up the VM for running Kitematic apps
-  {
-    install: function (callback) {
-      console.log('Checking if vm exists...');
-      boot2DockerVMExists(function (err, exists) {
-        console.log('VM exists: ' + exists);
-        if (exists) {
-          console.log('Stopping vm');
-          stopBoot2Docker(function () {
-            console.log('Upgrading vm');
-            upgradeBoot2Docker(function () {
-              callback();
-            });
-          });
-        } else {
-          console.log('init VM');
-           initBoot2Docker(function () {
-            callback();
-          });
-        }
-      });
-    },
-    pastMessage: 'Set up the Kitematic VM',
-    message: 'Setting up the Kitematic VM...',
-    futureMessage: 'Set up the Kitematic VM'
-  },
-
-  // Start the Kitematic VM
-  {
-    install: function (callback) {
-      startBoot2Docker(function (err) {
-        callback(err);
-      });
-    },
-    pastMessage: 'Started the Kitematic VM',
-    message: 'Starting the Kitematic VM',
-    subMessage: '(This may take a few minutes)',
-    futureMessage: 'Start the Kitematic VM',
-  },
-
-  // Set up the default Kitematic images
-  {
-    install: function (callback) {
-      Meteor.call('reloadDefaultContainers', function (err) {
-        callback(err);
-      });
-    },
-    pastMessage: 'Started the Kitematic VM',
-    message: 'Setting up the default Kitematic images...',
-    subMessage: '(This may take a few minutes)',
-    futureMessage: 'Set up the default Kitematic images',
-  }
-];
-
-runSetup = function (callback) {
-  // Run through the Kitematic installation, skipping steps if required.
-  var currentStep = 0;
-  Session.set('currentInstallStep', currentStep);
-  Session.set('numberOfInstallSteps', steps.length);
-  async.eachSeries(steps, function (step, callback) {
-    console.log('Performing step ' + currentStep);
-    step.install(function (err) {
-      if (err) {
-        callback(err);
-      } else {
-        currentStep += 1;
-        Session.set('currentInstallStep', currentStep);
-        callback();
-      }
-    });
-  }, function (err) {
-    if (err) {
-      // if any of the steps fail
-      console.log('Kitematic setup failed at step ' + currentStep);
-      console.log(err);
-      Session.set('failedStep', currentStep);
-      Session.set('failedError', err);
-      callback(err);
-    } else {
-      // Setup Finished
-      console.log('Setup finished.');
-      callback();
-    }
-  });
-};
-
 var installStarted = false;
 Template.setup_install.rendered = function() {
   if(!installStarted) {
     installStarted = true;
-    runSetup(function (err) {
+    Installer.run(function (err) {
       if (err) {
         console.log('Setup failed.');
         console.log(err);
       } else {
-        Installs.insert({});
-        startFixInterval();
+        Installs.insert({version: Installer.CURRENT_VERSION});
         Router.go('dashboard_apps');
       }
     });
@@ -144,7 +15,7 @@ Template.setup_install.rendered = function() {
 };
 
 Template.setup_install.steps = function () {
-  return steps.map(function (step, index) {
+  return Installer.steps.map(function (step, index) {
     step.index = index;
     return step;
   });
@@ -155,7 +26,7 @@ Template.setup_install.helpers({
     return Session.get('currentInstallStep');
   },
   installComplete: function () {
-    return Session.get('currentInstallStep') === steps.length;
+    return Session.get('currentInstallStep') === Installer.steps.length;
   },
   failedStep: function () {
     return Session.get('failedStep');
diff --git a/meteor/client/views/dashboard/setup/setup-layout.html b/meteor/client/views/dashboard/setup/setup-layout.html
index 12c0f8682b..ce3765beb4 100644
--- a/meteor/client/views/dashboard/setup/setup-layout.html
+++ b/meteor/client/views/dashboard/setup/setup-layout.html
@@ -1,8 +1,13 @@
 <template name="setup_layout">
   {{setTitle}}
   <div class="setup content text-center">
-    <h2>Welcome to Kitematic</h2>
-    <p>This will set up everything needed to run Kitematic on your Mac.</p>
+    {{#if isUpdating}}
+      <h2>Welcome Back</h2>
+      <p>Kitematic needs to update itself to continue.</p>
+    {{else}}
+      <h2>Welcome to Kitematic</h2>
+      <p>This will set up everything needed to run Kitematic on your Mac.</p>
+    {{/if}}
     {{> yield}}
   </div>
 </template>
diff --git a/meteor/client/views/dashboard/setup/setup-layout.js b/meteor/client/views/dashboard/setup/setup-layout.js
new file mode 100644
index 0000000000..2d99b3bc4b
--- /dev/null
+++ b/meteor/client/views/dashboard/setup/setup-layout.js
@@ -0,0 +1,11 @@
+Template.setup_layout.rendered = function () {
+  Meteor.setInterval(function () {
+    $('.header .icons a').tooltip();
+  }, 1000);
+};
+
+Template.setup_layout.helpers({
+  isUpdating: function () {
+    return Session.get('isUpdating');
+  }
+});
diff --git a/meteor/collections/apps.js b/meteor/collections/apps.js
index 3b0ff8b147..85cf5c5266 100755
--- a/meteor/collections/apps.js
+++ b/meteor/collections/apps.js
@@ -60,7 +60,7 @@ Apps.helpers({
     return Images.findOne(this.imageId);
   },
   hostUrl: function () {
-    return this.name + '.dev';
+    return this.name + '.kite';
   },
   ports: function () {
     var app = this;
@@ -78,7 +78,7 @@ Apps.helpers({
     var app = this;
     var image = Images.findOne(app.imageId);
     if (image && image.meta.app && image.meta.app.webPort) {
-      return 'http://' + app.name + '.dev:' + image.meta.app.webPort;
+      return 'http://' + app.name + '.kite:' + image.meta.app.webPort;
     } else if (image && image.meta.app && image.meta.app.webPort === false) {
       return null;
     } else {
@@ -93,14 +93,14 @@ Apps.helpers({
           }
         });
         if (pickedPort) {
-          return 'http://' + app.name + '.dev:' + pickedPort;
+          return 'http://' + app.name + '.kite:' + pickedPort;
         } else {
           if (keys.length > 0) {
             // Picks the first port that's not SSH
             for (var i = 0; i < keys.length; i++) {
               var port = parseInt(keys[i].split('/')[0], 10);
               if (port !== 22) {
-                return 'http://' + app.name + '.dev:' + port;
+                return 'http://' + app.name + '.kite:' + port;
               }
             }
             return null;
@@ -116,3 +116,41 @@ Apps.helpers({
 });
 
 Apps.attachSchema(schemaApps);
+
+Apps.after.insert(function (userId, app) {
+  // Give app an unique environment variable
+  var appId = this._id;
+  Apps.update(appId, {
+    $set: {
+      'config.APP_ID': appId
+    }
+  });
+  var image = Images.findOne(app.imageId);
+  Util.copyVolumes(image.path, app.name);
+  app = Apps.findOne(appId);
+  Docker.removeBindFolder(app.name, function (err) {
+    if (err) {
+      console.error(err);
+    }
+    Fiber(function () {
+      Meteor.call('runApp', app, function (err) {
+        if (err) { throw err; }
+      });
+    }).run();
+  });
+});
+
+Apps.after.remove(function (userId, app) {
+  if (app.docker) {
+    try {
+      Docker.removeContainerSync(app.docker.Id);
+    } catch (e) {
+      console.error(e);
+    }
+  }
+  var appPath = path.join(KITE_PATH, app.name);
+  Util.deleteFolder(appPath);
+  Docker.removeBindFolder(app.name, function () {
+    console.log('Deleted Kite ' + app.name + ' directory.');
+  });
+});
diff --git a/meteor/collections/images.js b/meteor/collections/images.js
index 15055ad021..5a4de5b4f8 100755
--- a/meteor/collections/images.js
+++ b/meteor/collections/images.js
@@ -91,3 +91,43 @@ Images.allow({
 });
 
 Images.attachSchema(schemaImages);
+
+Images.after.insert(function (userId, image) {
+  var imageId = this._id;
+  var imagePath = path.join(KITE_IMAGES_PATH, imageId);
+  Images.update(imageId, {
+    $set: {
+      path: imagePath
+    }
+  });
+  if (image.meta.logo) {
+    Images.update(imageId, {
+      $set: {
+        logoPath: path.join(imagePath, image.meta.logo)
+      }
+    });
+  }
+  image = Images.findOne(imageId);
+  Images.saveFolderSync(image.originPath, imageId);
+  Images.pull(fs.readFileSync(path.join(image.path, 'Dockerfile'), 'utf8'), imageId, function (err) {
+    if (err) { throw err; }
+    Images.build(image, function (err) {
+      if (err) { console.error(err); }
+    });
+  });
+});
+
+Images.after.remove(function (userId, image) {
+  if (image.docker) {
+    try {
+      Docker.removeImageSync(image.docker.Id);
+    } catch (e) {
+      console.error(e);
+    }
+  }
+  try {
+    Util.deleteFolder(image.path);
+  } catch (e) {
+    console.error(e);
+  }
+});
diff --git a/meteor/collections/installs.js b/meteor/collections/installs.js
index 6f0a492ce7..314b538050 100644
--- a/meteor/collections/installs.js
+++ b/meteor/collections/installs.js
@@ -1,7 +1,26 @@
 Installs = new Meteor.Collection('installs');
 
 schemaInstalls = new SimpleSchema({
-
+  createdAt: {
+    type: Date,
+    autoValue: function() {
+      var now = new Date();
+      if (this.isInsert) {
+        return now;
+      } else if (this.isUpsert) {
+        return {$setOnInsert: now};
+      } else {
+        this.unset();
+      }
+    },
+    denyUpdate: true,
+    label: 'Time of install created'
+  },
+  version: {
+    type: String,
+    label: 'Installed version',
+    optional: true
+  }
 });
 
 Installs.allow({
@@ -16,4 +35,4 @@ Installs.allow({
   }
 });
 
-Installs.attachSchema(schemaInstalls);
+Installs.attachSchema(schemaInstalls);
\ No newline at end of file
diff --git a/meteor/lib/utilities.js b/meteor/lib/utilities.js
new file mode 100644
index 0000000000..363ab245e7
--- /dev/null
+++ b/meteor/lib/utilities.js
@@ -0,0 +1,146 @@
+Util = {};
+
+Util.getHomePath = function () {
+  return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
+};
+
+Util.getBinDir = function () {
+  if (process.env.NODE_ENV === 'development') {
+    return path.join(path.join(process.env.PWD, '..'), 'resources');
+  } else {
+    if (Meteor.isClient) {
+      return path.join(process.cwd(), 'resources');
+    } else {
+      return path.join(process.cwd(), '../../../resources');
+    }
+  }
+};
+
+Util.deleteFolder = function (directory) {
+  if (fs.existsSync(directory)) {
+    fs.readdirSync(directory).forEach(function (file) {
+      var curDirectory = directory + '/' + file;
+      if (fs.lstatSync(curDirectory).isDirectory()) {
+        // Recurse
+        Util.deleteFolder(curDirectory);
+      } else {
+        // Delete File
+        try {
+          fs.unlinkSync(curDirectory);
+        } catch (e) {
+          console.error(e);
+        }
+      }
+    });
+    fs.rmdirSync(directory);
+  }
+};
+
+Util.copyFolder = function (src, dest) {
+  var exists = fs.existsSync(src);
+  var stats = exists && fs.statSync(src);
+  var isDirectory = exists && stats.isDirectory();
+  if (exists && isDirectory) {
+    try {
+      fs.mkdirSync(dest);
+    } catch (e) {
+      console.error(e);
+    }
+    fs.readdirSync(src).forEach(function (childItemName) {
+      Util.copyFolder(path.join(src, childItemName), path.join(dest, childItemName));
+    });
+  } else {
+    try {
+      fs.linkSync(src, dest);
+    } catch (e) {
+      console.error(e);
+    }
+  }
+};
+
+Util.copyVolumes = function (directory, appName) {
+  var KITE_VOLUMES_PATH = path.join(directory, 'volumes');
+  if (fs.existsSync(KITE_VOLUMES_PATH)) {
+    var destinationPath = path.join(KITE_PATH, appName);
+    Util.copyFolder(KITE_VOLUMES_PATH, destinationPath);
+    console.log('Copied volumes for: ' + appName);
+  }
+};
+
+Util.hasDockerfile = function (directory) {
+  return fs.existsSync(path.join(directory, 'Dockerfile'));
+};
+
+/**
+ * Compares two software version numbers (e.g. "1.7.1" or "1.2b").
+ *
+ * @param {string} v1 The first version to be compared.
+ * @param {string} v2 The second version to be compared.
+ * @param {object} [options] Optional flags that affect comparison behavior:
+ * <ul>
+ *     <li>
+ *         <tt>lexicographical: true</tt> compares each part of the version strings lexicographically instead of
+ *         naturally; this allows suffixes such as "b" or "dev" but will cause "1.10" to be considered smaller than
+ *         "1.2".
+ *     </li>
+ *     <li>
+ *         <tt>zeroExtend: true</tt> changes the result if one version string has less parts than the other. In
+ *         this case the shorter string will be padded with "zero" parts instead of being considered smaller.
+ *     </li>
+ * </ul>
+ * @returns {number|NaN}
+ * <ul>
+ *    <li>0 if the versions are equal</li>
+ *    <li>a negative integer iff v1 < v2</li>
+ *    <li>a positive integer iff v1 > v2</li>
+ *    <li>NaN if either version string is in the wrong format</li>
+ * </ul>
+ *
+ */
+Util.compareVersions = function (v1, v2, options) {
+  var lexicographical = options && options.lexicographical,
+      zeroExtend = options && options.zeroExtend,
+      v1parts = v1.split('.'),
+      v2parts = v2.split('.');
+
+  function isValidPart(x) {
+    return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x);
+  }
+
+  if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) {
+    return NaN;
+  }
+
+  if (zeroExtend) {
+    while (v1parts.length < v2parts.length) v1parts.push('0');
+    while (v2parts.length < v1parts.length) v2parts.push('0');
+  }
+
+  if (!lexicographical) {
+    v1parts = v1parts.map(Number);
+    v2parts = v2parts.map(Number);
+  }
+
+  for (var i = 0; i < v1parts.length; ++i) {
+    if (v2parts.length == i) {
+      return 1;
+    }
+
+    if (v1parts[i] == v2parts[i]) {
+      continue;
+    }
+    else if (v1parts[i] > v2parts[i]) {
+      return 1;
+    }
+    else {
+      return -1;
+    }
+  }
+
+  if (v1parts.length != v2parts.length) {
+    return -1;
+  }
+
+  return 0;
+};
+
diff --git a/meteor/server/apps.js b/meteor/server/apps.js
index af9383afa3..a8e7c8dce7 100755
--- a/meteor/server/apps.js
+++ b/meteor/server/apps.js
@@ -1,10 +1,55 @@
-removeBindFolder = function (name, callback) {
-  exec(path.join(getBinDir(), 'boot2docker') + ' ssh "sudo rm -rf /var/lib/docker/binds/' + name + '"', function(err, stdout) {
-    callback(err, stdout);
-  });
+Apps.restart = function (app, callback) {
+  if (app.docker && app.docker.Id) {
+    try {
+      Docker.restartContainerSync(app.docker.Id);
+    } catch (e) {
+      console.error(e);
+    }
+    var containerData = Docker.getContainerDataSync(app.docker.Id);
+    Fiber(function () {
+      Apps.update(app._id, {$set: {
+        status: 'READY',
+        docker: containerData
+      }});
+    }).run();
+    callback(null);
+    // Use dig to refresh the DNS
+    exec('/usr/bin/dig dig ' + app.name + '.dev @172.17.42.1 ', function() {});
+  } else {
+    callback(null);
+  }
 };
 
-recoverApps = function (callback) {
+Apps.logs = function (app) {
+  if (app.docker && app.docker.Id) {
+    var container = docker.getContainer(app.docker.Id);
+    container.logs({follow: false, stdout: true, stderr: true, timestamps: true, tail: 300}, function (err, response) {
+      if (err) { throw err; }
+      Fiber(function () {
+        Apps.update(app._id, {
+          $set: {
+            logs: []
+          }
+        });
+      }).run();
+      var logs = [];
+      response.setEncoding('utf8');
+      response.on('data', function (line) {
+        logs.push(convert.toHtml(line.slice(8)));
+        Fiber(function () {
+          Apps.update(app._id, {
+            $set: {
+              logs: logs
+            }
+          });
+        }).run();
+      });
+      response.on('end', function () {});
+    });
+  }
+};
+
+Apps.recover = function (callback) {
   var apps = Apps.find({}).fetch();
   _.each(apps, function (app) {
     // Update the app with the latest container info
@@ -17,7 +62,7 @@ recoverApps = function (callback) {
         console.log('restarting: ' + app.name);
         console.log(app.docker.Id);
         Fiber(function () {
-          restartApp(app, function (err) {
+          Apps.restart(app, function (err) {
             if (err) { console.error(err); }
           });
         }).run();
@@ -29,7 +74,8 @@ recoverApps = function (callback) {
 
 Meteor.methods({
   recoverApps: function () {
-    return Meteor._wrapAsync(recoverApps)();
+    this.unblock();
+    return Meteor._wrapAsync(Apps.recover)();
   },
   configVar: function (appId, configVars) {
     this.unblock();
@@ -48,64 +94,38 @@ Meteor.methods({
     if (!app) {
       throw new Meteor.Error(403, 'No app found with this ID');
     }
-    deleteApp(app, function (err) {
-      if (err) { console.error(err); }
-      var appPath = path.join(KITE_PATH, app.name);
-      deleteFolder(appPath);
-      removeBindFolder(app.name, function () {
-        console.log('Deleted Kite ' + app.name + ' directory.');
-        Fiber(function () {
-          Apps.remove({_id: app._id});
-        }).run();
-      });
-    });
+    Apps.remove({_id: app._id});
   },
   createApp: function (formData) {
+    this.unblock();
     var validationResult = formValidate(formData, FormSchema.formCreateApp);
     if (validationResult.errors) {
       throw new Meteor.Error(400, 'Validation Failed.', validationResult.errors);
     } else {
       var cleaned = validationResult.cleaned;
-      var appObj = {
-        name: cleaned.name,
-        imageId: cleaned.imageId,
-        status: 'STARTING',
-        config: {}
-      };
-      var appId = Apps.insert(appObj);
-      var appPath = path.join(KITE_PATH, appObj.name);
+      var appName = cleaned.name;
+      var appPath = path.join(KITE_PATH, appName);
       if (!fs.existsSync(appPath)) {
-        console.log('Created Kite ' + appObj.name + ' directory.');
+        console.log('Created Kite ' + appName + ' directory.');
         fs.mkdirSync(appPath, function (err) {
           if (err) { throw err; }
         });
       }
-      Apps.update(appId, {
-        $set: {
-          'config.APP_ID': appId,
-          path: appPath
-        }
-      });
-      var image = Images.findOne(appObj.imageId);
-      loadKiteVolumes(image.path, appObj.name);
-      var app = Apps.findOne(appId);
-      removeBindFolder(app.name, function (err) {
-        if (err) {
-          console.error(err);
-        }
-        Fiber(function () {
-          Meteor.call('runApp', app, function (err) {
-            if (err) { throw err; }
-          });
-        }).run();
-      });
+      var appObj = {
+        name: appName,
+        imageId: cleaned.imageId,
+        status: 'STARTING',
+        config: {},
+        path: appPath
+      };
+      Apps.insert(appObj);
     }
   },
   getAppLogs: function (appId) {
     this.unblock();
     var app = Apps.findOne(appId);
     if (app) {
-      getAppLogs(app, function (err) {
+      Apps.logs(app, function (err) {
         if (err) { throw err; }
       });
     }
@@ -117,12 +137,13 @@ Meteor.methods({
       Apps.update(app._id, {$set: {
         status: 'STARTING'
       }});
-      restartApp(app, function (err) {
+      Apps.restart(app, function (err) {
         if (err) { console.error(err); }
       });
     }
   },
   resolveWatchers: function () {
+    this.unblock();
     return Meteor._wrapAsync(resolveWatchers)();
   }
 });
diff --git a/meteor/server/constants.js b/meteor/server/constants.js
new file mode 100644
index 0000000000..cb35d90451
--- /dev/null
+++ b/meteor/server/constants.js
@@ -0,0 +1,24 @@
+KITE_PATH = path.join(Util.getHomePath(), 'Kitematic');
+KITE_TAR_PATH = path.join(KITE_PATH, '.tar');
+KITE_IMAGES_PATH = path.join(KITE_PATH, '.images');
+
+if (!fs.existsSync(KITE_PATH)) {
+  console.log('Created Kitematic directory.');
+  fs.mkdirSync(KITE_PATH, function (err) {
+    if (err) { throw err; }
+  });
+}
+
+if (!fs.existsSync(KITE_TAR_PATH)) {
+  console.log('Created Kitematic .tar directory.');
+  fs.mkdirSync(KITE_TAR_PATH, function (err) {
+    if (err) { throw err; }
+  });
+}
+
+if (!fs.existsSync(KITE_IMAGES_PATH)) {
+  console.log('Created Kitematic .images directory.');
+  fs.mkdirSync(KITE_IMAGES_PATH, function (err) {
+    if (err) { throw err; }
+  });
+}
diff --git a/meteor/server/docker.js b/meteor/server/docker.js
index d71b12688b..37a27d6d88 100755
--- a/meteor/server/docker.js
+++ b/meteor/server/docker.js
@@ -1,16 +1,11 @@
-Docker = Meteor.require('dockerode');
+Dockerode = Meteor.require('dockerode');
 
-var Convert = Meteor.require('ansi-to-html');
-var convert = new Convert();
+var DOCKER_HOST='192.168.60.103';
+docker = new Dockerode({host: DOCKER_HOST, port: '2375'});
 
-var DOCKER_HOST='192.168.59.103';
-docker = new Docker({host: '192.168.59.103', port: '2375'});
+Docker = {};
 
-hasDockerfile = function (directory) {
-  return fs.existsSync(path.join(directory, 'Dockerfile'));
-};
-
-removeContainer = function (containerId, callback) {
+Docker.removeContainer = function (containerId, callback) {
   var container = docker.getContainer(containerId);
   container.kill(function (err) {
     if (err) { callback(err); return; }
@@ -22,28 +17,11 @@ removeContainer = function (containerId, callback) {
   });
 };
 
-removeContainerSync = function (containerId) {
-  return Meteor._wrapAsync(removeContainer)(containerId);
+Docker.removeContainerSync = function (containerId) {
+  return Meteor._wrapAsync(Docker.removeContainer)(containerId);
 };
 
-deleteApp = function (app, callback) {
-  if (!app.docker) {
-    callback(null);
-    return;
-  }
-  try {
-    removeContainerSync(app.docker.Id);
-  } catch (e) {
-    console.error(e);
-  }
-  callback(null);
-};
-
-deleteAppSync = function (app) {
-  return Meteor._wrapAsync(deleteApp)(app);
-};
-
-getContainerData = function (containerId, callback) {
+Docker.getContainerData = function (containerId, callback) {
   var container = docker.getContainer(containerId);
   container.inspect(function (err, data) {
     if (err) {
@@ -59,11 +37,11 @@ getContainerData = function (containerId, callback) {
   });
 };
 
-getContainerDataSync = function (containerId) {
-  return Meteor._wrapAsync(getContainerData)(containerId);
+Docker.getContainerDataSync = function (containerId) {
+  return Meteor._wrapAsync(Docker.getContainerData)(containerId);
 };
 
-runContainer = function (app, image, callback) {
+Docker.runContainer = function (app, image, callback) {
   var envParam = [];
   _.each(_.keys(app.config), function (key) {
     var builtStr = key + '=' + app.config[key];
@@ -100,11 +78,11 @@ runContainer = function (app, image, callback) {
   });
 };
 
-runContainerSync = function (app, image) {
-  return Meteor._wrapAsync(runContainer)(app, image);
+Docker.runContainerSync = function (app, image) {
+  return Meteor._wrapAsync(Docker.runContainer)(app, image);
 };
 
-restartContainer = function (containerId, callback) {
+Docker.restartContainer = function (containerId, callback) {
   var container = docker.getContainer(containerId);
   container.restart(function (err) {
     if (err) {
@@ -117,83 +95,8 @@ restartContainer = function (containerId, callback) {
   });
 };
 
-restartContainerSync = function (containerId) {
-  return Meteor._wrapAsync(restartContainer)(containerId);
-};
-
-var getFromImage = function (dockerfile) {
-  var patternString = "(FROM)(.*)";
-  var regex = new RegExp(patternString, "g");
-  var fromInstruction = dockerfile.match(regex);
-  if (fromInstruction && fromInstruction.length > 0) {
-    return fromInstruction[0].replace('FROM', '').trim();
-  } else {
-    return null;
-  }
-};
-
-restartApp = function (app, callback) {
-  if (app.docker && app.docker.Id) {
-    try {
-      restartContainerSync(app.docker.Id);
-    } catch (e) {
-      console.error(e);
-    }
-    var containerData = getContainerDataSync(app.docker.Id);
-    Fiber(function () {
-      Apps.update(app._id, {$set: {
-        status: 'READY',
-        docker: containerData
-      }});
-    }).run();
-    callback(null);
-    // Use dig to refresh the DNS
-    exec('/usr/bin/dig dig ' + app.name + '.dev @172.17.42.1 ', function() {});
-  } else {
-    callback(null);
-  }
-};
-
-getAppLogs = function (app) {
-  if (app.docker && app.docker.Id) {
-    var container = docker.getContainer(app.docker.Id);
-    container.logs({follow: false, stdout: true, stderr: true, timestamps: true, tail: 300}, function (err, response) {
-      if (err) { throw err; }
-      Fiber(function () {
-        Apps.update(app._id, {
-          $set: {
-            logs: []
-          }
-        });
-      }).run();
-      var logs = [];
-      response.setEncoding('utf8');
-      response.on('data', function (line) {
-        logs.push(convert.toHtml(line.slice(8)));
-        Fiber(function () {
-          Apps.update(app._id, {
-            $set: {
-              logs: logs
-            }
-          });
-        }).run();
-      });
-      response.on('end', function () {});
-    });
-  }
-};
-
-createTarFile = function (image, callback) {
-  var TAR_PATH = path.join(KITE_TAR_PATH, image._id + '.tar');
-  exec('tar czf ' + TAR_PATH + ' -C ' + image.path + ' .', function (err) {
-    if (err) { callback(err, null); return; }
-    console.log('Created tar file: ' + TAR_PATH);
-    callback(null, TAR_PATH);
-  });
-};
-
-createTarFileSync = function (image) {
-  return Meteor._wrapAsync(createTarFile)(image);
+Docker.restartContainerSync = function (containerId) {
+  return Meteor._wrapAsync(Docker.restartContainer)(containerId);
 };
 
 var convertVolumeObjToArray = function (obj) {
@@ -209,7 +112,7 @@ var convertVolumeObjToArray = function (obj) {
   return result;
 };
 
-getImageData = function (imageId, callback) {
+Docker.getImageData = function (imageId, callback) {
   var image = docker.getImage(imageId.toLowerCase());
   image.inspect(function (err, data) {
     if (err) {
@@ -224,11 +127,11 @@ getImageData = function (imageId, callback) {
   });
 };
 
-getImageDataSync = function (imageId) {
-  return Meteor._wrapAsync(getImageData)(imageId);
+Docker.getImageDataSync = function (imageId) {
+  return Meteor._wrapAsync(Docker.getImageData)(imageId);
 };
 
-removeImage = function (imageId, callback) {
+Docker.removeImage = function (imageId, callback) {
   var image = docker.getImage(imageId.toLowerCase());
   image.remove({force: true}, function (err) {
     if (err) { callback(err); return; }
@@ -237,25 +140,14 @@ removeImage = function (imageId, callback) {
   });
 };
 
-removeImageSync = function (imageId) {
-  return Meteor._wrapAsync(removeImage)(imageId);
+Docker.removeImageSync = function (imageId) {
+  return Meteor._wrapAsync(Docker.removeImage)(imageId);
 };
 
-deleteImage = function (image, callback) {
-  if (!image.docker) {
-    callback(null, {});
-    return;
-  }
-  try {
-    removeImageSync(image.docker.Id);
-  } catch (e) {
-    console.error(e);
-  }
-  callback(null);
-};
-
-deleteImageSync = function (image) {
-  return Meteor._wrapAsync(deleteImage)(image);
+Docker.removeBindFolder = function (name, callback) {
+  exec(path.join(Util.getBinDir(), 'boot2docker') + ' ssh "sudo rm -rf /var/lib/docker/binds/' + name + '"', function (err, stdout) {
+    callback(err, stdout);
+  });
 };
 
 var defaultContainerOptions = function () {
@@ -304,7 +196,7 @@ resolveDefaultImages = function () {
     image.inspect(function (err) {
       if (err) {
         if (err.reason === 'no such image') {
-          docker.loadImage(path.join(getBinDir(), 'base-images.tar.gz'), {}, function (err) {
+          docker.loadImage(path.join(Util.getBinDir(), 'base-images.tar.gz'), {}, function (err) {
             if (err) {
               innerCallback(err);
               return;
@@ -374,7 +266,7 @@ reloadDefaultContainers = function (callback) {
   async.until(function () {
     return ready;
   }, function (callback) {
-    docker.listContainers(function (err, containers) {
+    docker.listContainers(function (err) {
       if (!err) {
         ready = true;
       }
@@ -391,7 +283,7 @@ reloadDefaultContainers = function (callback) {
         return;
       }
       console.log('Loading new Kitematic default images.');
-      docker.loadImage(path.join(getBinDir(), 'base-images.tar.gz'), {}, function (err) {
+      docker.loadImage(path.join(Util.getBinDir(), 'base-images.tar.gz'), {}, function (err) {
         if (err) {
           callback(err);
           return;
@@ -500,142 +392,18 @@ killAndRemoveContainers = function (names, callback) {
   });
 };
 
-pullImageFromDockerfile = function (dockerfile, imageId, callback) {
-  var fromImage = getFromImage(dockerfile);
-  console.log('From image: ' + fromImage);
-  var installedImage = null;
-  try {
-    installedImage = getImageDataSync(fromImage);
-  } catch (e) {
-    console.error(e);
-  }
-  if (fromImage && !installedImage) {
-    Fiber(function () {
-      Images.update(imageId, {
-        $set: {
-          buildLogs: []
-        }
-      });
-    }).run();
-    var logs = [];
-    docker.pull(fromImage, function (err, response) {
-      if (err) { callback(err); return; }
-      response.setEncoding('utf8');
-      response.on('data', function (data) {
-        try {
-          var logData = JSON.parse(data);
-          var logDisplay = '';
-          if (logData.id) {
-            logDisplay += logData.id + ' | ';
-          }
-          logDisplay += logData.status;
-          if (logData.progressDetail && logData.progressDetail.current && logData.progressDetail.total) {
-            logDisplay += ' - ' + Math.round(logData.progressDetail.current / logData.progressDetail.total * 100) + '%';
-          }
-          logs.push(logDisplay);
-          Fiber(function () {
-            Images.update(imageId, {
-              $set: {
-                buildLogs: logs
-              }
-            });
-          }).run();
-        } catch (e) {
-          console.error(e);
-        }
-      });
-      response.on('end', function () {
-        console.log('Finished pulling image: ' + fromImage);
-        callback(null);
-      });
-    });
-  } else {
-    callback(null);
-  }
-};
-
-buildImage = function (image, callback) {
-  Fiber(function () {
-    var tarFilePath = createTarFileSync(image);
-    Images.update(image._id, {
-      $set: {
-        buildLogs: []
-      }
-    });
-    docker.buildImage(tarFilePath, {t: image._id.toLowerCase()}, function (err, response) {
-      if (err) { callback(err); }
-      console.log('Building Docker image...');
-      var logs = [];
-      response.setEncoding('utf8');
-      response.on('data', function (data) {
-        try {
-          var line = JSON.parse(data).stream;
-          logs.push(convert.toHtml(line));
-          Fiber(function () {
-            Images.update(image._id, {
-              $set: {
-                buildLogs: logs
-              }
-            });
-          }).run();
-        } catch (e) {
-          console.error(e);
-        }
-      });
-      response.on('end', function () {
-        console.log('Finished building Docker image.');
-        try {
-          fs.unlinkSync(tarFilePath);
-          console.log('Cleaned up tar file.');
-        } catch (e) {
-          console.error(e);
-        }
-        Fiber(function () {
-          var imageData = null;
-          try {
-            imageData = getImageDataSync(image._id);
-            Images.update(image._id, {
-              $set: {
-                docker: imageData,
-                status: 'READY'
-              }
-            });
-          } catch (e) {
-            console.log(e);
-            Images.update(image._id, {
-              $set: {
-                status: 'ERROR'
-              }
-            });
-          }
-          var oldImageId = null;
-          if (image.docker && image.docker.Id) {
-            oldImageId = image.docker.Id;
-          }
-          if (oldImageId && imageData && oldImageId !== imageData.Id) {
-            try {
-              removeImageSync(oldImageId);
-            } catch (e) {
-              console.error(e);
-            }
-          }
-        }).run();
-        callback(null);
-      });
-    });
-  }).run();
-};
-
 Meteor.methods({
   runApp: function (app) {
     this.unblock();
     var image = Images.findOne({_id: app.imageId});
+    // Delete old container if one already exists
     try {
-      removeContainerSync(app.name);
+      Docker.removeContainerSync(app.name);
     } catch (e) {}
     try {
-      var container = runContainerSync(app, image);
-      var containerData = getContainerDataSync(container.id);
+      var container = Docker.runContainerSync(app, image);
+      var containerData = Docker.getContainerDataSync(container.id);
+      // Set a delay for app to spin up
       Meteor.setTimeout(function () {
         Apps.update(app._id, {$set: {
           docker: containerData,
@@ -650,18 +418,23 @@ Meteor.methods({
     return DOCKER_HOST;
   },
   reloadDefaultContainers: function () {
+    this.unblock();
     return Meteor._wrapAsync(reloadDefaultContainers)();
   },
   checkDefaultImages: function () {
+    this.unblock();
     return Meteor._wrapAsync(checkDefaultImages)();
   },
   resolveDefaultImages: function () {
+    this.unblock();
     return Meteor._wrapAsync(resolveDefaultImages)();
   },
   checkDefaultContainers: function () {
+    this.unblock();
     return Meteor._wrapAsync(checkDefaultContainers)();
   },
   resolveDefaultContainers: function () {
+    this.unblock();
     return Meteor._wrapAsync(resolveDefaultContainers)();
   }
 });
diff --git a/meteor/server/images.js b/meteor/server/images.js
index 5429dd53b1..6ad43fe7d0 100755
--- a/meteor/server/images.js
+++ b/meteor/server/images.js
@@ -1,4 +1,38 @@
-getImageMetaData = function (directory) {
+var createTarFile = function (image, callback) {
+  var TAR_PATH = path.join(KITE_TAR_PATH, image._id + '.tar');
+  exec('tar czf ' + TAR_PATH + ' -C ' + image.path + ' .', function (err) {
+    if (err) { callback(err, null); return; }
+    console.log('Created tar file: ' + TAR_PATH);
+    callback(null, TAR_PATH);
+  });
+};
+
+var createTarFileSync = function (image) {
+  return Meteor._wrapAsync(createTarFile)(image);
+};
+
+var getFromImage = function (dockerfile) {
+  var patternString = "(FROM)(.*)";
+  var regex = new RegExp(patternString, "g");
+  var fromInstruction = dockerfile.match(regex);
+  if (fromInstruction && fromInstruction.length > 0) {
+    return fromInstruction[0].replace('FROM', '').trim();
+  } else {
+    return null;
+  }
+};
+
+var getImageJSON = function (directory) {
+  var KITE_JSON_PATH = path.join(directory, 'image.json');
+  if (fs.existsSync(KITE_JSON_PATH)) {
+    var data = fs.readFileSync(KITE_JSON_PATH, 'utf8');
+    return JSON.parse(data);
+  } else {
+    return null;
+  }
+};
+
+var getImageMetaData = function (directory) {
   var kiteJSON = getImageJSON(directory);
   if (kiteJSON) {
     if (!kiteJSON.name) {
@@ -12,8 +46,24 @@ getImageMetaData = function (directory) {
   return kiteJSON;
 };
 
-rebuildImage = function (image, callback) {
-  deleteFolder(image.path);
+Images.saveFolder = function (directory, imageId, callback) {
+  var destinationPath = path.join(KITE_IMAGES_PATH, imageId);
+  if (!fs.existsSync(destinationPath)) {
+    fs.mkdirSync(destinationPath, function (err) {
+      if (err) { callback(err); return; }
+    });
+    Util.copyFolder(directory, destinationPath);
+    console.log('Copied image folder for: ' + imageId);
+    callback(null);
+  }
+};
+
+Images.saveFolderSync = function (directory, imageId) {
+  return Meteor._wrapAsync(Images.saveFolder)(directory, imageId);
+};
+
+Images.rebuild = function (image, callback) {
+  Util.deleteFolder(image.path);
   var imageMetaData = getImageMetaData(image.originPath);
   if (imageMetaData.logo) {
     Images.update(image._id, {
@@ -35,18 +85,144 @@ rebuildImage = function (image, callback) {
     }
   });
   image = Images.findOne(image._id);
-  saveImageFolderSync(image.originPath, image._id);
-  pullImageFromDockerfile(fs.readFileSync(path.join(image.path, 'Dockerfile'), 'utf8'), image._id, function (err) {
+  Images.saveFolderSync(image.originPath, image._id);
+  Images.pull(fs.readFileSync(path.join(image.path, 'Dockerfile'), 'utf8'), image._id, function (err) {
     if (err) { callback(err, null); return; }
-    buildImage(image, function (err) {
+    Images.build(image, function (err) {
       if (err) { console.error(err); }
       callback(null, null);
     });
   });
 };
 
-rebuildImageSync = function (image) {
-  return Meteor._wrapAsync(rebuildImage)(image);
+Images.rebuildSync = function (image) {
+  return Meteor._wrapAsync(Images.rebuild)(image);
+};
+
+Images.pull = function (dockerfile, imageId, callback) {
+  var fromImage = getFromImage(dockerfile);
+  console.log('From image: ' + fromImage);
+  var installedImage = null;
+  try {
+    installedImage = Docker.getImageDataSync(fromImage);
+  } catch (e) {
+    console.error(e);
+  }
+  if (fromImage && !installedImage) {
+    Fiber(function () {
+      Images.update(imageId, {
+        $set: {
+          buildLogs: []
+        }
+      });
+    }).run();
+    var logs = [];
+    docker.pull(fromImage, function (err, response) {
+      if (err) { callback(err); return; }
+      response.setEncoding('utf8');
+      response.on('data', function (data) {
+        try {
+          var logData = JSON.parse(data);
+          var logDisplay = '';
+          if (logData.id) {
+            logDisplay += logData.id + ' | ';
+          }
+          logDisplay += logData.status;
+          if (logData.progressDetail && logData.progressDetail.current && logData.progressDetail.total) {
+            logDisplay += ' - ' + Math.round(logData.progressDetail.current / logData.progressDetail.total * 100) + '%';
+          }
+          logs.push(logDisplay);
+          Fiber(function () {
+            Images.update(imageId, {
+              $set: {
+                buildLogs: logs
+              }
+            });
+          }).run();
+        } catch (e) {
+          console.error(e);
+        }
+      });
+      response.on('end', function () {
+        console.log('Finished pulling image: ' + fromImage);
+        callback(null);
+      });
+    });
+  } else {
+    callback(null);
+  }
+};
+
+Images.build = function (image, callback) {
+  Fiber(function () {
+    var tarFilePath = createTarFileSync(image);
+    Images.update(image._id, {
+      $set: {
+        buildLogs: []
+      }
+    });
+    docker.buildImage(tarFilePath, {t: image._id.toLowerCase()}, function (err, response) {
+      if (err) { callback(err); }
+      console.log('Building Docker image...');
+      var logs = [];
+      response.setEncoding('utf8');
+      response.on('data', function (data) {
+        try {
+          var line = JSON.parse(data).stream;
+          logs.push(convert.toHtml(line));
+          Fiber(function () {
+            Images.update(image._id, {
+              $set: {
+                buildLogs: logs
+              }
+            });
+          }).run();
+        } catch (e) {
+          console.error(e);
+        }
+      });
+      response.on('end', function () {
+        console.log('Finished building Docker image.');
+        try {
+          fs.unlinkSync(tarFilePath);
+          console.log('Cleaned up tar file.');
+        } catch (e) {
+          console.error(e);
+        }
+        Fiber(function () {
+          var imageData = null;
+          try {
+            imageData = Docker.getImageDataSync(image._id);
+            Images.update(image._id, {
+              $set: {
+                docker: imageData,
+                status: 'READY'
+              }
+            });
+          } catch (e) {
+            console.log(e);
+            Images.update(image._id, {
+              $set: {
+                status: 'ERROR'
+              }
+            });
+          }
+          var oldImageId = null;
+          if (image.docker && image.docker.Id) {
+            oldImageId = image.docker.Id;
+          }
+          if (oldImageId && imageData && oldImageId !== imageData.Id) {
+            try {
+              Docker.removeImageSync(oldImageId);
+            } catch (e) {
+              console.error(e);
+            }
+          }
+        }).run();
+        callback(null);
+      });
+    });
+  }).run();
 };
 
 Meteor.methods({
@@ -58,29 +234,7 @@ Meteor.methods({
     };
     var imageMetaData = getImageMetaData(directory);
     imageObj.meta = imageMetaData;
-    var imageId = Images.insert(imageObj);
-    var imagePath = path.join(KITE_IMAGES_PATH, imageId);
-    Images.update(imageId, {
-      $set: {
-        path: imagePath
-      }
-    });
-    if (imageObj.meta.logo) {
-      Images.update(imageId, {
-        $set: {
-          logoPath: path.join(imagePath, imageObj.meta.logo)
-        }
-      });
-    }
-    var image = Images.findOne(imageId);
-    saveImageFolderSync(directory, imageId);
-    console.log('Saved folder sync');
-    pullImageFromDockerfile(fs.readFileSync(path.join(image.path, 'Dockerfile'), 'utf8'), imageId, function (err) {
-      if (err) { throw err; }
-      buildImage(image, function (err) {
-        if (err) { console.error(err); }
-      });
-    });
+    Images.insert(imageObj);
   },
   rebuildImage: function (imageId) {
     this.unblock();
@@ -92,7 +246,13 @@ Meteor.methods({
     if (apps.length > 0) {
       _.each(apps, function (app) {
         console.log('Updating app: ' + app.name);
-        deleteAppSync(app);
+        if (app.docker) {
+          try {
+            Docker.removeContainerSync(app.docker.Id);
+          } catch (e) {
+            console.error(e);
+          }
+        }
         Apps.update(app._id, {
           $set: {
             'docker.Id': null,
@@ -101,7 +261,7 @@ Meteor.methods({
           }
         });
       });
-      rebuildImageSync(image);
+      Images.rebuildSync(image);
       _.each(apps, function (app) {
         app = Apps.findOne(app._id);
         Meteor.call('runApp', app, function (err) {
@@ -109,7 +269,7 @@ Meteor.methods({
         });
       });
     } else {
-      rebuildImageSync(image);
+      Images.rebuildSync(image);
     }
   },
   changeDirectory: function (imageId, directory) {
@@ -125,7 +285,8 @@ Meteor.methods({
     });
   },
   validateDirectory: function (directory) {
-    if (!hasDockerfile(directory)) {
+    this.unblock();
+    if (!Util.hasDockerfile(directory)) {
       throw new Meteor.Error(400, "Only directories with Dockerfiles are supported now.");
     }
   },
@@ -137,15 +298,7 @@ Meteor.methods({
     }
     var app = Apps.findOne({imageId: imageId});
     if (!app) {
-      console.log('here');
-      try {
-        deleteImageSync(image);
-        deleteFolder(image.path);
-      } catch (e) {
-        console.log(e);
-      } finally {
-        Images.remove({_id: image._id});
-      }
+      Images.remove({_id: image._id});
     } else {
       throw new Meteor.Error(400, 'This image is currently being used by <a href="/apps/' + app.name + '">' + app.name + "</a>.");
     }
diff --git a/meteor/server/kite.js b/meteor/server/kite.js
deleted file mode 100755
index 712635616a..0000000000
--- a/meteor/server/kite.js
+++ /dev/null
@@ -1,59 +0,0 @@
-KITE_PATH = path.join(getHomePath(), 'Kitematic');
-KITE_TAR_PATH = path.join(KITE_PATH, '.tar');
-KITE_IMAGES_PATH = path.join(KITE_PATH, '.images');
-
-if (!fs.existsSync(KITE_PATH)) {
-  console.log('Created Kitematic directory.');
-  fs.mkdirSync(KITE_PATH, function (err) {
-    if (err) { throw err; }
-  });
-}
-
-if (!fs.existsSync(KITE_TAR_PATH)) {
-  console.log('Created Kitematic .tar directory.');
-  fs.mkdirSync(KITE_TAR_PATH, function (err) {
-    if (err) { throw err; }
-  });
-}
-
-if (!fs.existsSync(KITE_IMAGES_PATH)) {
-  console.log('Created Kitematic .images directory.');
-  fs.mkdirSync(KITE_IMAGES_PATH, function (err) {
-    if (err) { throw err; }
-  });
-}
-
-getImageJSON = function (directory) {
-  var KITE_JSON_PATH = path.join(directory, 'image.json');
-  if (fs.existsSync(KITE_JSON_PATH)) {
-    var data = fs.readFileSync(KITE_JSON_PATH, 'utf8');
-    return JSON.parse(data);
-  } else {
-    return null;
-  }
-};
-
-loadKiteVolumes = function (directory, appName) {
-  var KITE_VOLUMES_PATH = path.join(directory, 'volumes');
-  if (fs.existsSync(KITE_VOLUMES_PATH)) {
-    var destinationPath = path.join(KITE_PATH, appName);
-    copyFolder(KITE_VOLUMES_PATH, destinationPath);
-    console.log('Copied volumes for: ' + appName);
-  }
-};
-
-saveImageFolder = function (directory, imageId, callback) {
-  var destinationPath = path.join(KITE_IMAGES_PATH, imageId);
-  if (!fs.existsSync(destinationPath)) {
-    fs.mkdirSync(destinationPath, function (err) {
-      if (err) { callback(err); return; }
-    });
-    copyFolder(directory, destinationPath);
-    console.log('Copied image folder for: ' + imageId);
-    callback(null);
-  }
-};
-
-saveImageFolderSync = function (directory, imageId) {
-  return Meteor._wrapAsync(saveImageFolder)(directory, imageId);
-};
diff --git a/meteor/server/lib/requires.js b/meteor/server/lib/requires.js
index cac98d255a..6174cd0202 100755
--- a/meteor/server/lib/requires.js
+++ b/meteor/server/lib/requires.js
@@ -6,4 +6,6 @@ path = Meteor.require('path');
 exec = Meteor.require('child_process').exec;
 async = Meteor.require('async');
 Fiber = Meteor.require('fibers');
-child_process = Meteor.require('child_process');
\ No newline at end of file
+child_process = Meteor.require('child_process');
+Convert = Meteor.require('ansi-to-html');
+convert = new Convert();
diff --git a/meteor/server/lib/utilities.js b/meteor/server/lib/utilities.js
deleted file mode 100755
index 6b4d68df1a..0000000000
--- a/meteor/server/lib/utilities.js
+++ /dev/null
@@ -1,53 +0,0 @@
-getHomePath = function () {
-  return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
-};
-
-getBinDir = function () {
-  if (process.env.NODE_ENV === 'development') {
-    return path.join(path.join(process.env.PWD, '..'), 'resources');
-  } else {
-    return path.join(process.cwd(), '../../../resources');
-  }
-};
-
-deleteFolder = function (directory) {
-  if (fs.existsSync(directory)) {
-    fs.readdirSync(directory).forEach(function (file) {
-      var curDirectory = directory + '/' + file;
-      if (fs.lstatSync(curDirectory).isDirectory()) {
-        // Recurse
-        deleteFolder(curDirectory);
-      } else {
-        // Delete File
-        try {
-          fs.unlinkSync(curDirectory);
-        } catch (e) {
-          console.error(e);
-        }
-      }
-    });
-    fs.rmdirSync(directory);
-  }
-};
-
-copyFolder = function (src, dest) {
-  var exists = fs.existsSync(src);
-  var stats = exists && fs.statSync(src);
-  var isDirectory = exists && stats.isDirectory();
-  if (exists && isDirectory) {
-    try {
-      fs.mkdirSync(dest);
-    } catch (e) {
-      console.error(e);
-    }
-    fs.readdirSync(src).forEach(function (childItemName) {
-      copyFolder(path.join(src, childItemName), path.join(dest, childItemName));
-    });
-  } else {
-    try {
-      fs.linkSync(src, dest);
-    } catch (e) {
-      console.error(e);
-    }
-  }
-};
diff --git a/meteor/server/publications.js b/meteor/server/publications.js
index fe75a8a9ba..6e3540fc61 100755
--- a/meteor/server/publications.js
+++ b/meteor/server/publications.js
@@ -7,5 +7,5 @@ Meteor.publish('images', function () {
 });
 
 Meteor.publish('installs', function () {
-  return Installs.find({}, {sort: {createdAt: -1}});
+  return Installs.find({});
 });
diff --git a/resources/install b/resources/install
index 723ff280d1..6f8f7c122f 100755
--- a/resources/install
+++ b/resources/install
@@ -1,45 +1,39 @@
 #!/bin/sh
 
-# Lookup the Kitematic VM resolver for .dev domains
+# This script must be run as root and sets up Mac OS X to route all .kite domains to the virtual box VM with the name
+# 'boot2docker-vm'. It does the following:
+# 1) Adds a file under /etc/resolver/kite
+# 2) Sets up a LaunchAgent for adding entries to the route table to route all requests to the Docker subnet (172.17.0.0/16)
+# And expects the $IFNAME variable to contain the interface on which to send traffic to the boot2docker VM.
+
 mkdir -p /etc/resolver
-echo "nameserver 172.17.42.1" > /etc/resolver/dev
+echo "nameserver 172.17.42.1" > /etc/resolver/kite
 
 DIR=$(dirname "$0")
 USER=`w -h | sort -u -t' ' -k1,1 | awk '{print $1}'`
 
 /bin/rm -rf /Library/LaunchAgents/com.kitematic.route.plist
-
-echo '<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
+echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
+<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
+<plist version=\"1.0\">
 <dict>
   <key>Label</key>
   <string>com.kitematic.route</string>
   <key>ProgramArguments</key>
   <array>
-    <string>/sbin/route</string>
-    <string>-n</string>
-    <string>add</string>
-    <string>172.17.0.0/16</string>
-    <string>192.168.59.103</string>
+    <string>bash</string>
+    <string>-c</string>
+    <string>/usr/sbin/scutil -w State:/Network/Interface/$IFACE/IPv4;/sbin/route -n add -net 172.17.0.0 -netmask 255.255.0.0 -gateway $GATEWAY</string>
   </array>
   <key>KeepAlive</key>
   <false/>
   <key>RunAtLoad</key>
   <true/>
-  <key>ServiceIPC</key>
-  <false/>
-  <key>UserName</key>
-  <string>root</string>
   <key>LaunchOnlyOnce</key>
   <true/>
 </dict>
-</plist>' > /Library/LaunchAgents/com.kitematic.route.plist
-
-sudo -u $USER $DIR/boot2docker init
-VBoxManage modifyvm boot2docker-vm --nic2 hostonly --nictype2 virtio --cableconnected2 on --hostonlyadapter2 vboxnet0
-VBoxManage dhcpserver add --netname=vboxnet0 --ip=192.168.59.99 --netmask=255.255.255.0 --lowerip=192.168.59.103 --upperip=192.168.59.103
+</plist>" > /Library/LaunchAgents/com.kitematic.route.plist
 
 # Add entries to routing table for Kitematic VM
-/sbin/route delete 172.17.0.0/16 192.168.59.103
-/sbin/route -n add 172.17.0.0/16 192.168.59.103
\ No newline at end of file
+/sbin/route delete -net 172.17.0.0 -netmask 255.255.0.0 -gateway $GATEWAY
+/sbin/route -n add -net 172.17.0.0 -netmask 255.255.0.0 -gateway $GATEWAY
\ No newline at end of file
diff --git a/resources/kite-dns/dnsmasq.tmpl b/resources/kite-dns/dnsmasq.tmpl
index 546fda7910..0355b257cc 100644
--- a/resources/kite-dns/dnsmasq.tmpl
+++ b/resources/kite-dns/dnsmasq.tmpl
@@ -1,6 +1,6 @@
 user=root
 {{ range $index, $value := $ }}
   {{ with $address := index $value.Addresses 0 }}
-address=/{{$value.Name}}.dev/{{$address.IP}}
+address=/{{$value.Name}}.kite/{{$address.IP}}
   {{ end }}
 {{ end }}
diff --git a/script/setup.sh b/script/setup.sh
index c40c686e78..217e096da2 100755
--- a/script/setup.sh
+++ b/script/setup.sh
@@ -15,6 +15,7 @@ pushd cache
 if [ ! -f $BASE_IMAGE_VERSION_FILE ]; then
   cecho "-----> Downloading Kitematic base images..." $purple
   curl -L --progress-bar -o $BASE_IMAGE_VERSION_FILE https://s3.amazonaws.com/kite-installer/$BASE_IMAGE_VERSION_FILE
+  cp $BASE_IMAGE_VERSION_FILE ../resources/$BASE_IMAGE_FILE
 fi
 
 if [ ! -f $BOOT2DOCKER_CLI_VERSION_FILE ]; then
@@ -60,11 +61,6 @@ if [ ! -f $COCOASUDO_FILE ]; then
   chmod +x $COCOASUDO_FILE
 fi
 
-
-if [ ! -f $BASE_IMAGE_FILE ]; then
-  cp ../cache/$BASE_IMAGE_VERSION_FILE $BASE_IMAGE_FILE
-fi
-
 cp ../cache/$BOOT2DOCKER_CLI_VERSION_FILE $BOOT2DOCKER_CLI_FILE
 chmod +x $BOOT2DOCKER_CLI_FILE
 
diff --git a/script/versions.sh b/script/versions.sh
index f1f7853a2f..e46149f22c 100644
--- a/script/versions.sh
+++ b/script/versions.sh
@@ -1,4 +1,4 @@
-BASE_IMAGE_VERSION=0.0.1
+BASE_IMAGE_VERSION=0.0.2
 BASE_IMAGE_VERSION_FILE=base-images-$BASE_IMAGE_VERSION.tar.gz
 BASE_IMAGE_FILE=base-images.tar.gz