The Ghost in the Machine: Setting up a Ghost blog on Heroku

Heroku is not officially supported for hosting a Ghost blog. There are plenty of other blog posts out there about how to set up a Ghost blog on Heroku, and for the most part, they are useful. I used several different ones when setting up this blog. However, there were some holes and, frankly, things that just didn't work that I discovered when following the steps outlined in other blogs. So I'm here to set the record straight . . . or, straight-ish. My guess is that my challenges were due primarily to the version of Ghost I was using (0.5.8 for reference), and if you're using a different version, you may encounter an entirely different set of challenges.

For the record, here are some of the other blogs/tutorials I looked at when getting started:

Host a Ghost blog on Heroku - My primary source of information (at least until I got stuck).

Ghost on Heroku - Has additional information on swapping file storage with s3.

Ghost for Beginners - Tons of great resources, from adding a menu, to installing disqus, to using Google Analytics

Table of Contents

Installing Ghost Locally
Putting Ghost on Heroku
Adding a Mail Server
Adding Syntax Highlighting
Adding Disqus

Installing Ghost Locally

I use git regularly, so I'm comfortable with it, and, since Heroku itself is a git-based deployment model, it felt natural to install Ghost using git. But I wanted to maintain the ability to upgrade via merging, without dealing with tons of conflicts. So I ran the following commands:

  1. git clone ghost
  2. cd ghost
  3. git checkout -b stable
  4. After creating a new repo on my on my own github account called "ghost", git remote set-url origin
  5. git push origin master
  6. git push origin stable - (my push.default is configured to upstream, so pushing multiple branches requires separate steps)
  7. git remote add upstream
  8. git remote add heroku

Yes, you could just fork TryGhost/Ghost instead. I have a deep, inexplicable aversion to capitalized repo names, so I wanted to do it this way.

Finally, because I wanted to keep any upstream branches clean, I created a second branch - git checkout -b stable-local - and pushed that up to github: git push origin stable-local. I knew I would be making changes, and I wanted to be able to git pull upstream stable without merge conflicts.

At this point, you need to do some local initialization, via npm install, grunt init, and grunt prod (if you want to run in production mode locally . . . which, frankly, hasn't been that useful to me). Note that, if you don't have grunt installed already, you'll need to run npm install -g grunt-cli. I use grunt regularly, so I already had it. For some as yet mysterious reason, occasionally, npm install did not install every dependency (it just randomly ignored path-match).

The official installation instructions may help if you have trouble. Honestly, I'm having a hard time remembering if I'm missing any steps here. When things work, it's easy to overlook them.

At this point, you should be able to visit localhost:2368/ghost. However, there is (or was at the time of my installation) a bug from a breaking change in a dependency. If you get a blank (completely white) page, congratulations! You managed to get a buggy version too. Here's the issue that helped me debug it: Admin console not loading. The tl;dr is that you need to npm uninstall grunt-ember-templates && npm install grunt-ember-templates@0.5.0-alpha which will replace the faulty version of grunt-ember-templates with one that works.

Putting Ghost on Heroku

This is where things got interesting. All the other blogs I read made it sound like it was as easy as swapping postgres for sqlite, but it's slightly more complicated than that.

First, if you don't have the heroku toolbelt, you should start with that. You can get it here. This is not a tutorial on setting up heroku, however, so I am assuming that you a) have a heroku app set up already and b) have some knowledge of how heroku (and its toolbelt) work.

Configuring Postgres

If you don't already know this (I didn't when I started), heroku's filesystem is ephemeral. Which means it occasionally gets blown away. Sqlite uses the filesystem, so you can't use it on heroku, or you'll continually lose everything you write. So start by installing the postgres addon for heroku (it's free for the first 10,000 rows): heroku addons:add heroku-postgresql:dev --app tandrewnichols. You might be able to run this without the --app flag, but given how much trouble heroku/ghost gave me, I'd opt to be explicit. Adding postgres will print something like HEROKU_POSTGRESQL_BRONZE (or some other color) in your terminal. Use that color and run heroku pg:promote HEROKU_POSTGRESQL_BRONZE (or whatever color it gave you).

