Installing ghost on production linux server (CentOS)

Pre-requisites

  1. Root access to your server or have access to an account that has sudo powers on your servers – Instructions here to see how to give a linux user sudo powers.
  2. nodejs and npm installed – Instructions here.

Installing

Where to install

First let’s decide where to put the ghost directory. I chose /var/www/, because that’s where websites are usually on CentOS. You can choose whichever directory you like. However note that we will be running ghost using a specialized user so make sure you can give another user permission to access that directory. That means don’t put it in /root/. Go to your chosen directory and get the latest ghost file.

[ahmed@amayem ~]$ cd /var/www
[ahmed@amayem www]$ curl -L https://ghost.org/zip/ghost-latest.zip -o ghost.zip
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
102   307    0   307    0     0    212      0 --:--:--  0:00:01 --:--:--     0Warning: Failed to create the file ghost.zip
  0 1823k    0  4096    0     0   2097      0  0:14:50  0:00:01  0:14:49  2097
curl: (23) Failed writing body (0 != 4096)

Hmm that’s not good. Looks like a permissions issue. Let’s check if that is the case:

[ahmed@amayem www]$ ls -l /var | grep www
drwxr-xr-x  6 root  root  4096 Oct  5  2012 www

Yup. The folder is owner by root and only root can write to it. We need to sudo.

[ahmed@amayem www]$ sudo curl -L https://ghost.org/zip/ghost-latest.zip -o ghost.zip
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 1823k  100 1823k    0     0   969k      0  0:00:01  0:00:01 --:--:-- 2178k

Ah looks much better. Let’s check the new file is there.

[ahmed@amayem www]$ ls
cgi-bin  error  ghost.zip  html  icons

Good! Now we unzip it into a directory called ghost. We will once again need to use sudo.

[ahmed@amayem www]$ sudo unzip -uo ghost.zip -d ghost
Archive:  ghost.zip
inflating: ghost/Gruntfile.js      
inflating: ghost/LICENSE
...

A huge load of lines should appear. I truncated the output to preserve space. Let’s check the directory has been made.

[ahmed@amayem www]$ ls
cgi-bin  error  ghost  ghost.zip  html  icons

Looks good. We don’t need the ghost.zip file so let’s remove it. If we ever need it again we can always download the latest version.

[ahmed@amayem www]$ sudo rm ghost.zip 
[ahmed@amayem www]$ ls
cgi-bin  error  ghost  html  icons

Now let’s enter the ghost directory and see what we have.

[ahmed@amayem ghost]$ ls
bower.json  config.example.js  content  core  Gruntfile.js  index.js  LICENSE  package.json  README.md

Looks good! Let’s install the production version of ghost.

[ahmed@amayem ghost]$ npm install --production

In the beginning things will look good with a lot of green. Then things will not look so good with a lot of red, with an output that looks like this.

Error: EACCES, mkdir '/var/www/ghost/node_modules'
npm ERR!  { [Error: EACCES, mkdir '/var/www/ghost/node_modules']
npm ERR!   errno: 3,
npm ERR!   code: 'EACCES',
npm ERR!   path: '/var/www/ghost/node_modules',
npm ERR!   fstream_type: 'Directory',
npm ERR!   fstream_path: '/var/www/ghost/node_modules/connect-slashes',
npm ERR!   fstream_class: 'DirWriter',
npm ERR!   fstream_stack: 
npm ERR!    [ '/usr/lib/node_modules/fstream/lib/writer.js:171:23',
npm ERR!      '/usr/lib/node_modules/mkdirp/index.js:37:53',
npm ERR!      'Object.oncomplete (fs.js:107:15)' ] }
npm ERR! 
npm ERR! Please try running this command again as root/Administrator.

Argh we need another sudo.

[ahmed@amayem ghost]$ sudo npm install --production

This time you should only see green. After a lot of output, ghost should be ready to go. Let’s see what we have in the directory first.

[ahmed@amayem ghost]$ ls
bower.json  config.example.js  content  core  Gruntfile.js  index.js  LICENSE  node_modules  package.json  README.md

