Self-hosting a blog with Ghost and Docker

This is the story of how I got this blog up and running, and hopefully it's helpful to other people looking to self-host Ghost (hey that rhymed).

After some trouble getting the official Docker image for Ghost to work outside of my development box, I decided to make my own image.

Checking the Ghost Github repository, the documentation says:

  1. Download the latest release of Ghost
  2. Unzip in the location you want to install
  3. Fire up a terminal
  4. npm install --production
  5. Start Ghost!
    • Local environment: npm start
    • On a server: npm start --production
  6. http://localhost:2368/ghost

Converted into a Dockerfile, those instruction look something like the following.

FROM download13/node

WORKDIR /ghost  
COPY config.js /ghost/config.js  
RUN wget -O \  
    && unzip \
    && rm \
    && npm install --production \
    && npm cache clean \
    && rm -rf /tmp/npm*

CMD ["npm", "start", "--production"]

The npm cache clean and rm -rf /tmp/npm* bits are just cleanup to keep the image size down. The important part here is the COPY config.js /ghost/config.js line, which will bring in our custom config file.

Speaking of which...

module.exports = {  
    production: {
        url: process.env.BLOG_URL,
        mail: {},
        database: {
            client: 'sqlite3',
            connection: {
                filename: '/ghost/content/data/ghost.db'
            debug: false
        server: {
            host: '',
            port: process.env.PORT || '80'

Two fields are set via environment variables. The url field (set by BLOG_URL) will be the base URL for your blog. It's required if you don't want a somewhat broken experience. server.port defaults to 80 which will be fine for most uses, but can be overridden by setting the PORT environment variable.

Let's test what we've got so far.

# docker build -t ghosttest .
Successfully built 767d2c6e0177  

Sweet! This might actually work. Now let's try running it.

Note that is the IP of the Docker host VM on my computer. Replace it with localhost or the address of your Docker host if different.

# docker run -it -e "BLOG_URL=" -p 80:80 ghosttest

> ghost@0.7.9 start /ghost
> node index

WARNING: Ghost is attempting to use a direct method to send email.  
It is recommended that you explicitly configure an email service.  
Help and documentation can be found at

Migrations: Database initialisation required for version 004  
Migrations: Creating tables...  
Migrations: Creating table: posts  
Migrations: Creating table: users  
Migrations: Creating table: roles  
Migrations: Creating table: roles_users  
Migrations: Creating table: permissions  
Migrations: Creating table: permissions_users  
Migrations: Creating table: permissions_roles  
Migrations: Creating table: permissions_apps  
Migrations: Creating table: settings  
Migrations: Creating table: tags  
Migrations: Creating table: posts_tags  
Migrations: Creating table: apps  
Migrations: Creating table: app_settings  
Migrations: Creating table: app_fields  
Migrations: Creating table: clients  
Migrations: Creating table: client_trusted_domains  
Migrations: Creating table: accesstokens  
Migrations: Creating table: refreshtokens  
Migrations: Running fixture populations  
Migrations: Creating owner  
Migrations: Ensuring default settings  
Ghost is running in production...  
Your blog is now available on  
Ctrl+C to shut down  

Navigating to I see the following page.

Ghost Blog Homepage

Woohoo! Done! Well, not quite.

There's a little more to deploying it, but first I'm going to put what we have so far into an Docker Hub Automated Build for the sake of convenience. Now whenever we want to use this image, we can just refer to it as download13/ghost.

If you want to deploy this on your personal server, you're probably going to want to keep the contents of your blog even if you have to re-create it's container.

There are two directories that need to persist across containers if you want your blog to work correctly.

/ghost/content/data contains the sqlite database file which stores all your users/posts/tags/etc.

/ghost/content/images is where all uploaded images are kept.

Let's add these volumes to our container.

# mkdir -p ~/ghost_data/{data,images}

# docker run -it -e "BLOG_URL=" -p 80:80 -v ~/ghost_data/data:/ghost/content/data -v ~/ghost_data/images:/ghost/content/images download13/ghost

Of course running it from Docker Compose is a bit easier.

version: "2"  
        image: download13/ghost
        restart: always
            - ~/ghost_data/data:/ghost/content/data
            - ~/ghost_data/images:/ghost/content/images
# mkdir -p ~/ghost_data/{data,images}

# docker-compose up -d

Creating ghosttest_blog_1  

And that's it! Now you've got a working Ghost blog!