Ghost 0.11.3 to 0.11.4 Upgrade on DigitalOcean Droplet

Multicurrency Money Bags
Taken from Shutterstock, watermarks and all.

Upgrading your Cost of Ownership

Previously, this blog was running Ghost v0.11.3 on the smallest available DigitalOcean droplet:

Pricing 512MB

As an FYI it appears that Ghost v0.11.4 requires resizing the droplet to the next size (at minimum):

Pricing 1GB

Otherwise the service cannot successfully restart and/or running npm install --production to update the dependencies will fail. I encountered both situations when trying to update without scaling up the droplet size.

I weighed the pros and cons of the upgrade as such, since right now I'm pretty sure I'm the only one here and my traffic doesn't really warrant doubling the monthly cost of my blog. That said, stopping upgrades comes with the drawback that as 0.11.3 becomes increasingly antiquated ... it will be harder and harder to do an in place upgrade. And what if one of those upgrades patches a security vulnerability, you know what I mean?

So here we are, upgrading Ghost.

The Actual Upgrade Process

I found that, for some reason, when I grabbed the latest Ghost release from ghost.org that I was still not able to get the final step (npm install) to complete. Instead, I spun up a new DigitalOcean "One-Click" Ghost droplet (already on 0.11.4), let it run its initial setup when you SSH into the droplet for the first time, and then prepared to tar the /var/www/ghost directory and its subdirectories like so:

$ cd /var/www/
$ cp -a ghost ghost-0.11.4

Before taring up the directory, I updated the ghost-0.11.4/config.js file to use the my settings from my current droplet. Specifically, I wanted to make sure that url: 'http://IP-ADDRESS' was updated to 'http://MY-DOMAIN', mail: {} was updated to use my Mailgun settings, and the password being supplied to the MySQL database under database.client.connection.password was updated to use the password on my current droplet. As part of the one-click process, the password supplied is automatically generated for each droplet. After all that:

$ tar -czf ghost-0.11.4.tgz ghost-0.11.4

Then I put the tar file on my existing Ghost droplet and killed the new droplet.

Before starting the upgrade process, I tared my existing ghost for quick, complete restores if necessary:

$ cd /var/www/
$ cp -a ghost ghost-current
$ tar -czf ghost-current.tgz ghost-current

And also backed up the MySQL database that Ghost is actually using:

$ mysqldump -u ghost -p ghost > ghostdb_backup_yyyymmdd.sql

With those handy backups in place, I got started:

