Ghost Blogging Platform in Docker

Photo credit: Pexels: Ed Gregory

Now that PostgreSQL is up and running, we’ve got a great database to connect our blogging platform to.

Running Ghost in Docker

Ghost is a Node.js application, and launches using npm. I set up Ghost with my own Dockerfile. Here it is:

FROM node:0.10

RUN apt-get update && \
    apt-get install -y unzip

RUN mkdir -p /usr/src/app && \
    wget -O /tmp/ghost-latest.zip https://ghost.org/zip/ghost-latest.zip && \
    unzip /tmp/ghost-latest.zip -d /usr/src/app && \
    rm /tmp/ghost-latest.zip && \
    useradd ghost --home /usr/src/app

WORKDIR /usr/src/app
RUN npm install --production

ENV NODE_ENV production

COPY config.js /usr/src/app/config.js

RUN chown ghost:ghost -R /usr/src/app

USER ghost

COPY apps   /usr/src/app/content/apps/
COPY themes /usr/src/app/content/themes/

VOLUME ["/usr/src/app/content/images", "/usr/src/app/content/data"]
EXPOSE 2368
CMD [ "npm", "start" ]

Breaking it down

Let’s look at the Dockerfile piece by piece. I start with the official node 0.10 image from the Docker Registry. The next part of the file just downloads and unpacks the Ghost zip from ghost.org.

FROM node:0.10

RUN apt-get update && \
    apt-get install -y unzip

RUN mkdir -p /usr/src/app && \
    wget -O /tmp/ghost-latest.zip https://ghost.org/zip/ghost-latest.zip && \
    unzip /tmp/ghost-latest.zip -d /usr/src/app && \
    rm /tmp/ghost-latest.zip && \
    useradd ghost --home /usr/src/app

WORKDIR /usr/src/app
RUN npm install --production

ENV NODE_ENV production

After upacking the zip, I let npm install the production dependencies and set the NODE_ENV to production so that Ghost boots in production mode.

Moving on, I copy my customized config.js, set the file ownership, set the running user, and copy in my custom content into the Ghost content folder. This is how I copy my custom theme into the image. I’ve also got volumes set up for the images and data directories should I want to utilize them (I’m not using them, I put all my blog post images in S3).

COPY config.js /usr/src/app/config.js

RUN chown ghost:ghost -R /usr/src/app

USER ghost

COPY apps   /usr/src/app/content/apps/
COPY themes /usr/src/app/content/themes/

VOLUME ["/usr/src/app/content/images", "/usr/src/app/content/data"]
EXPOSE 2368
CMD [ "npm", "start" ]

Finally the config set the port to expose to other containers or host port mapping (2368 is the standard port Ghost will listen to) and I set the default command for the image to run npm start (production mode will be set by the env var we set earlier).

That config.js file

The config file I use pulls some specific information from the launching process env, the PG_PORT_5432_TCP_* variables get set by Docker when I link the ‘pg’ container. The other 3 get set on the command line when I start the container via docker run.

I also take the opportunity to set a useful flag in Ghost, the fileStorage: false forces me to only provide a URL to an image, which I get from hosting my images on S3. This way I can’t upload a file into the running Ghost container, of which I make no effort to maintain the run-time data.

// # Ghost Configuration
var path = require('path'),
    config;

var db_addr = process.env.PG_PORT_5432_TCP_ADDR;
var db_port = process.env.PG_PORT_5432_TCP_PORT;
var public_url = process.env.GHOST_URL;
var mailgun_username = process.env.MAILGUN_USERNAME;
var mailgun_password = process.env.MAILGUN_PASSWORD;

config = {
    production: {
        url: public_url,
        forceAdminSSL: true,
        database: {
            client: 'pg',
            connection: {
                host     : db_addr,
                port     : db_port,
                user     : 'postgres',
                password : '',
                database : 'ghost',
                charset  : 'utf8'
            },
            debug: false
        },
        server: {
            host: '0.0.0.0',
            port: '2368'
        },
        mail: {
            transport: 'SMTP',
            options: {
                service: 'Mailgun',
                auth: {
                    user: mailgun_username,
                    pass: mailgun_password
                }
            }
        },
        fileStorage: false,
    },
}

I’m also using mailgun to send email with Ghost, it was simple to set up, is directly supported by Ghost, and the free plan will more than cover the very few emails (admin functions only) that Ghost needs to send. Additionally you’ll notice that I tell Ghost to forceAdminSSL: true so that any access to the admin interface of my blog is over SSL, to protect my password. I just use a self-signed certificate since it is just for me, at least until Let’s Encrypt is ready.

Updates to systemd

Finally a simple systemd unit configuration to launch my Ghost container.

[Unit]
Description=Run ghost
After=pg.service
Requires=pg.service

[Service]
Restart=always
RestartSec=30s
ExecStartPre=-/usr/bin/docker kill ghost
ExecStartPre=-/usr/bin/docker rm ghost
ExecStart=/usr/bin/docker run --rm --name ghost --link pg:pg -e "MAILGUN_USERNAME=YOURMAILGUN_USERNAME" -e "MAILGUN_PASSWORD=YOURMAILGUN_PASSWORD" -e "GHOST_URL=http://yourdomain.com" ghost
ExecStop=/usr/bin/docker kill ghost
ExecStartPost=-/usr/bin/docker rm ghost

[Install]
WantedBy=multi-user.target

Here I make sure to link in the PostgreSQL container named pg, and set the specific environment variables my config.js is expecting.

Those are the basics for the Ghost portion of my website stack. There is still more to come in this series examining the tech behind my website relaunch. Check back soon!