A Ghost Workflow

Host multiple Ghost blog sites on the same server. Develop and deploy your theme updates with ease. Most expeditious.

I wanted to start using Ghost for my blogs. For those who don't know, Ghost is a spiffy new Open Source platform solely created to manage and publish blogs. I was drawn to Ghost because it is a Node.js application, and because it uses Markdown for publishing.

This guide shows one way to set up a Ghost blog dev environment to

  • host multiple blogs sites on a single server and
  • use Git to deploy your theme updates.

I am using the following dev/hosting platforms:

  • I develop on a Mac
  • I host production on an Ubuntu 13.04 x64 VM at DigitalOcean

Overview of the Environment:

  • Mac development has Node.js and Ghost app installations
  • Production Linux VM uses Nginx to route requests to the respective Ghost blog Node apps
  • Production node apps run as Upstart services to stay up
  • Each blog app has its own Ghost installation
  • Git tracks only custom theme files or code edits
  • Git deploy hooks in production allow one command deployments for each blog site

Install Ghost on Development Mac

Be sure you have Node.js installed first.

Sign up for Ghost: https://ghost.org/signup

Follow their instructions to download and extract the zip file.

If you plan to manage multiple blogs, it will help if you stay consistent with where you put your blog projects. I keep all mine right off the root of my user folder. So for this example, move all the extracted ghost files to ~/yourblog.

Now install (build) the ghost app:

cd ~/yourblog
npm install --production

Then rename the config.example.js to config.js to avoid any confusion. (You can keep the config.example.js file for reference if you rather - Ghost will create the config.js file from it the first time you start Ghost.)

The config.js file is used to manage your specific environment settings. Ghost keeps separate settings for development and production environments here. When you start the Ghost app, you pass an argument to control which environment settings you want Ghost to use.

Edit the development section of the config.js file to assign an open port you want to use. Ghost defaults to 2368. I find it easiest to use the same port for development and production. So here I change the port to 8000 for this example:

development: {
    // The url to use when providing links to the site, E.g. in RSS and email.
    url: 'http://localhost:8000',

    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: '8000'
    }

So while your at it, let's change the production section as well. Also, set the url to match the domain of your blog:

production: {
    url: 'http://yourblogdomain.com',
    mail: {},
    database: {
        client: 'sqlite3',
        connection: {
            filename: path.join(__dirname, '/content/data/ghost.db')
        },
        debug: false
    },
    server: {
        // Host to be passed to node's `net.Server#listen()`
        host: 'localhost',
        // Port to be passed to node's `net.Server#listen()`, for iisnode set this to `process.env.PORT`
        port: '8000'
    }
},

Now test the install (npm start uses --development by default):

cd ~/yourblog
npm start

Open a web browser to http://localhost:8000 to verify Ghost is running.

To learn about setting up and switching Ghost themes, visit the Ghost documentation.

Troubleshooting: Watch the terminal's live logging to see what might be causing any potential issues.

Initiate Your Custom Theme(s)

If you're developing your own theme, copy the default Casper theme into your own theme folder:

cd ~/yourblog/content/themes
cp -r casper yourtheme

Setup Git Version Control

Make sure you have Git installed: git-scm.com or Homebrew, etc.

Now go to your project cd ~/yourblog and run git init to create a local repository for the blog site.

You will have different Ghost files installed in production (due to the binaries and such created during installation), so you want to be sure to only track code changes to your theme files, or any additional Ghost core files as you may need to edit. To accomplish this, use a .gitignore file to filter out all files except those you want to manage and deploy to production.

This can be tricky because .gitignore is optimized to ignore files, not include them.

In your ~/yourblog directory, add a new file named .gitignore and use something like this:

