This post is intended to demonstrate setting up a production version of Ghost running under Docker. I’ve been experimenting with Docker the last few weeks and I want to share what I’ve learned in that time.

What is Docker?

For those who have not heard of it, Docker is a project that allows you to build self contained instances of an application.

These containers, as they are called, are able to be reliably built and replicated on a server from a set of commands (located in a Dockerfile) that you build on your development box.

This means that the container is consistent across different machines. The resulting container should act the same if it’s on your laptop, desktop, bare metal server, or your cloud server.

Writing Dockerfiles is out of the scope of this post, but please do take the time to learn how they work. Dockerfile Tutorial here

Why Docker?

To quote Docker, Inc. themselves:

Common use cases for Docker include:

  • Automating the packaging and deployment of applications
  • Creation of lightweight, private PAAS environments
  • Automated testing and continuous integration/deployment
  • Deploying and scaling web apps, databases and backend service

With Chef, I would have to tear down the entire server VM to completely test everything from start to finish. Containers are built very quickly, so the time to run through the build/teardown cycle is painless.

The containers also make testing a deploy artifact before it goes into production so easy that it’s very hard to find an excuse not to. The containers being self contained means you can rollback to a previous version of your app with relative ease and less worry.

What is Dokku?

Dokku builds on Docker and turns your server into something similar to Heroku. You have the same git based deploy that you’ve come to rely on when using Heroku. Environment variables are managed the same way as on Heroku.

Basically, if your app runs on Heroku, it will run similarly under Docker/Dokku. Dokku is built in Bash and has a budding plugin ecosystem to handle things like databases and Redis.

What is Ghost?

Ghost is a blogging platform built with Node.js. It started its life as a Kickstarter campaign in April 2013 and hit its funding goal very quickly.

Ghost aims to make the writing experience pleasurable. It uses a Markdown editor with a live preview pane next to that editor, which makes the feedback loop instant.

Ghost is still in its early stages (like the rest of the software used in this post), but I see great things ahead.

Getting Up And Running

Easy mode: Digital Ocean has a droplet image that already has Docker and Dokku installed, complete with an initial web page to setup git push access. I recommend it if you’re not comfortable with server management. If you use it, skip down to the Dokku Plugins section. Digital Ocean setup doc here

For this post, we will be using Ubuntu 13.04 64-bit. Docker can be installed on other distributions, but official support is still on Ubuntu. I am also assuming you are sudo’ed into root.

Installing Docker

Add the Docker apt repository:

wget -qO- https://get.docker.io/gpg | apt-key add -

echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list

apt-get update

Install the “extra” Ubuntu kernel:

apt-get install linux-image-extra-`uname -r`

Reboot to make sure the new kernel is loaded.

Install Docker:

apt-get install lxc-docker

Docker will now be installed. You can verify this by running docker help

Additionally, you will want to edit the upstart config in /etc/init/docker.conf to make sure Docker restarts the containers that were running in case your server reboots.

The script, in the current version of Docker (0.7), the upstart file looks like this:

description "Docker daemon"

start on filesystem and started lxc-net
stop on runlevel [!2345]

respawn

script
     DOCKER=/usr/bin/$UPSTART_JOB
     DOCKER_OPTS=
     if [ -f /etc/default/$UPSTART_JOB ]; then
          . /etc/default/$UPSTART_JOB
     fi
     "$DOCKER" -d $DOCKER_OPTS
end script

Update the DOCKER_OPTS line to DOCKER_OPTS="-r=true"

Installing Dokku

Run the following command:

wget -qO- https://raw.github.com/progrium/dokku/master/bootstrap.sh | bash

You may also clone https://github.com/progrium/dokku.git and run the bootstrap.sh command:

git clone https://github.com/progrium/dokku.git
cd dokku
bash ./bootstrap.sh

This will take a bit as it is downloading a base image to use for applications. It shouldn't take too long, so go watch yourself some Milli Vanilli

When done, you will have an almost working system. You need to do two more things.

Configuring Dokku

First, add your ssh key to dokku (in a terminal on your local machine, not the server):

cat ~/.ssh/id_rsa.pub | ssh root@your-domain.com "sshcommand acl-add dokku your_username”

On the server, update HOSTNAME and VHOST file in /home/dokku.

Both of mine contain my domain name servesapps.com

Dokku works by creating a subdomain on your domain that points to your app. So, for example, the domain I use with dokku is servesapps.com, so my urls are in the form of .servesapps.com (e.g. testapp.servesapps.com)

Dokku Plugins

I currently use a single plugin in my dokku installation. It is a plugin written by Paul Lietar, a very lovely fellow that commits to Dokku and hangs out in the #dokku freenode IRC channel.

Currently it is in his github fork of dokku, so clone his dokku fork with:

https://github.com/plietar/dokku.git dokku_with_addons
cd dokku_with_addons
git checkout addons

You will now want to copy the addons directory under the plugins directory to /var/lib/dokku/plugins

cp -R plugins/addons /var/lib/dokku/plugins

You will then want to run dokku plugins-install to install the plugin.