$ service ghost stop
$ tar -xzf ghost-0.11.4.tgz
$ rm -rf ghost/core
$ cp -a ghost-0.11.4/core/ ghost/
$ for ext in md js json; do cp -a ghost-0.11.4/*.${ext} ghost/; done
$ rm -rf ghost/node_modules
$ cp -a ghost-0.11.4/node_modules ghost/

Optional

If you are using the Casper theme you'll want to upgrade that as well:

$ rm -rf ghost/content/themes/casper
$ cp -a ghost-0.11.4/content/themes/casper ghost/content/themes/

If you did not use cp -a above, then you might run into permissions issues. cp -a is equivalent to cp -pPR, which preserves the permissions and attributes (see man cp for which attributes) of copied files and is also recursive when copying a directory / directory tree. If this is the case, you'll want to fix the permissions on your files like so:

$ chown -R ghost:ghost ghost/* 
$ chown -R root:root ghost/config.js

After all that is done, including the Optional instructions if needed/desired, you need to update the dependencies with:

$ cd /var/www/ghost
$ npm install --production

If npm completes without errors (warnings are fine), then all that's left is to restart the ghost service:

$ service ghost restart

Troubleshooting

Wrangling Node Modules

If you encounter an error where the npm process stops and/or throws an error try deleting the node_modules directory, clearing the cache, and re-running the process like so:

$ service ghost stop
$ rm -rf node_modules  
$ npm cache clean  
$ npm install --production 
$ chown -R ghost:ghost node_modules
$ service ghost restart

(Note that when you delete/re-create the node_modules directory as root that you'll need to change the owner and group to ghost as the directory and its elements will have root:root.)

Paying Attention

When I initially did this process I neglected my db password. Now, I did not know this at the time - all I knew is when I (thought I) completed the upgrade process I would encounter a 502 Bad Gateway when I tried to load my web page in a browser. In order to figure out what was wrong, I ran npm start --production which gave me an error like so:

$ npm start --production

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


ERROR: ER_ACCESS_DENIED_ERROR: Access denied for user 'ghost'@'localhost' (using password: YES)

 Error: ER_ACCESS_DENIED_ERROR: Access denied for user 'ghost'@'localhost' (using password: YES)
    at Handshake.Sequence._packetToError (/var/www/ghost/node_modules/mysql/lib/protocol/sequences/Sequence.js:30:14)
    at Handshake.ErrorPacket (/var/www/ghost/node_modules/mysql/lib/protocol/sequences/Handshake.js:91:18)
    at Protocol._parsePacket (/var/www/ghost/node_modules/mysql/lib/protocol/Protocol.js:202:24)
    at Parser.write (/var/www/ghost/node_modules/mysql/lib/protocol/Parser.js:62:12)
    at Protocol.write (/var/www/ghost/node_modules/mysql/lib/protocol/Protocol.js:37:16)
    at Socket.<anonymous> (/var/www/ghost/node_modules/mysql/lib/Connection.js:72:28)
    at emitOne (events.js:77:13)
    at Socket.emit (events.js:169:7)
    at readableAddChunk (_stream_readable.js:146:16)
    at Socket.Readable.push (_stream_readable.js:110:10)
    at TCP.onread (net.js:523:20)
    --------------------
    at Protocol._enqueue (/var/www/ghost/node_modules/mysql/lib/protocol/Protocol.js:110:48)
    at Protocol.handshake (/var/www/ghost/node_modules/mysql/lib/protocol/Protocol.js:42:41)
    at Connection.connect (/var/www/ghost/node_modules/mysql/lib/Connection.js:98:18)
    at /var/www/ghost/node_modules/knex/lib/dialects/mysql/index.js:106:18
    at Promise._execute (/var/www/ghost/node_modules/bluebird/js/release/debuggability.js:300:9)
    at Promise._resolveFromExecutor (/var/www/ghost/node_modules/bluebird/js/release/promise.js:481:18)
    at new Promise (/var/www/ghost/node_modules/bluebird/js/release/promise.js:77:14)
    at Client_MySQL.acquireRawConnection (/var/www/ghost/node_modules/knex/lib/dialects/mysql/index.js:104:12)
    at Object.create (/var/www/ghost/node_modules/knex/lib/client.js:231:16)
    at Pool._createResource (/var/www/ghost/node_modules/generic-pool/lib/generic-pool.js:325:17)
    at Pool._ensureMinimum (/var/www/ghost/node_modules/generic-pool/lib/generic-pool.js:363:12)
    at new Pool (/var/www/ghost/node_modules/generic-pool/lib/generic-pool.js:156:8)
    at Client_MySQL.initializePool (/var/www/ghost/node_modules/knex/lib/client.js:261:17)
    at Client_MySQL.Client (/var/www/ghost/node_modules/knex/lib/client.js:108:12)
    at new Client_MySQL (/var/www/ghost/node_modules/knex/lib/dialects/mysql/index.js:62:20)
    at Knex (/var/www/ghost/node_modules/knex/lib/index.js:60:34)
    at Object.<anonymous> (/var/www/ghost/core/server/data/db/connection.js:54:20)
    at Module._compile (module.js:410:26)
    at Object.Module._extensions..js (module.js:417:10)
    at Module.load (module.js:344:32)
    at Function.Module._load (module.js:301:12)
    at Module.require (module.js:354:17)
    at require (internal/module.js:12:17)

I could see that it wasn't able to connect to the MySQL database, but was initially unsure of why. So, I tried checking user ghost for database ghost in MySQL:

mysql> SELECT * FROM mysql.db WHERE Db='ghost'\G;
*************************** 1. row ***************************
                 Host: localhost
                   Db: ghost
                 User: ghost
          Select_priv: Y
          Insert_priv: Y
          Update_priv: Y
          Delete_priv: Y
          Create_priv: Y
            Drop_priv: Y
           Grant_priv: N
      References_priv: Y
           Index_priv: Y
           Alter_priv: Y
Create_tmp_table_priv: Y
     Lock_tables_priv: Y
     Create_view_priv: Y
       Show_view_priv: Y
  Create_routine_priv: Y
   Alter_routine_priv: Y
         Execute_priv: Y
           Event_priv: Y
         Trigger_priv: Y
1 row in set (0.00 sec)

ERROR:
No query specified

This indicated to me that the permissions were correct, so I tried connecting to mysql myself:

$ mysql --host=127.0.0.1 --port=3306 --user=ghost -p ghost

This connects to MySQL using localhost (127.0.01) on port 3306 as user ghost. Notably, the -p and last ghost are unrelated - the -p tells mysql to prompt me for a password and the final argument, in this case ghost, is the name of the database. If you are using the DigitalOcean One-Click app the passwords for the ghost and root users are in /root/.digitalocean_password. I was able to connect just fine to mysql this way, so I just exited (exit to exit).

Side note: If you do not know what port MySQL is running on and/or have reason to suspect that it is running on a non-default port (default is 3306), then run the following:

$ netstat -tlnp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN      1689/master
tcp        0      0 127.0.0.1:2368          0.0.0.0:*               LISTEN      6195/node
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      1382/mysqld
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      1392/nginx -g daemo
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      1490/sshd
tcp6       0      0 ::1:25                  :::*                    LISTEN      1689/master
tcp6       0      0 :::80                   :::*                    LISTEN      1392/nginx -g daemo
tcp6       0      0 :::22                   :::*                    LISTEN      1490/sshd

Here, you can see/verify that mysqld is running on port 3306.

Back to troubleshooting.

I had a 2 day old snapshot so in a moment of desperation I restored that ... without taking a snapshot of the current state of things because, I supposed, the current state of things was hosed and who wants to keep hosed?

The answer is everyone. Everyone wants to keep hosed. Because until you know "why hosed", you want "hosed". In this case I lost about a day's worth of drafts, oh well. It could be worse. But if I had taken a snapshot before restoring old snapshot, I'd have those drafts ;)

Annnnyway it took a second pair of eyes to make make look at the config.js file and, sure enough, the password was incorrect. d'oh! Obviously it's fixed now.

TL;DR - Remember to update your MySQL password if nothing else. Or your blog will boom.