Since Xenial Xerus is reaching its end of life, it is time to migrate this blog to a new server running Focal Fossa and to update to Ghost 4.


A server running Focal Fossa must already be set up, as described in the article A Focal Fossa on Digital Ocean.

A registered domain name is obviously necessary as well.

(Optional) Backup

If a running instance of Ghost is to be migrated to the new server, a complete json backup should be taken. The export features can be found under Settings -> Labs -> Export your content. Store the created .json-file on the computer.

The .json-file does not contain any information about themes or other custom content such as images, thus the content folder, i.e. /var/www/bell0bytes/content, of the previous Ghost installation must be backed up as well.

Installing NGINX

Ghost uses nginx as server and revery proxy.


Installing nginx is as easy as it gets:

sudo apt-get install nginx

Firewall Rules

To allow the outside world to communicate with nginx, and thus to later on access the Ghost blog, UFW must be told to allow traffic on the http(s) ports nginx listens to. Since nginx is quite commonly used, ufw comes with pre-defined rules for nginx:

sudo ufw allow 'Nginx Full'

This allows http and https traffic, in and out, on IPv4 and IPv6.

Installing MySQL

Unfortunately, Hello Mr. Ellison, Ghost officially suggests using a MySQL database for Ghost production instances. Although it is obviously possible to use MariaDB as well, this article will keep true with the official Ghost instructions and use MySQL. Fun Fact: My and Maria are the two daughters of the original creator of both databases, Ulf Michael Widenius.

To install MySQL, simply invoke aptitude:

sudo apt-get install mysql-server

Once the installation is finished, log in to the newly created instance and set up the root password, obviously using a secure password instead of pwd:

sudo mysql

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'pwd';


Last, but not least, the deployed MySQL server instance should be hardened. Thankfully there is a nice script to do just that, the mysql_secure_installation script. Just run it as superuser do and follow the on-screen instructions.

sudo mysql_secure_installation

NodeJS, NPM and the Ghost-CLI

Ghost uses nodejs and npm. To install a supported version of both, first add the NodeSource APT repository for Node 14 to Aptitude:

curl -sL | sudo -E bash

And then install node.js:

sudo apt-get install -y nodejs

Finally, globally install the commandline tool of Ghost using npm:

sudo npm install ghost-cli@latest -g

Installing Ghost

With the commandline tool installed, installing Ghost itself is now very easy.

First, a directory must be created with the proper owner and permissions:

sudo mkdir -p /var/www/bell0bytes
sudo chown symplectos:symplectos /var/www/bell0bytes
sudo chmod 755 /var/www/bell0bytes
cd /var/www/bell0bytes

Finally start the installation of Ghost and follow the on-screen instructions:

ghost install

Blog URL

The blog URL specifies the location of the ghost blog, i.e. If HTTPS is used, the Ghost-CLI will automatically invoke LetsEncrypt to set up SSL.

MySQL Hostname

The hostname of the MySQL instance. When following this article, the hostname would be localhost.

MySQL Username and Password

Specify the username and password as set up above, i.e. root and pwd.

Ghost MySQL User

Allow the Ghost-CLI to create a non-root user to access and edit the Ghost relevant databases.

Configure NGINX

Allow Ghost to set up nginx.

Set up SSL

Allow Ghost to use LetsEncrypt to set up SSL as well.

E-Mail for the SSL Certificate

SSL certificates require an e-mail address to be able to contact administrators of websites.

Set up a SystemD Process

Allow the Ghost-CLI to set up a SystemD process.

Start Ghost

Select yes, then follow the on-screen instructions, i.e. go to https://yourdom.ain/ghost and finish setting up the Ghost instance.

(Optional) Restoring Old Content

Once Ghost 4 is up and running, delete the demo posts and pages, and then copy he themes and images from the old instance can to the content folder of the new instance.

Once done, go to Settings -> Labs -> Import Content and upload the previously backed up .json file.

(Optional) Disabling Login

To disable the newly created ghost user to be able to log in to the system, modify its shell to /bin/false with the usermod command as follows:

sudo usermod --shell /bin/false ghost

Protecting Ghost with Fail2Ban

Ghost already implements two types of login restrictions, namely:

  • Account-level lockout after 5 failed login attempts
  • IP-Level login attempt throttling after one login attempt per 2 seconds

These settings should be reasonably secure and implementing fail2ban on top of those protection methods may provide diminishing returns, but better safe than sorry. Following a previous article on how to secure nginx with fail2ban, a filter and jail can for Ghost can easily be configured.

Failed login attempts do generate a 404 in the /var/log/nginx/access.log file in the following form:

2001:7e8:c87b:d800:90d5:c60d:8804:6772 - - [20/Mar/2021:19:04:35 +0000] "POST /ghost/api/canary/admin/session HTTP/2.0" 422 299 "" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0"

Thus a fail2ban filter can be written and added to a jail for Ghosts:

sudo vim /etc/fail2ban/filter.d/ghost.conf

# ghost bruteforce filter
failregex = ^<HOST> .* "POST /ghost/api/canary/admin/session
ignoreregex =

sudo vim /etc/fail2ban/jail.d/ghost.conf

enabled = true
filter = ghost
action = ufw
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 120
bantime = 1800

This jail definition would ban users for half an hour after ten unsuccessful login attempts within two minutes.

Remember to reload the fail2ban-client and to check the status:

sudo fail2ban-client reload
sudo fail2ban-client status

|- Number of jail:      4
`- Jail list:   ghost, nginx-botsearch, nginx-http-auth, sshd

The Road Ahead

Have a look at this older article, describing how to add LaTeX and Code Highlighting to Ghost.