Running a Ghost Blog on Heroku

While I'm looking at Ghost related-things, let's take a quick look at using Ghost on Heroku. One of the organizations I volunteer at recently spun up a website and decided they wanted a blog. I figured integrating a Ghost blog in there would be a great, lightweight solution. Best part is that someone else would be maintaining the software updates for the platform itself.

Personally, this is my first foray with Heroku but luckily it looks I was able to find someone who has this process started for 0.11.3, cobyism/ghost-on-heroku. I forked the repo and made a couple of changes, one was to remove the "click to deploy" feature. My main reason for doing this is that Heroku prompted me for a payment method even though the app was only using free resources when I went to deploy. So instead I just linked Heroku to the private Github repo and deployed that way.

One thing I did learn is that app.json, the handy Heroku app manifest, is only read on the initial deploy. So if you, like me, did a bare deploy and then added the info for an S3 bucket or an actual URL instead of just the Heroku app URL, the updated information isn't picked up from app.json.

I discovered this when the app crashed fabulously like so:

2017-02-04T17:53:42.152191+00:00 heroku[web.1]: Starting process with command `npm start --production`
2017-02-04T17:53:45.872184+00:00 app[web.1]:
2017-02-04T17:53:45.872201+00:00 app[web.1]: > node server.js
2017-02-04T17:53:45.872200+00:00 app[web.1]: > ghost-on-heroku@0.11.3 start /app
2017-02-04T17:53:45.872202+00:00 app[web.1]:
2017-02-04T17:53:49.636003+00:00 heroku[web.1]: Process exited with status 0
2017-02-04T17:53:49.512391+00:00 app[web.1]: Unhandled rejection TypeError: This library (validator.js) validates strings only
2017-02-04T17:53:49.512403+00:00 app[web.1]:     at assertString (/app/node_modules/ghost/node_modules/validator/lib/util/assertString.js:9:11)
2017-02-04T17:53:49.512404+00:00 app[web.1]:     at Object.isURL (/app/node_modules/ghost/node_modules/validator/lib/isURL.js:54:30)
2017-02-04T17:53:49.512405+00:00 app[web.1]:     at ConfigManager.validate (/app/node_modules/ghost/core/server/config/index.js:420:20)
2017-02-04T17:53:49.512406+00:00 app[web.1]:     at /app/node_modules/ghost/core/server/config/index.js:334:29
2017-02-04T17:53:49.512407+00:00 app[web.1]:     at tryCatcher (/app/node_modules/ghost/node_modules/bluebird/js/release/util.js:16:23)
2017-02-04T17:53:49.512409+00:00 app[web.1]:     at Promise._settlePromiseFromHandler (/app/node_modules/ghost/node_modules/bluebird/js/release/promise.js:510:31)
2017-02-04T17:53:49.512410+00:00 app[web.1]:     at Promise._settlePromise (/app/node_modules/ghost/node_modules/bluebird/js/release/promise.js:567:18)
2017-02-04T17:53:49.512410+00:00 app[web.1]:     at Promise._settlePromiseCtx (/app/node_modules/ghost/node_modules/bluebird/js/release/promise.js:604:10)
2017-02-04T17:53:49.512411+00:00 app[web.1]:     at Async._drainQueue (/app/node_modules/ghost/node_modules/bluebird/js/release/async.js:143:12)
2017-02-04T17:53:49.512412+00:00 app[web.1]:     at Async._drainQueues (/app/node_modules/ghost/node_modules/bluebird/js/release/async.js:148:10)
2017-02-04T17:53:49.512413+00:00 app[web.1]:     at Immediate.Async.drainQueues [as _onImmediate] (/app/node_modules/ghost/node_modules/bluebird/js/release/async.js:17:14)
2017-02-04T17:53:49.512413+00:00 app[web.1]:     at processImmediate [as _immediateCallback] (timers.js:367:17)
2017-02-04T17:53:49.652290+00:00 heroku[web.1]: State changed from starting to crashed
2017-02-04T17:53:50.746150+00:00 heroku[router]: at=error code=H10 desc="App crashed" method=GET path="/" request_id=24929605-de9c-4504-9326-a3f2d766c9b4 fwd="" dyno= connect= service= status=503 bytes=
2017-02-04T17:53:51.086501+00:00 heroku[router]: at=error code=H10 desc="App crashed" method=GET path="/favicon.ico" request_id=104cb0c8-fcd1-4173-aae6-b40861ce4e9e fwd="" dyno= connect= service= status=503 bytes=

Since it was failing validation, I checked out the environment variables for the app:

$ heroku run printenv --app yagb
Running printenv on ⬢ yagb... up, run.8203 (Free)
PS1=\[\033[01;34m\]\w\[\033[00m\] \[\033[01;32m\]$ \[\033[00m\]

Lo! No updated HEROKU_URL or anything else. So these needed to be updated/added in the Heroku UI under Settings -> Config Variables - no big deal. I also created and linked to a named Ghost database instead of just the default using the Heroku CLI:

$ heroku addons:create heroku-postgresql:hobby-dev --as ghostdb --app yagb

Looking at cobyism's package.json file, currently s/he's set up for 0.11.3. Since the latest version of Ghost is 0.11.4, now that I have 0.11.3 working I'm going to update to 0.11.4. Hopefully this'll go easier than last time. ¯\_(ツ)_/¯

Anywho, in order to update to 0.11.4 aside from changing 0.11.3 to 0.11.4 I also update Casper to 1.3.5 (as that ships with 0.11.4), the version of node to ~4.7 as the recommended versions of node to use with Ghost are the 4.x Argon LTS releases. ncp and ghost-s3-storage-adapter were still at their latest releases.

The deploy was successful and now the org has v0.11.4 of Ghost on Heroku. Happy endings ^.^

Fun facts: How to Semver with npm

So you may be wondering what the difference between ~x.y.z and ^x.y.z is, since both versions (see what I did thar) appear in package.json. In the first case, ~x.y.z, matches the most recent minor version (the y). The second case, ^x.y.z, matches the most recent major version. With some relevant numbers, that means ~4.7.3 pick up the latest 4.7.x release. Right now, it happens to be 4.7.3 but if there was a update to 4.7.4 that version would be applied on the next deploy. That said, a 4.8.x release would not be applied - unless I used ^4.7.3, then the latest release through (but not including) 5.x.x would be used during a deploy.

For more information, see tilde and caret releases on the NPM docs.