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:
As an FYI it appears that Ghost v0.11.4 requires resizing the droplet to the next size (at minimum):
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 tar
ing 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 tar
ed 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.