Looks like the directory node_modules was made. Let’s give ghost a run then.

[ahmed@amayem ghost]$ npm start

> ghost@0.4.2 start /var/www/ghost
> node index


ERROR: Could not open config.js for write. 
 /var/www/ghost 
 Please check your deployment for config.js or config.example.js. 
 Error: Could not open config.js for write.
    at WriteStream.<anonymous> (/var/www/ghost/core/bootstrap.js:46:36)
    at WriteStream.EventEmitter.emit (events.js:117:20)
    at WriteStream.<anonymous> (fs.js:1657:12)
    at OpenReq.Req.done (/var/www/ghost/node_modules/express-hbs/node_modules/readdirp/node_modules/graceful-fs/graceful-fs.js:143:5)
    at OpenReq.done (/var/www/ghost/node_modules/express-hbs/node_modules/readdirp/node_modules/graceful-fs/graceful-fs.js:63:22)
    at Object.oncomplete (fs.js:107:15) 

Argh! Again with the sudo.

[ahmed@amayem ghost]$ sudo npm start

> ghost@0.4.2 start /var/www/ghost
> node index

Ghost is running in development... 
Listening on 127.0.0.1:2368 
Url configured as: http://my-ghost-blog.com 
Ctrl+C to shut down

That’s what we wanted. I can also run it using index.js.

[ahmed@amayem ghost]$ node index.js 

ERROR: SQLITE_READONLY: attempt to write a readonly database, sql: update "settings" set "created_at" = ?, "created_by" = ?, "id" = ?, "key" = ?, "type" = ?, "updated_at" = ?, "updated_by" = ?, "uuid" = ?, "value" = ? where "id" = ?, bindings: Thu Apr 10 2014 18:57:56 GMT+0400 (MSD),1,16,installedApps,app,Thu Apr 10 2014 19:00:12 GMT+0400 (MSD),1,0f92d9a8-3531-481c-889d-c75023848aa9,[],16 

 Error: SQLITE_READONLY: attempt to write a readonly database, sql: update "settings" set "created_at" = ?, "created_by" = ?, "id" = ?, "key" = ?, "type" = ?, "updated_at" = ?, "updated_by" = ?, "uuid" = ?, "value" = ? where "id" = ?, bindings: Thu Apr 10 2014 18:57:56 GMT+0400 (MSD),1,16,installedApps,app,Thu Apr 10 2014 19:00:12 GMT+0400 (MSD),1,0f92d9a8-3531-481c-889d-c75023848aa9,[],16
    at /var/www/ghost/node_modules/knex/clients/server/base.js:73:22
    at process._tickDomainCallback (node.js:459:13)
From previous event:
    at ClientBase.extend.getConnection (/var/www/ghost/node_modules/knex/clients/server/base.js:90:59)
    at ClientBase.extend.query (/var/www/ghost/node_modules/knex/clients/server/base.js:36:22)
    at null.<anonymous> (/var/www/ghost/node_modules/knex/lib/common.js:52:28)
From previous event:
    at exports.Common.then (/var/www/ghost/node_modules/knex/lib/common.js:50:31)
    at Promise.all.bind.then.then.attributes.(anonymous function).(anonymous function) (/var/www/ghost/node_modules/bookshelf/dialects/sql/model.js:185:38)
From previous event: 


ERROR: SQLITE_READONLY: attempt to write a readonly database, sql: update "settings" set "created_at" = ?, "created_by" = ?, "id" = ?, "key" = ?, "type" = ?, "updated_at" = ?, "updated_by" = ?, "uuid" = ?, "value" = ? where "id" = ?, bindings: Thu Apr 10 2014 18:57:56 GMT+0400 (MSD),1,16,installedApps,app,Thu Apr 10 2014 19:00:12 GMT+0400 (MSD),1,0f92d9a8-3531-481c-889d-c75023848aa9,[],16 
 The app will not be loaded 
 Check with the app creator, or read the app documentation for more details on app requirements 

Ghost is running in development... 
Listening on 127.0.0.1:2368 
Url configured as: http://my-ghost-blog.com 
Ctrl+C to shut down

