Skip to content

Instantly share code, notes, and snippets.

@leviwheatcroft
Last active October 10, 2015 00:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save leviwheatcroft/d57777ed9d49a9a285a9 to your computer and use it in GitHub Desktop.
Save leviwheatcroft/d57777ed9d49a9a285a9 to your computer and use it in GitHub Desktop.
guide to creating / configuring kestonejs heroku projects

overview

objectives

  • create a project we can develop locally and then push to heroku
  • automate deployment as much as is practical
  • avoid storing built resources in master branch
  • avoid storing development npm modules in production
  • configure the project to behave appropriately in each environment

a word about your environment

Now, as an idiot migrating from php/apache stacks this bit was quite foreign to me, but I'm sure it was a snap for you guru's. Much of your app's configuration is going to be done through environment variables. So when your app looks for a database connection, it's going to check the MONGOHQ_URL environment variable for a connection url. Heroku does a great job of managing your environment variables. In other environments, like your local host, you're going to need to deal with that yourself by using the /.env file. When you ran yo keystone, your /.env would have been populated with the environment variables for the third party services you chose. it's worth pointing out that /.env is .gitignored, so you might want to copy /.env to /.env.backup or something which is stored in the git repo just so you can get your keystone instance working on any other machine you might use.

Most of the stuff you're doing in this guide is discerning between your development and your production environments, and configuring keystone to behave correctly in each. Beyond that, the build script and git structure is the bridge between those two environments.

where to build

when you push to heroku it's fairly easy to have heroku run the build script, but there's a number of disadvantages to this. It's not easy to figure out what's gone wrong if your build script shits itself, and you have to put all your dev packages in the production area of your package.json

So you can build locally, but putting built resources in your git development branch can make merges a bit of a disaster.

The best solution I can find is to build in a separate branch, and rebase that branch as required. I can't take the credit for this, because I saw it in a blog post by Shane Stillwell

keystonejs settings

the keystone.js file in your project root does your bootstrapping / configuration stuff. In this document I'm just going to show settings as keystone.set('key', 'value'); because that's much easier than trying to explain what to remove or edit. These lines should be put in your keystone.js file right after the keystone.init() call. Of course, you'll be able to edit your keystone.init() call itself to make things much tidier.

the process

prerequisites

  • node
  • npm
  • heroku toolbelt

note that you don't need mongodb

the basics

There's no magic here, we're just going to make a project as per the keystone / git / heroku guides.

npm install -g generator-keystone
mkdir my-project
cd my-project
yo keystone

Choose grunt as task runner, otherwise defaults are ok / straight forward. Once you're done, don't run node keystone just yet, because we haven't set up our db.

Now to make a git repo

git init
git add .
git commit -m "initial commit"

Next up, heroku.

heroku login
heroku keys:add
heroku create

I've deliberately reserved the awesome for later, so right now you've just created a vanilla keystonejs project, git repo, and provisioned a heroku app.

database connection

Now, the first thing we want to do is create the db, rather than allowing keystone to write to a local mongodb, which we would have to transfer later, we may as well provision the heroku mongohq addon and start using that from the outset.

heroku addons:add mongohq
heroku config:get MONGOHQ_URL

if you've not used heroku addons before you might get an error when you try to add mongohq about verifying your account. Log in to heroku and add your credit card details.

This will print the url to your console, you need to paste that into your .env file with MONGOHQ_URL= in front of it, so it looks something like this:

MONGOHQ_URL=mongodb://heroku:lfYrQi9tQEZSmliGfE9Hjk34q8vDA0i61P0EDUiL-S3IYeMtndqwiOQ98B730formike7_0PU0bf16Ao8lQ@kahana.mongohq.com:10098/app30175691

because your .env is .gitignored you should probably make a copy of it to store in the repo

cp ./.env ./.env.backup

We also need to configure keystonejs to use that environment variable as your mongo string, so edit your keystone.js file ala:

keystone.set('mongo', process.env.MONGOHQ_URL);

configure your public folders

We're going to put our development stuff in ./publicDev, and then later copy/contact/uglify/less those resources to ./public in our build script. This keeps things nice and tidy and avoids committing built resources to your development branches.