You shouldn't check your postgres credential into github, so you need to use environment variables to tell Ghost where to save things. Go to the Heroku postgres dashboard, click on the database you just promoted, and look at "Connection settings," which has everything you need to get Ghost connected correctly.

Start by setting the environment variables in your heroku configuration. The values for these variables come from the "Connection settings" area mentioned above. I'm using my own blog as an example, but obviously, use your own url for MY_URL.

heroku config:set POSTGRES_DATABASE=<database>  
heroku config:set POSTGRES_HOST=<host>  
heroku config:set POSTGRES_PASSWORD=<password>  
heroku config:set POSTGRES_USER=<username>  
heroku config:set MY_URL=  
heroku config:set NODE_ENV=production  

Here, however, is one place where my installation process differed from many of the tutorials. You need to add your database configuration to config.js in the root of your blog repo. Most of the tutorials say to change the production config to something like the following:

database: {  
  client: 'postgres',
  connection: {
    host: process.env.POSTGRES_HOST,
    user: process.env.POSTGRES_USER,
    password: process.env.POSTGRES_PASSWORD,
    database: process.env.POSTGRES_DATABASE,
    port: '5432'
  debug: false

This configuration did not work for me, for whatever reason. Possibly I needed to run the heroku config:set commands with --app; possibly, the version of knex (the node module ghost uses to connet to it's database) changed. I kept getting strange undefined is not a function errors from somewhere in the bowels of knex. If you look at your heroku environement variables at<heroku_user_name>/settings, you'll see that there's one you didn't add: DATABASE_URL. I was able to configure postgres using the following configuration:

database: {  
  client: 'pg',
  connection: process.env.DATABASE_URL,
  debug: false

Incidentally, I believe either pg or postgres will work under client.

Additional configuration

You do also need to change some other things inside config.js, so while you have it open, change url to your blog's url. If you're using a custom domain, put that here, otherwise, use your heroku app url (http://<app_name> Below url, add fileStorage: false,. This actually turns off the photo upload feature of the admin area, replacing it with a text box that asks for the url of an image (for embedding). Finally, change the server to:

server: {  
  host: '',
  port: process.env.PORT

You can also add forceAdminSSL: true if you want to ensure that you don't login to the admin area on http and send your credentials in plain text. I was originally concerned about how to freely/cheaply handle SSL on my custom domain, but that's over-thinking it. Heroku has SSL enabled by default on all apps, so just make sure you do all your writing/editing/etc. at https://<your_app> Your posts will still be visible at your custom domain for all other users.


Well, that's it right? A simple git add --all and git commit -m "Everything is awesome", followed by git push heroku stable-local:master, and you're up and running.


After the above steps, I was basically just getting started. When I pushed to heroku, I got a message from ghost that all the assets had not been compiled. Right, of course . . . the files generated by grunt are in the .gitignore, so they aren't on heroku. So we need to compile them manually on deploy. Open up package.json and under scripts add install: "grunt init prod". install scripts run after npm has finished installing node_modules, which is exactly when you'd want it to run.

Now everything is awesome. Push it up and . . . dang, it can't find grunt. That's because heroku runs npm install --production and grunt is a dev dependency. You can fix that, however, by adding another environment variable to tell npm to stay in line. heroku config:set NPM_CONFIG_PRODUCTION=false. Now npm will install all dependencies (which is important not only for grunt, but also because the pg module is listed under optionalDependencies).

Now . . . surely . . . everything is awesome. You guessed it. It's not. Push it up, and you'll get an error about something not being a git repository (or any of its parents). This happens when grunt tries to run the update_submodules task (any installed themes, which is only "casper" by default, are git submodules in the content/themes directory). I haven't looked at this task in depth, but presumably, it spawns a process that runs git submodule update --init, but on heroku, your app is not in a git repository. Fortunately, heroku automatically detects and installs git submodules. In fact, if you look at the output from your last deploy, one of the very first lines says that it successfully installed the submodules. So what we really need is to run all the grunt stuff but without the submodule task. Too bad grunt doesn't allow exclude syntax like globstar patterns, something like grunt init prod !update_submodules. As it turns out, however, grunt init is just a collection of grunt shell:bower, grunt update_submodules, and grunt default, so we can just amend the package.json install script to say install: "grunt shell:bower default prod" and leave out the submodule task.

If I remember correctly, it was at this point that I actually DID get ghost up and working correctly. I was able to login to the admin area and do stuff. But there were still some configurations left to do.

Adding a Mail Server

This was surprisingly straight-forward. The instructions from worked fine. The hardest part was deciding whether to use Mandrill or MailGun. The ghost support docs for mail suggest Mailgun, primarily because it allows 10,000 emails a month with a free plan, but Mandrill gives you 12,000, and the example I was looking at used Mandrill. So that's what I did. There's nothing tricky here; just sign up at mandrill, add an API key, then add some configuration to config.js. By default, under production, you'll see mail: {}. Change this to say:

mail: {  
  transport: 'SMTP',
  host: '',
  options: {
    service: 'Mandrill',
    auth: {
      user: process.env.MANDRILL_USERNAME,
      pass: process.env.MANDRILL_APIKEY

Then run heroku config:set MANDRILL_USERNAME=<username> MANDRILL_APIKEY=<apikey> using the values on your mandrill dashboard.

Adding Syntax Highlighting

I'm a programmer, and I knew the majority of my blogging would require syntax highlighting. Adding syntax highlighting is both easy and hard. The actual editing required is next to nothing. I used this tutorial and had it done in minutes. Just open content/themes/casper/default.hbs and, in the section labeled {{! Style'n'Scripts }}, just add an additional css link after the existing two: <link rel="stylesheet" type="text/css" href="//"> using whatever theme you prefer ("hybrid" is the theme there). Then near the bottom , in the section labeled {{! The main JavaScript file for Casper }}, add two additional scripts under the existing two:

<script src=""></script>  

I also had to change the css for the casper theme, otherwise code snippets wrapped in bizarre ways. I edited the pre code, tt styles in assets/css/screen.css to use white-space: pre (it was pre-wrap) and word-wrap: normal. That made code snippets scroll in the x direction but still wrap at line breaks.

That's the easy part. The hard part is that the casper theme is in a git submodule. I'll spare you the rant on git submodules. Suffice it to say that I don't think they are the answer ever. They are just hard to work with, and heaven forbid you forget a step!

I tried a couple things here. First, I tried to clone the casper theme and edit it, preserving the submodule structure, but pointing it at a different repo. Unforunately, I did this wrong, and git submodules do not accept failure. Basically, the push to heroku failed because it couldn't find the sha referenced by the main repo (even though I did in fact push that commit up to github). Second, I tried just blowing away the submodule (harder than it sounds) and replacing it with just a regular old directory with the files I needed. Gotchas abound with this approach.

  1. If you copy the theme repo wholesale into content/themes, even if you blow away .gitmodules and any references under .git/modules, git will believe the theme is a submodule because it has a .git directory.
  2. If you copy only the actual handlebars templates and static assets, when you launch ghost, it will complain, telling you that a package.json will, at some point in the future, be required for ghost themes.
  3. git submodules are still terrible . . . updating them, removing them, putting them back after mistakenly removing them.
  4. This method involves significantly more change, and I doubt severly that dealing with conflicts involving what was a submodule and now isn't will be straight-forward.

Finally, I settled on what probably should've been my preferred approach from the beginning for the same reason I created a stable-local branch: easy merging. I cloned the casper theme and pushed it up to my github account as casper-dev and made all my edits there. Then I just had to figure out how to install another theme in Ghost (this means using submodules more . . . huzzah!).

I started by running git submodule content/themes/casper-dev. I've actually already made a mistake, but I won't know this until after another couple hours of annoyance with heroku, ghost, and . . . as always . . . git submodules. One command you should know about (which I found on this stackoverflow question) is git submodule update --remote --merge which will basically pull changes in each submodule then checkout the latest commit. Also don't forget, whenever you change the submodule reference, you need to run git submodule sync or git will hate you forever.

So back to the mistake I made. When I committed and deployed these changes, I started getting errors (from heroku) about my host key being invalid (even though my casper-dev repo was public). If you search for this error in conjunction with "heroku", you will be told you need to upload a public key to heroku. Following this advice resulted in the fruitless endeavor of uploading, in succession, each public rsa key I had and discovering that the problem still existed. The real problem is that<username>/<reponame>.git is the writable SSH version of a git url. Replacing the url for this repo in .gitmodules with git:// did the trick.

One more gotcha occurred at this point, which was mostly unrelated to everything I had been doing. It turns out there's a task run by grunt called "buildAboutPage" that dynamically generates the "About" page (as the name suggests) that you see in the admin area. This task includes making a call to the github api to fetch the primary contributors to a project. Nothing wrong with that really other than the fact that that api is rate limited, and apparently, at exactly this moment that I solved all my other problems, I exceeded the limit. Heroku successfully installed all the submodules (and even reported that it launched successfully), but when I tried to run it, I got a 500, and the logs complained (yet again) that the javascript assets had not been built. I tried wrapping the majority of that task in an if (false) {, but still had no luck. Finally, I noticed that there's a check there to see if the page already exists before ghost builds it. So I just removed /core/client/templates/-contributors.hbs from the .gitignore file and committed the version that had been built at the beginning of this whole adventure when I ran grunt locally. I could probably undo this change at this point - the rate limiting will have reset by now - but at the same time, I feel like, "Why should I build this page every single time when a) it doesn't change that much and b) I'm never going to look at it?"

Adding Disqus

Once I had my custom theme working, installing disqus was very easy. Disqus itself has a simple walkthrough of how to do it. It basically involved modifying post.hbs under my custom theme, to add the disqus snippet:

<div id="disqus_thread"></div>  
<script type="text/javascript">  
    var disqus_shortname = 'example'; // required: replace example with your forum shortname
    var disqus_identifier = '{{}}';

    /* * * DON'T EDIT BELOW THIS LINE * * */
    (function() {
        var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
        dsq.src = '//' + disqus_shortname + '';
        (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
<noscript>Please enable JavaScript to view the <a href="">comments powered by Disqus.</a></noscript>  
<a href="" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>  

The only tricky part was figuring out where to put the snippet on the page. They, somewhat unhelpfully, tell you to put it between {{/post}} and </article>. The {{/post}} is the very last thing on the page, but right above that (before you reach </article>) there's a </main>. I tried, at first, putting the disqus snippet right before </main>, but this made it stretch across the whole screen. I had to put it inside the <article></article> tags, right at the end to get the appearance to be consistent with the post itself.


Yes, now everything really is awesome. Hey, I wrote this post didn't I? That means things are working. One last caveat (which I almost forgot to do myself) is that you need to enable your custom theme from the admin interface. Go to "Settings", and then way at the bottom, there's a "Theme" dropdown that lists all the themes it finds under content/themes. Don't forget to click "Save" (which is hiding way at the top of the screen trying not to be found).

Hopefully, your experience setting up Ghost on Heroku is more graceful than mine. If you follow this tutorial sometime in the next 10 days or so, I imagine it will be. But I'm guessing the next patch increment on ghost will introduce new challenges and make these fixes outmoded. I guess the take away from this post should just be "Persistence pays off." Ghost and Heroku are like those lovers on sitcoms that don't get along at all, until suddenly they really get along. That is to say, they are not kidding when they say Heroku is not officially supported, but . . . it can be done.

comments powered by Disqus