Yet another tech blog on the interwebs

Ghost deployed with Dokku

What could be better than describing how easy it is to get Ghost deployed with dokku as my first post? I obviously couldnt come up with anything better, so here it is.

Getting Ghost deployed with dokku

Other than giving a thumbsup for Ghost I'm not going to do yet another poor introduction of the blog platform. If you havent heard of it Ghost's project page should suffice.

As for hosting I've got my pet projects on DigitalOcean. For $5/month they provide a cloud server with enough oomph to have some fun. When creating a cloud server you get to choose amongst a set of different clean linux distros or a pre-built application setup (such as Wordpress, LAMP or MEAN-stack). Luckily DigitalOcean has a dokku application setup, which makes creating your own cheap alternative to heroku a walk in the park. It's actually so fast and easy to get started with that I would recommend creating such a cloud server for testing dokku's awesomeness, rather wasting time installing dokku (an its required stack) on your local machine.

The post expects you have a dokku instance ready for action already.

Download and prepare

  1. Download Ghost
  2. Unzip it
  3. Use example config file
$ unzip -d ghost-blog
$ cd ghost-blog
$ mv config.example.js config.js


Your unpacked Ghost directory needs to be git-ified in order to make it dokku friendly.

  1. Initialize a git repo in Ghost directory
  2. Prevent node_modules from being commited
  3. Commit
  4. Add dokku as remote
$ git init
$ echo "node_modules" > .gitignore
$ git add .
$ git commit -m 'Initial commit'
$ git remote add dokku dokku@<your-server-hostname>:blog

Production host and port number

When deploying with dokku you have to let dokku decide which port number the application should listen to. Also which IP it should attach to needs to be changed from the localhost interface. At this time of writing the default Ghost config has hardcoded port number in production, luckily very easy to fix.

// config.js
config = {
    production: {
        url: 'http://<your-domain>',
        mail: {},
        database: {
            client: 'sqlite3',
            connection: {
                filename: path.join(__dirname, '/content/data/ghost.db')
            debug: false
        server: {
            host: '',         // was ''
            port: process.env.PORT   // was '2638'

Getting configuration saved for production means doing another commit

$ git commit -am 'Fixed port number for production'


Now for the most exciting step; let dokku do its magic and deploy the blog by pushing the repo

$ git push dokku master

Ghost is actually running on your dokku instance as soon as the push finishes, sadly nginx will fail to proxy any requests to the application appropriately. As any other express.js based web application it runs in development mode by default, which also decides which config it uses (remember the hardcoded port number?).

The last piece in the puzzle is to tell Ghost it's running in production.

$ ssh dokku@<your-server-hostname> config:set blog NODE_ENV=production
-----> Setting config vars and restarting blog
NODE_ENV: production
-----> Releasing blog ...
-----> Release complete!
-----> Deploying blog ...
-----> Deploy complete!  

This would be time you visit your Ghost blog and see signs of life for the first time!

The first time I had this blog up and runnig I thought FINISHED at this point. Little did I know about what nasty surprise awaited me after the next deploy! Ghost stores all the settings and posts in a filebased datastore (SQLite) by default. With the config.js shown above, these data will be stored in <app-root>/content/data/ghost.db. The (not so) funny thing about docker is that all files related to an instance are flushed at every deploy, leaving you with only the files checked into your git repository.

Persistence data

UPDATED after comments from Ketil Moland Olsen, Martin Halík and Hilder Santos

Many jumps the PostgreSQL wagon when this problem arises. As I've got very limited amount of memory on my DigitalOcean server, I want to avoid a fully fledged database engine for as long as possible. I'd rather use the shared volume feature in docker which enables the docker host to mount a directory into the filesystem of the docker container. Such directories shared by the docker host are not flushed between deploys, effectively providing us a persistent filesystem for docker containers. Read more about managing data volumes in docker docs.

1. Install dokku-persistent-storage
2. Create directories for persistent blog files
3. Tell dokku which directories to mount

Since writing this post "persistent storage" has been deprecated in favor of the core storage feature, meaning you dont have to install a third-party plugin for this functionality.

# mkdir -p /var/www/blog/data
# mkdir -p /var/www/blog/images
# dokku storage:mount blog /var/www/blog/data:/app/content/data
# dokku storage:mount blog /var/www/blog/images:/app/content/images

The storage feature was added in dokku v0.5.0. If you're on a lower version and dont want to upgrade right away, you could still use docker-options:

# dokku docker-options:add blog deploy "-v /var/www/blog/data:/app/content/data"


git push fails

If you get this error message while doing git push:

runtime: panic before malloc heap initialized  
fatal error: runtime: cannot allocate heap metadata  

Follow the steps in this memory fixing blog post which describes how to create and mount a swap partition.

Thanks to Martin Halík for referencing the blog post about fixing this in his comment.


Multiple domains

dokku-domains-plugin enables me to have one dokku application respond to multiple (sub-) domains such as and Read more about it in the plugin's readme.

Since writing this post "multiple domains" has been added as a core dokku feature, meaning you dont have to install a third-party plugin for this functionality.


# dokku domains:add blog <yet-another-domain-name>


I forked Whisper and customized to my likings. There's alot of free and cheap themes on

comments powered by Disqus