mv ./public ./publicDev
mkdir public
touch ./public/.gitkeep
echo public >> .gitignore
echo !public/.gitkeep >> .gitignore

you may want to just edit your .gitignore file rather than echoing lines to it so you can keep it nice & pretty.

configure keystonejs to use the correct folder

if (process.env.NODE_ENV == 'development') {
  keystone.set('static', 'publicDev');
  keystone.set('less', 'publicDev');
  keystone.set('favicon', 'publicDev/favicon.ico');
}

Note that we're using the environment variable NODE_ENV to detect which environment we're in. So we also need to configure that. NODE_ENV defaults to development if it's unset. So we need to set that in heroku

heroku config:set NODE_ENV=production

Your localhost is going to default to development, but if you wanted to change that so you can check things are working correctly in the production environment, you'd just have set NODE_ENV=production in your ./.env file.

deal with sessions

By default keystonejs is going to use some sort of memory based session store, so you probably want to configure something else for production environments. You can use the if (process.env.NODE_ENV == 'development') {} block from the previous step, but basically you need something like this:

if (process.env.NODE_ENV == 'development') {
  keystone.set('session store', 'mongo');
}

depending on the options you chose in the wizard, 'sessions' may be false, make sure it's set to true

configure grunt watch task to use the correct folder

open up your Gruntfile.js and have a look at the grunt.initConfig() object. By default the watch.files array looks like this:

  [
    'public/styles/**/*.css',
    'public/styles/**/*.less',
    'templates/**/*.jade',
    'node_modules/keystone/templates/**/*.jade'
  ],

you need to change both instances of public/ to publicDev/.

jade template

There's loads of npm modules which will handle this kind of stuff for you, but personally I think a simple jade conditional is nice and clean. We're just going to serve different resources depending on NODE_ENV.

If you really want a fancy module, have a look at sails linker, file blocks, or usemin.

if (process.env.NODE_ENV == 'production') 
  script(type='text/javascript' src='/js/main.js')
else 
  script(type='text/javascript' src='/js/jquery-1.7.2.js')
  script(type='text/javascript' src='/js/utilities.js')
  script(type='text/javascript' src='/js/main.js')

and now the build script

first install the modules we're going to use

npm install grunt-contrib-clean grunt-contrib-concat grunt-contrib-uglify grunt-contrib-less grunt-contrib-copy --save-dev

We all know and love the dogma-free nature of grunt and node in general, so consider the following to be an example that works, rather than "how it should be".. these members go in the grunt.init() object:

    clean: {
      build: ["public"]
    },
    concat: {
      options: {
        banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
      },
      build: {
        src: 'publicDev/js/*.js',
        dest: 'publicDev/js/<%= pkg.name %>.concat'
      }
    },
    uglify: {
      options: {
        // the banner is inserted at the top of the output
        banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n'
      },
      build: {
        dest: 'public/js/<%= pkg.name %>.min.js',
        src: '<%= concat.build.dest %>'
      }
    },
    less: {
      options: {
        banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n',
        cleancss: true
      },
      build: {
        files: {
          'public/styles/site.min.css': 'publicDev/styles/site.less'
        }
      }
    },
    copy: {
      build: {
        files: [{
          expand: true,
          cwd: 'publicDev',
          src: [
            'fonts/**',
            'ico/**',
            'images/**'
          ],
          dest: 'public'
        }]
      }
    },

and then create the task itself

  grunt.registerTask('build', function(target) {
    grunt.task.run([
      'clean',
      'concat',
      'uglify',
      'less',
      'copy:build'

    ]);
  });

branching, building, deploying

For this we pretty much just follow the Shane Stillwell blog post I mentioned earlier. Basically, we make a branch and force that branch to include the built resources we've excluded from the master / development branches, then push that to heroku.

the first time:

git add --all .
git commit -m "added grunt build task"
git checkout -b heroku
grunt build
git add -f ./public
git push heroku dist:master --force

subsequent updates:

git add --all .
git commit -m "shizzled my nizzle"
git checkout heroku
git rebase master
grunt build
git add -f ./public
git commit -m "added built resources"
git push heroku heroku:master --force

there are grunt plugins which can handle this git stuff for you, personally I prefer to do it manually because meh.

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