NOOOO! It’s the sudo problem again.

[ahmed@amayem ghost]$ sudo node index.js
Ghost is running in development... 
Listening on 127.0.0.1:2368 
Url configured as: http://my-ghost-blog.com 
Ctrl+C to shut down

Solving the sudo problem

Let’s solve this sudo issue. By using sudo I am effectively running ghost as root. That means I am exposing an application to the outside world with root priveleges, which is not very good security-wise. We can check the truth of this by opening another terminal and running following command to see who is running node index.js:

[ahmed@amayem ~]$ ps aux | grep index.js
root      9439  0.0  0.0   9180  1728 pts/0    S+   20:19   0:00 sudo node index.js
root      9440  0.1  0.9 120900 40228 pts/0    Sl+  20:19   0:00 node index.js
ahmed     9478  0.0  0.0   5508   732 pts/1    S+   20:26   0:00 grep index.js

Instead let’s make a ghost user that has ownership over the ghost files, that way we can avoid all this sudo stuff.

Adding the ghost user

First we add the user

[ahmed@amayem ghost]$ sudo useradd -rm ghost -U

The command above makes a service user account called ‘ghost’ with a home directory and also adds him to a group of the same name. If you omit the -m flag you will face some problems if you want to run forever afterwards as I outline here.

Let’s check the permissions of the files in the ghost directory.

[ahmed@amayem ghost]$ ls -l /var/www/ghost/
total 92
-rw-r--r--  1 root  root    558 Mar 26 18:39 bower.json
-rw-r--r--  1 root  root   4108 Mar 26 18:39 config.example.js
-rw-r--r--  1 root  root   4108 Apr 10 18:57 config.js
drwxr-xr-x  6 root  root   4096 Apr 10 18:39 content
drwxr-xr-x  6 root  root   4096 Apr 10 18:39 core
-rw-r--r--  1 root  root  36979 Mar 26 18:39 Gruntfile.js
-rw-r--r--  1 root  root    270 Mar 26 18:39 index.js
-rw-r--r--  1 root  root   1099 Mar 26 18:39 LICENSE
drwxr-xr-x 27 ahmed ahmed  4096 Apr 10 18:54 node_modules
-rw-r--r--  1 root  root   2628 Mar 26 18:39 package.json
-rw-r--r--  1 root  root   5627 Mar 26 18:39 README.md

I notice it’s mostly root. We want to change that to say ‘ghost’ in both columns. Let’s change the ownership and check again.

[ahmed@amayem ghost]$ sudo chown -R ghost:ghost /var/www/ghost
[ahmed@amayem ghost]$ ls -l /var/www/ghost/
total 92
-rw-r--r--  1 ghost ghost   558 Mar 26 18:39 bower.json
-rw-r--r--  1 ghost ghost  4108 Mar 26 18:39 config.example.js
-rw-r--r--  1 ghost ghost  4108 Apr 10 18:57 config.js
drwxr-xr-x  6 ghost ghost  4096 Apr 10 18:39 content
drwxr-xr-x  6 ghost ghost  4096 Apr 10 18:39 core
-rw-r--r--  1 ghost ghost 36979 Mar 26 18:39 Gruntfile.js
-rw-r--r--  1 ghost ghost   270 Mar 26 18:39 index.js
-rw-r--r--  1 ghost ghost  1099 Mar 26 18:39 LICENSE
drwxr-xr-x 27 ghost ghost  4096 Apr 10 18:54 node_modules
-rw-r--r--  1 ghost ghost  2628 Mar 26 18:39 package.json
-rw-r--r--  1 ghost ghost  5627 Mar 26 18:39 README.md

Beautiful! Now let’s switch to the ghost user and run node again without sudo.

[ahmed@amayem ghost]$ sudo su ghost
[ghost@amayem ghost]$ node index.js 
Ghost is running in development... 
Listening on 127.0.0.1:2368 
Url configured as: http://my-ghost-blog.com 
Ctrl+C to shut down

Thats what we want.