# Ignore everything first:
/*

# Then un-ignore the following:
!.gitignore

# --- Ghost core server index.js file ---
# --- Only needed if you edit this file ---
core/*
!/core/
core/server/*
!/core/server/
/core/server/*
!/core/server/index.js

# ---- Theme files -----
content/*
!/content/

content/themes/*
!/content/themes/

content/themes/yourtheme/*
!/content/themes/yourtheme/

!/content/themes/yourtheme/*

The trick is to first specifically ignore each directory that you then re-include files from.

You can test your .gitignore file by running this in your ~/yourblog directory:

git add .
git commit -m "init commit"
git ls-tree --full-tree -r HEAD

This will add all the trackable files and the last command show you the files that were added and are now tracked by Git. You should only see the files you expect. If not, edit your .gitignore file until it works as required for your needs. You don't want the core Ghost files (unless you asked for some specifically). You want to also make sure it is picking up the files you plan on developing and deploying. To "un-track" a file in Git that was added by accident, use git rm --cached <file>.


Create and Configure VM Server

I use DigitalOcean and create a Linux VM using the latest version of Ubuntu.

ssh into your VM, then update Ubuntu:

apt-get update
apt-get upgrade
apt-get dist-upgrade

Install Git:

apt-get install git

Create your git user who will deploy updates to production. I call the user git:

adduser git

Install Node.js:

apt-get install python-software-properties python g++ make
apt-get install software-properties-common
add-apt-repository ppa:chris-lea/node.js
apt-get update
apt-get install nodejs

Install Ghost:

You want to log in or change your user to git for the following steps. This will help ensure that the permissions settings allow git user to deploy files to production when we get to that step. Also create a directory to hold the production blog app.

Putting the git repository in the git user home directory (/home/git) makes for a nice easy path when we set up the remote git repo in development.

Note: Be sure to use the latest version of ghost where I have ghost-0.4.0.zip for this example.

su git

cd /home/git
mkdir yourblog
mkdir tmp
cd tmp
wget https://ghost.org/zip/ghost-0.4.0.zip
unzip -uo ghost-0.4.0.zip -d /home/git/yourblog
cd /home/git/yourblog
npm install --production
mv config.example.js config.js

Note: If unzip is not installed yet, log in as root su - and install it: apt-get install unzip

If you want to test your Ghost install at this point (before getting to Nginx), remember to edit the config.js file to add the correct host and port settings for your server. For a quick test, change the port to 80, run npm start --production from your /home/git/yourblog directory, then point a web browser to your server and verify that you see the default Ghost blog page.

Troubleshooting: As in development, hopefully the interactive logging will show you any issues.

Install Nginx

This will allow you to run multiple Ghost blog apps on the server. We will use Nginx as a reverse proxy to route user requests to the appropriate Node.js app.

Log in as root for this step:

su -

apt-get install nginx
service nginx start

Test Nginx is running now by visiting your server in a web browser. You should see the Nginx splash screen.

Configure Nginx

For each of the Ghost blog sites you want to host, create an Nginx .conf file. You can use any name for the .conf file, but matching your blog name will help avoid confusion:

cd /etc/nginx/conf.d
cat > yourblog.conf

Now paste in this file content, modified for your setup:

server {
    listen 80;

    server_name yourblogdomain.com www.yourblogdomain.com;

    location / {
        proxy_pass http://localhost:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

The important settings to modify are the server_name and proxy_pass lines.

This file tells Nginx to listen for any requests on port 80 for yourblogdomain.com or www.yourblogdomain.com and route them to http://localhost:8000 which is the host and port we set up in our Ghost config.js file. (You will assign different unique port numbers for each subsequent Ghost app you want to host.)

Note: You can also combine all your blogs in one .conf file - just put one server {} block after another. Also, you can use *.yourdomain.com to grab any host names and route them. I did have one issue where I needed to specifically add mydomain.com *.mydomain.com for it to route requests to mydomain.com with no host prefix.

To test the Nginx reverse proxy process:

  • Restart Nginx to enable the configuration: service nginx restart (or use nginx -s reload)
  • cd ~/yourblog and npm start --production to start the Ghost Node app
  • Point your web browser to yourblogdomain.com and verify you see your blog now

If you map more than a few domain names, the proxy may fail and you will get something like the following error in the Nginx error log:

could not build the server_names_hash, you should increase server_names_hash_bucket_size: 32

To increase the server_names_hash_bucket_size, edit the nginx.conf file using vi:

vi /etc/nginx/nginx.conf

The following line may be commented out with a # at the front of the line. Delete the # to re-activate the line:

server_names_hash_bucket_size 64;

Restart Nginx. If 64 does not fix the problem at first, increase it by a powers of 2, until it works (64, 128, 256, etc.)

Troubleshooting: To better see what is going on, you can monitor the Nginx error or access logs while you try to reach the server:

tail /var/log/nginx/access.log -f

tail /var/log/nginx/error.log -f

tail will show you the last few lines of the log, and the -f will let you monitor any new log entries as they come in. Use ctrl-c to exit.

Another key check: Your proxy_pass destination in the Nginx .conf file must match the production server: {host: 'XXX'} host setting in your Ghost config.js file. If you use localhost, it must be used in both files. Or if you use your server ip address, it must be used in both places as well!

Setup Git Deployment

This process will let you easily push out updates to your blog theme or any custom Ghost server edits you might implement. We'll use the post-receive Git hook to automatically deploy changes to the Ghost app and restart the Ghost service any time we push our an update.

Log in as git user so we create the git repository with permissions that the git user will be able to use.

su git

First, add a bare git repository on the server for your blog:

cd /home/git
mkdir yourblog.git
cd yourblog.git
git --bare init

Next, create the post-receive git hook file that will deploy updates to the /home/git/yourblog directory:

cd /home/git/yourblog.git/hooks
cat > post-receive

Then paste in this file content modified for your blog path:

#!/bin/sh
GIT_WORK_TREE=/home/git/yourblog git checkout -f

(ctrl-d to exit the cat process)

Change permissions on the file to allow it to be executed:

chmod +x post-receive

Now go back to your local development mac and add the remote git repository:

cd ~/yourblog
git remote add yourblogrepo_label git@yourproductionserver.com:yourblog.git

You will be typing yourblogrepo_label a lot, so use something short ;)

Test the deployment process on your mac. (Add the files to git and make an initial commit if you did not do so when you first set up the local git repo for this project: git add . and git commit -m "init commit").

cd ~/yourblog
git push yourblogrepo_label master

The git push command will prompt you for the password for the git user you created on the production server (unless you're using an ssh key to connect.)

This should push your tracked and committed code changes, and the git hook on the server should automatically deploy the updated files to the ghost app. Log into the production server and verify that your code change has now indeed been deployed to /home/git/yourblog.

Troubleshooting: Be sure you can log in to your server as git: ssh git@yourproductionserver.com. Once you log in as git user, you should start at /home/git and your yourblog.git repository should be right in that directory.

There is a limitation to our deployment process at this point. If we make any updates to the Ghost node server or its code dependancies, the node app needs to be restarted in order to use the updates. We will address this next when we set up our node app as an Upstart system service.

Running Ghost as a Service

In order to keep your Ghost blog apps running at all times, you can create an Ubuntu Upstart script and set up each node Ghost app as a system service which will do the following:

  • Start the Ghost Node.js app whenever the server restarts
  • Restart the app if it should fail for some reason (a crash from a bug, memory leak, etc.)
  • Allow us to conveniently start/stop the app via system commands

To set up a service for the Ghost node app, you need to be root user:

su -
cd /etc/init

cat > yourblog.conf

Now copy in this file content modified for your blog specifics:

description "yourblog Ghost Node Service"
author      "Your info ifya want"
start on started mountall
stop on shutdown

respawn
respawn limit 99 5

script
    cd /home/git/yourblog
    npm start --production >> /var/log/yourblog.log  2>&1
end script

post-start script

end script

(ctrl-d to finish cat process)

This service script tells the server to cd to your ghost app root, start it, and log the stdout and stderr from the app to the log file you specified.

Test starting, stopping and restarting your service:

start yourblog
stop yourblog
restart yourblog

Once started, it will keep running now. Note: if you have a major error in the app that causes a crash on startup, this script will start it over and over again 99 times (or for 5 seconds, whichever comes first) before giving up.

Enable App Restart on Deployment

We can leverage the service commands to enhance our git deployment process to also restart the node app.

First we need to change the sudoers file to allow the git user to sudo as root to start and stop your blog service. We also need to stop the sudo process from prompting for a password when using sudo for this task, otherwise the automated git hook script will fail due to the prompting.

Add one line to the end of the sudoers file. I like to use vi, but you can use the editor of your choice. To use vi: export EDITOR="vi"

Now to edit the sudoers file:

visudo

It is important to add this line to the end of the file:

git ALL = (root) NOPASSWD: /sbin/stop yourblog, /sbin/start yourblog

This tells the system to allow git user to sudo as root with no password prompt, but only when running the start and stop commands. Note: yourblog refers the service name you created- the same name of your Upstart .conf file. Also note that you need to use the full path for the start/stop commands. To double check your path, use which start to see the full path.

Note: You need to logout and log into the server again for the sudoers file change to take effect in your ssh session.

To test, ssh as git@yourserver.com

You should be able to stop/start the service as git user as follows:

sudo /sbin/stop yourblog
sudo /sbin/start yourblog

Now update the git post-receive hook to stop and start the service:

cd /home/git/yourblog.git/hooks
cat > post-receive

Then paste this file contents in modified for your blog:

#!/bin/sh
sudo /sbin/stop yourblog
GIT_WORK_TREE=/home/git/yourblog git checkout -f
sudo /sbin/start yourblog

(ctrl-d to finish)

Now when you deploy updates via git, the ghost service will restart. Make a small change to a ghost site file on your mac, commit the change, and deploy:

cd ~/yourblog
git add .
git commit -m "test deployment process"
git push yourblogrepo_label master

You should see the new process id # to confirm the restart of the service - something like this:

git push yourblogrepo_label master

git@yourblogdomain.coms password: xxxxx

Counting objects: 11, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), 499 bytes | 0 bytes/s, done.
Total 6 (delta 2), reused 0 (delta 0)
remote: yourblogrepo_label stop/waiting
remote: yourblogrepo_label start/running, process 3469
To git@yourblogdomain.com:yourblog.git
   60f6037..ece0ca8  master -> master

Seeing the process # gives us nice comfort that the service has indeed been restarted.

Conclusion

Now you can develop your Ghost theme files in ~/yourblog/content/themes/yourtheme, stage and commit the changes via Git, and deploy the updates to production with one command :)

When you want to add an additional blog to your server:

  • Create a new Ghost app in development
    • Create a new project directory
    • Install Ghost in the directory
    • Configure a unique port number in the config.js file
    • Create the local git repo for the project: git init
  • Create a new Ghost app in production as above
    • The app directory goes under the /home/git directory
    • Create a bare git repo for production: git --bare init
  • Configure Nginx
    • Create a .conf file with the new port number for this blog
    • Restart Nginx: service nginx start
  • Create Upstart service and enable deployment process
    • Create the Upstart .conf script
    • Add a new line to the sudoers file so git user can start/stop the new service
    • Create the post-receive file and grant execute permissions to it
    • Add a remote git repository reference to your development project

Now you can get back to development knowing you have a lightning fast and reliable way to deploy your changes to production.

Cheers,

-Sv

comments powered by Disqus