Next, you will want to grab his other repository dokku-addons

git clone https://github.com/plietar/dokku-addons.git

You will then need to create the addons directory and copy the addons over. You only need the mariadb, but you can copy the others if you wish:

cd dokku-addons
mkdir /var/lib/dokku/addons
cp -R mariadb /var/lib/dokku/addons

Finally, enable the addon:

dokku addons:enable mariadb

Now we are ready for Ghost itself.

Installing Ghost

Grab the latest release from Github:

https://github.com/TryGhost/Ghost/releases/download/0.3.3/Ghost-0.3.3.zip

Extract it to a directory so we can make a git repository out of it.

Git

Change into the directory and run initial a repository

cd ghost
git init

Add a remote that points to your dokku server

git remote add dokku dokku@your_domain.com:yourappname

Configuring Ghost

We need to do a few things to Ghost to get it to work under Dokku.

In the root of the Ghost directory, add a new file named Procfile. Add the following to it:

web: node index.js

Also in the root of the Ghost directory, add a config.js with the following:

// # Ghost Configuration
// Setup your Ghost install for various environments

var path = require('path'),
    config;

var url = require('url');

var connectionString = url.parse(process.env.DOKKU_MARIADB_URL);

config = {
    // ### Development **(default)**
    development: {
        // The url to use when providing links to the site, E.g. in RSS and email.
        url: 'http://my-ghost-blog.com',

        // Example mail config
        // Visit http://docs.ghost.org/mail for instructions
        // ```
        //  mail: {
        //      transport: 'SMTP',
        //      options: {
        //          service: 'Mailgun',
        //          auth: {
        //              user: '', // mailgun username
        //              pass: ''  // mailgun password
        //          }
        //      }
        //  },
        // ```

        database: {
            client: 'sqlite3',
            connection: {
                filename: path.join(__dirname, '/content/data/ghost-dev.db')
            },
            debug: false
        },
        server: {
            // Host to be passed to node's `net.Server#listen()`
            host: '127.0.0.1',
            // Port to be passed to node's `net.Server#listen()`, for iisnode set this to `process.env.PORT`
            port: '2368'
        }
    },

    // ### Production
    // When running Ghost in the wild, use the production environment
    // Configure your URL and mail settings here
    production: {
        url: 'http://blog.yourapp.com',
        mail: {},
        database: {
            client: connectionString.protocol.replace(':', ''),
            connection: {
                host: connectionString.host.split(':')[0],
                user: connectionString.auth.split(':')[0],
                password: connectionString.auth.split(':')[1],
                database: connectionString.path.replace('/', '')
            },
            debug: false
        },
        server: {
            // Host to be passed to node's `net.Server#listen()`
            host: '0.0.0.0',
            // Port to be passed to node's `net.Server#listen()`, for iisnode set this to `process.env.PORT`
            port: process.env.PORT
        }
    },

    // **Developers only need to edit below here**

    // ### Testing
    // Used when developing Ghost to run tests and check the health of Ghost
    // Uses a different port number
    testing: {
        url: 'http://127.0.0.1:2369',
        database: {
            client: 'sqlite3',
            connection: {
                filename: path.join(__dirname, '/content/data/ghost-test.db')
            }
        },
        server: {
            host: '127.0.0.1',
            port: '2369'
        }
    },

    // ### Travis
    // Automated testing run through Github
    travis: {
        url: 'http://127.0.0.1:2368',
        database: {
            client: 'sqlite3',
            connection: {
                filename: path.join(__dirname, '/content/data/ghost-travis.db')
            }
        },
        server: {
            host: '127.0.0.1',
            port: '2368'
        }
    }
};

// Export config
module.exports = config;

This is mostly the default config.js provided in the Ghost package. The key points that I had to add/change where:

  • Connection string parsing. You will find the initialization of connectionString at the top, with the parsing in the production config area
  • The host and port number in the production config.

These two changes in config.js are to support Dokku. IPs and ports are dynamically assigned to containers. The database connection string is provided by the mariadb add-on. You can

Commit everything and push it up to your dokku server:

git add .
git commit -a -m "Initial commit"
git push dokku master

It will build and install Ghost, assign a subdomain, and attempt to start Ghost. It will fail because we are missing a couple of configuration things. These configuration options can only be made after the app is first pushed.

Adding a database and environment variables

Finally, we need to setup the database and an environment variable used by Ghost to run.

Add the database:

dokku addons:add yourapp mariadb

This will take a bit, so just wait for it to return.

Add one configuration variable:

dokku config:set yourapp NODE_ENV=production

This will set these config options and restart the app. You should now find a working Ghost installation at your app address.

In my case, it was http://definitivecode.servesapps.com/ghost

Very important note: Ghost doesn't yet support uploading to S3, so if you upload images, they will disappear at some point. If the container is ever rebuilt, they will be gone. So use an image hosting service, wait for a S3 plugin to appear, or maybe contribute one yourself! :)

If anything is not clear, please hit me up on Twitter (@rerouse) or post in the comments and I will clarify.

Thanks!

Robert