Let’s take a look at those lines.

  1. Ghost is running in development…I want to run it in production, since I am, afterall, installing it on my production server.
  2. Listening on 127.0.0.1:2368
    Url configured as: http://my-ghost-blog.com
    I want it to point to my server’s ip and my domain.

Looks like we need to do some configuration.

Configuration

Development versus Production

Let’s take a look at config.js to see what environments are already pre-configured. You should see a config Javascript Object that looks something like the following.

config = {
    development: {
        …
    }
    production: {
        …
    }
    testing: {
        …
    }
    …
}

From this we learn that there is a pre-configured environment called production. We need to tell node to use the production environment by changing the NODE_ENV enviornment variable. Run ghost again as follows:

[ghost@amayem ghost]$ NODE_ENV=production npm start

> ghost@0.4.2 start /var/www/ghost
> node index

Ghost is running... 
Your blog is now available on http://my-ghost-blog.com 
Ctrl+C to shut down

Now it’s saying Ghost is running... looks like we succeeded in getting it to use the production environment.

Configuring the ip and web address.

Your web address is the domain you bought and any subdomains you made on it. In my case it is ahmed.amayem.com (my domain is amayem.com and ahmed is a subdomain).

You can find your ip as follows:

[ahmed@amayem ~]$ ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: venet0: <BROADCAST,POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN 
    link/void 
    inet 127.0.0.1/32 scope host venet0
    inet 192.241.121.253/32 brd 192.241.121.253 scope global venet0:0
    inet 192.241.121.254/32 brd 192.241.121.254 scope global venet0:1

My ips in this example are 192.241.121.253 and 192.241.121.254. My server has two ips.

Let’s modify the production part of the config object. Change the url to your own url and the host to the ip of your server. Issue the following command:

[ghost@amayem ghost]$ vi config.js

Move your cursor using the arrow keys to where you want it. Press i to enter insert mode and make your changes. Exit insert mode using Esc or ctrl+c then save and exit using :x.

This is what mine looked like.

// ### Production
// When running Ghost in the wild, use the production environment
// Configure your URL and mail settings here
production: {
    url: 'http://ahmed.amayem.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: '192.241.121.253',
        // Port to be passed to node's `net.Server#listen()`, for iisnode set this to `process.env.PORT`
        port: '2368'
    }
}

Now that my blog is running in production and pointing to the correct ip I would look to see it. Point your browser at your address and port number and you should see it. In my case it was ahmed.amayem.com:2368.

Next Steps

Making sure ghost runs forever

We have our blog running now, but you will notice that our terminal is preocccupied with ghost. If we close our terminal then the blog will close. We want it to run even when I close my terminal. Let’s try the following.

[ghost@amayem ghost]$ npm start &
[1] 11652
[ghost@amayem ghost]$

Looks good, we got our prompt back that means that ghost is running and we don’t have to have our terminal open … but wait what’s this:

> ghost@0.4.2 start /var/www/ghost
> node index

Ghost is running in development... 
Listening on 127.0.0.1:2368 
Url configured as: http://my-ghost-blog.com 
Ctrl+C to shut down

Argh! So close. Looks like that didn’t work. We are left with the following ways:

  1. Using forever with crontabInstructions here
  2. Using supervisor – Intsructions coming soon
  3. Using an init script – Intsructions coming soon

Making the blog available without putting a port number

Now I would like to be able to have my blog show up without the port number, in my case ahmed.amayem.com. If the blog is the only site you have running on your domain then you may just setup ghost to use port 80 (Intsructions coming soon). Otherwise, whether you want to run your blog on a subdomain (like blog.amayem.com) and serve another site on your base domain (e.g. amayem.com), or you just want to serve ghost alone you can always use one of the following servers as a proxy:

  1. Apache – Intsructions coming soon
  2. nginx – Intsructions coming soon
  3. haproxy – Intsructions coming soon

I plan to discuss the differences between these options in later posts.

References

  1. Installing ghost on linux ghost docs
  2. Cyberciti for the linux commands ps and ip addr show

Ahmed Amayem has written 90 articles

A Web Application Developer Entrepreneur.