I’m a big fan of the $5/mo droplet that Digital Ocean offer. For that price, you get a VPS in the cloud with 512MB RAM, a single core CPU (2.4GHz Xeon), 20GB of SSD storage and 1TB of bandwidth – perfect for testing things and small side projects. I decided to start blogging again, but WordPress has become bloatware. After some digging, Ghost looks like an ideal platform to play with.

UPDATE: Digital Ocean have now lowered their prices, and the $5/mo droplet now features 1GB RAM! This means you no longer need to perform the SWAP section of this tutorial, but it’s still probably worth doing.

UPDATE 2: Ghost has been updated a lot since this tutorial, I’ll write a newer version soon.

Digital Ocean also offer one-click apps, which are pre-built VMs you can spin up in seconds with everything ready to go. Ghost is one of those options, but unfortunately the smallest size droplet it can be installed on is the $10/mo option. Whilst this doesn’t break the bank, Ghost is definitely capable of running on the smaller VM, and it’s an excuse for a blog post.

It’s always good to build your own droplets now and then instead of relying on the one-click apps, you’ll learn a lot about what your server is doing and how to customise things to your liking.

$5 droplet: The only thing we’ll be doing differently is adding SWAP, apart from that, the following steps will install Ghost on any Ubuntu VPS from any provider.

Pre-requisites

We’ll be setting up a simple Ghost blog, on a $5 droplet, using a MySQL backend and nginx as the front door.

This article is up-to-date as of July 2017, using the following software and versions:

Ubuntu 16.04

nginx/1.10.3

ghost/1.0.0-rc.1

mariadb/10.1.25

You won’t need much Linux knowledge, but you should at least know how to traverse around the filesystem, as well as understand basic permissions and user/group settings.

You’ll need to have a domain setup with the appropriate record setup pointing to your droplet. I happen to have a spare domain I’ll use for this post from an old project that I canned: www.pedalcrate.co.uk. Also note, I choose to setup a www domain, and I forward non-www to www in DNS, rather than with nginx – do whatever suits you.

1. Spin Up a Droplet

Login to your Digital Ocean account (click here to get $10 credit, enough for two VMs!) and spin up a Ubuntu $5 droplet in your preferred region.

2. Linux Housekeeping

SSH to your VM as root, it’s time to do some basic housekeeping.

Welcome to Ubuntu 16.04.2 LTS (GNU/Linux 4.4.0-83-generic x86_64) 6 packages can be updated. 0 updates are security updates. root@ghost1-nyc3:~#

I’ve removed some of the irrelevant motd message above, but you may notice your packages are already overdue an update, as packages will have been updated since Digital Ocean last refreshed their Ubuntu image. Run the following commands to get us up-to-date:

root@ghost1-nyc3:~# apt-get update root@ghost1-nyc3:~# apt-get dist-upgrade

Note: dist-upgrade is similar to upgrade but handles dependencies and conflict-resolution better, read heremore info.

Create a non-root user

It’s best practice to create a non-root user with sudo access, and use this user for administration, rather than use the root account directly. We’ll make a user called ghost .

root@ghost1-nyc3:~# adduser ghost root@ghost1-nyc3:~# usermod -a -G www-data ghost root@ghost1-nyc3:~# gpasswd -a ghost sudo

We now have a ghost user with a password you set using adduser , which has sudo access and is a member of one of the default Ubuntu groups www-data .

We’ll need to add the SSH public key from your local machine to /home/ghost/.ssh/authorized_keys .

root@ghost1-nyc3:~# vim /home/ghost/.ssh/authorized_keys root@ghost1-nyc3:~# exit Dans-MBP:~ dwalker$ ssh ghost@45.55.67.153 ghost@ghost1-nyc3:~$

We can now disable remote root login to SSH, and only allow logins with keys. Open up the sshd config file with sudo vim /etc/ssh/sshd_config and change the values to the following:

PermitRootLogin no PasswordAuthentication no

Save the file and restart the SSH daemon.

ghost@ghost1-nyc3:~$ sudo service sshd restart

We now have a VPS with a non-root user that we can login as, and use sudo to execute root commands. We’ve also locked down SSH, so keys are required – nice and secure.

3. Add SWAP Memory

One of the reasons getting Ghost going on a smaller VPS can be an issue is that the installer will fail if there’s not enough memory available – and 512mb isn’t enough! Ghost may be able to work fine on these specs, but installs and upgrades won’t work.

To get around this, we can use SWAP memory. For the uninformed: SWAP allows us to allocate disk space to the operating system for use as RAM. On servers with regular HDDs this can cause performance issues, but as DO only offers SSDs the impact is lessened.

We’re going to add a 1GB SWAP file, which should be plenty for upgrades and emergencies.

ghost@ghost1-nyc3:~$ sudo fallocate -l 1G /swapfile ghost@ghost1-nyc3:~$ sudo chmod 600 /swapfile ghost@ghost1-nyc3:~$ sudo mkswap /swapfile Setting up swapspace version 1, size = 1024 MiB (1073737728 bytes) no label, UUID=2af502dd-d50f-4269-a95f-89eaf60bd567 ghost@ghost1-nyc3:~$ sudo swapon /swapfile

We’ve created our SWAP file, granted it the correct permissions and enabled it. At this point free -m should show that we have a functioning SWAP partition:

ghost@ghost1-nyc3:~$ free -m total used free shared buff/cache available Mem: 488 43 71 2 373 415 Swap: 1023 0 1023

All is well, but if we reboot, we’ll lose our SWAP. In order to make the changes permanent, we’ll need to add the changes to our /etc/fstab file. Open the file using vim and add the file to the end, it should look something like this:

LABEL=cloudimg-rootfs / ext4 defaults 0 0 /swapfile none swap sw 0 0

Note: The spacing isn’t important, but it’s nice to keep things neat

Swappiness

There’s some additional tweaks we can make to SWAP for better performance. The exact numbers you can use will vary from situation to situation, and people have their own opinions about what values to use.

vm.swappiness – a numer between 0-100 that dictates how ‘swappy’ Linux acts. Setting this to 20 means that Linux will begin to SWAP when memory usage hits 80%. Setting this value to 0 would disable SWAP, whilst 100 would always utilise it. We will set it to 10.

Setting this variable is similar to SWAP – we run a command to make the change immediately, then edit a config file to make the change persist over reboots.

ghost@ghost1-nyc3:~$ sudo sysctl vm.swappiness vm.swappiness = 60 ghost@ghost1-nyc3:~$ sudo sysctl vm.swappiness=10 vm.swappiness = 10

Open your sysctl file with vim by typing sudo vim /etc/sysctl and add the following value:

vm.swappiness=10

4. Install Required Software

nginx

In order to accept connections to our server, deal with SSL and more, we’ll need nginx. Ghost will listen privately on port 2368 , nginx will listen publicly on 80 and 443 (http and https) and proxy requests to Ghost.

ghost@ghost1-nyc3:~$ sudo apt-get install nginx

MariaDB (MySQL)

For those out of the loop, Oracle (evil) acquired MySQL. Because of this, some of the original developers forked MySQL and created MariaDB – a community maintained version intended to remain free forever.

ghost@ghost1-nyc3:~$ sudo apt-get install mariadb-server ghost@ghost1-nyc3:~$ sudo mysql_secure_installation

Configure MySQL

Open up the MySQL console and run the following commands to create a database for Ghost:

sudo mysql -u root -p Enter password: MariaDB [(none)]> CREATE USER 'ghost'@'localhost' identified by 'SuperSecretAwesomePassWord'; Query OK, 0 rows affected (0.01 sec) MariaDB [(none)]> CREATE DATABASE ghost_live; Query OK, 1 row affected (0.00 sec) MariaDB [(none)]> GRANT ALL PRIVILEGES ON ghost_live.* to 'ghost'@'localhost' IDENTIFIED BY 'SuperSecretAwesomePassWord'; Query OK, 0 rows affected (0.00 sec) MariaDB [(none)]> FLUSH PRIVILEGES; Query OK, 0 rows affected (0.00 sec)

Node and NPM

Node and NPM has a slightly longer install process, we also include build-essential as it is required by Ghost.

ghost@ghost1-nyc3:~$ curl https://deb.nodesource.com/setup_6.x -o nodesource_setup.sh ghost@ghost1-nyc3:~$ sudo chmod u+x nodesource_setup.sh ghost@ghost1-nyc3:~$ sudo ./nodesource_setup.sh ghost@ghost1-nyc3:~$ sudo apt-get install nodejs ghost@ghost1-nyc3:~$ sudo apt-get install build-essential

LetsEncrypt (certbot)

Of course we will want our new site to be https, and we’ll use LetsEncrypt to get a free SSL cert using certbot for this (you’ll need to press [Enter] to complete this):

ghost@ghost1-nyc3:~ $ sudo add-apt-repository ppa:certbot/certbot

Now we should be able to update apt-get and install the certbot software:

ghost@ghost1-nyc3:~ $ sudo apt-get update ghost@ghost1-nyc3:~ $ sudo apt-get install certbot

We don’t have an SSL certificate just yet, but we’ll get to that part later.

Install ghost-cli

Finally we can install the Ghost CLI app that will let us install and manage Ghost. We’ll also need to add the repo source for Yarn in order to install Yarn and ghost-cli .

ghost@ghost1-nyc3:~$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list ghost@ghost1-nyc3:~$ sudo apt-get update ghost@ghost1-nyc3:~$ sudo apt-get install yarn ghost@ghost1-nyc3:~$ sudo yarn global add ghost-cli ghost@ghost1-nyc3:~$ sudo npm i -g ghost-cli

5. Setup UFW (Firewall)

Ubuntu uses UFW (Uncomplicated Firewall), although by default it will be disabled. We can quickly and easily enable it, as well as add some rules for nginx:

ghost@ghost1-nyc3:~ $ sudo ufw status Status: inactive ghost@ghost1-nyc3:~ $ sudo ufw allow 'Nginx Full' Rules updated Rules updated (v6) ghost@ghost1-nyc3:~ $ sudo ufw enable Command may disrupt existing ssh connections. Proceed with operation (y|n)? y Firewall is active and enabled on system startup ghost@ghost1-nyc3:~ $ sudo ufw status verbose Status: active Logging: on (low) Default: deny (incoming), allow (outgoing), disabled (routed) New profiles: skip To Action From -- ------ ---- 80,443/tcp (Nginx Full) ALLOW IN Anywhere 80,443/tcp (Nginx Full (v6)) ALLOW IN Anywhere (v6)

We’ll need to make sure we can get back in to the server, so make sure to allow SSH access.

ghost@ghost1-nyc3:~ $ sudo ufw allow ssh Rule added Rule added (v6) ghost@ghost1-nyc3:~ $ sudo ufw status Status: active To Action From -- ------ ---- Nginx Full ALLOW Anywhere 22 ALLOW Anywhere Nginx Full (v6) ALLOW Anywhere (v6) 22 (v6) ALLOW Anywhere (v6)

There’s additional security steps you can optionally take here. For example, if you have a static IP for your connection or VPN, you can lock down SSH to only allow from that address. You can also run SSH on a non-standard port which is common good practice – this guide is by no means complete, so feel free to adjust as you wish.

6. Take a Snapshot

We’re almost ready to configure Ghost. The server has our user accounts, software and config all ready to go. At this point, it’s often a good idea to take a snapshot in the Digital Ocean control panel and save it as something like ‘ghost-base’. This means any time you need to setup a Ghost droplet, you can spin up your own one-click app.

7. Pre-configure nginx

I’ve ran into issues using the CLI Ghost installer when trying to get an SSL certificate and nginx setup, so I choose to do the first bit manually and then run the installer. The installer will repeat the LetsEncrypt steps later but it doesn’t matter, I’ve found it sometimes doesn’t work unless you do this step, but you might be fine skipping ahead to #8.

Let’s delete the default config and add our own basic HTTP server for LetsEncrypt domain verification – we’ll use ghost-cli to replace this config later:

ghost@ghost1-nyc3:~ $ sudo rm -rf /etc/nginx/sites-available/default ghost@ghost1-nyc3:~ $ sudo rm -rf /etc/nginx/sites-enabled/default ghost@ghost1-nyc3:~ $ sudo vim /etc/nginx/sites-available/www.pedalcrate.co.uk.conf server { listen 80; server_name www.pedalcrate.co.uk; access_log /var/log/nginx/www.pedalcrate.co.uk.log; location /.well-known { root /var/www/ghost; allow all; } } ghost@ghost1-nyc3:~ $ sudo ln -s /etc/nginx/sites-available/www.pedalcrate.co.uk.conf /etc/nginx/sites-enabled/www.pedalcrate.co.uk.conf

We’ve now got our config in place and enabled it, we just need to reload nginx for the changes to take effect. It’s a good habit to always verify your nginx config with nginx -t before you try and reload/restart the service.

ghost@ghost1-nyc3:~ $ sudo nginx -t nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful ghost@ghost1-nyc3:~ $ sudo service nginx restart

We’ve created a temporary server using nginx, that’s listening on port 80 and will allow access a folder called .well-known – used by LetsEncrypt for initial verification. Let’s create the relevant folders:

ghost@ghost1-nyc3:~$ sudo mkdir /var/www/ghost ghost@ghost1-nyc3:~$ sudo chown ghost:www-data /var/www/ghost ghost@ghost1-nyc3:~$ mkdir /var/www/ghost/.well-known

Everything is now in place for us to use the LetsEncrypt service.

8. Add SSL to nginx

We now use certbot , specifying the webroot folder we created earlier and the domain we’re using:

ghost@ghost1-nyc3:~ $ sudo certbot certonly --webroot --webroot-path=/var/www/ghost -d www.pedalcrate.co.uk Saving debug log to /var/log/letsencrypt/letsencrypt.log Obtaining a new certificate Performing the following challenges: http-01 challenge for www.pedalcrate.co.uk Using the webroot path /var/www/ghost for all unmatched domains. Waiting for verification... Cleaning up challenges IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at /etc/letsencrypt/live/www.pedalcrate.co.uk/fullchain.pem. Your cert will expire on 2017-10-20. To obtain a new or tweaked version of this certificate in the future, simply run certbot again. To non-interactively renew *all* of your certificates, run "certbot renew" - If you like Certbot, please consider supporting our work by: Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate Donating to EFF: https://eff.org/donate-le ghost@ghost1-nyc3:~ $

We now have certificates installed and we can get on with installing Ghost using the CLI installer.

9. Install Ghost

We’ll need to create a directory for it to live in, and allocate the correct owner. We’ll also need to erase the nginx config we made earlier or the tool won’t write config out.

During the installer: use your https domain as the blog URL , allow setup of nginx and SSL, and you can answer n to mysql user creation as we’ve already done that step.

ghost@ghost1-nyc3:~ $ rm -rf /etc/nginx/sites-available/www.pedalcrate.co.uk.conf/ ghost@ghost1-nyc3:~ $ rm -rf /etc/nginx/sites-enabled/www.pedalcrate.co.uk.conf/ ghost@ghost1-nyc3:~ $ cd /var/www/ghost ghost@ghost1-nyc3:/var/www/ghost$ rm -rf .well-known/ ghost@ghost1-nyc3:/var/www/ghost$ ghost install ✔ Checking system Node.js version ✔ Checking current folder permissions ✔ Checking operating system ✔ Checking MySQL is installed ✔ Checking for latest Ghost version ✔ Setting up install directory ✔ Downloading and installing Ghost v1.0.0 ✔ Finishing install process ? Enter your blog URL: https://www.pedalcrate.co.uk ? Enter your MySQL hostname: localhost ? Enter your MySQL username: ghost ? Enter your MySQL password: [hidden] ? Enter your Ghost database name: ghost_live ✔ Configuring Ghost ✔ Setting up instance Running sudo command: chown -R ghost:ghost /var/www/ghost/content ✔ Setting up "ghost" system user ? Do you wish to set up Nginx? Yes ✔ Creating nginx config file at /var/www/ghost/system/files/www.pedalcrate.co.uk.conf Running sudo command: ln -sf /var/www/ghost/system/files/www.pedalcrate.co.uk.conf /etc/nginx/sites-available/www.pedalcrate.co.uk.conf Running sudo command: ln -sf /etc/nginx/sites-available/www.pedalcrate.co.uk.conf /etc/nginx/sites-enabled/www.pedalcrate.co.uk.conf Running sudo command: service nginx restart ✔ Setting up Nginx ? Do you wish to set up SSL? Yes ? Enter your email (used for Let's Encrypt notifications) dan@danwalker.com ✔ Creating ssl security parameters file at /var/www/ghost/system/files/ssl-params.conf ✔ Creating ssl config file at /var/www/ghost/system/files/www.pedalcrate.co.uk-ssl.conf Running sudo command: ln -sf /var/www/ghost/system/files/www.pedalcrate.co.uk-ssl.conf /etc/nginx/sites-available/www.pedalcrate.co.uk-ssl.conf Running sudo command: ln -sf /etc/nginx/sites-available/www.pedalcrate.co.uk-ssl.conf /etc/nginx/sites-enabled/www.pedalcrate.co.uk-ssl.conf Running sudo command: service nginx restart ✔ Setting up SSL ? Do you wish to set up "ghost" mysql user? No ℹ Setting up "ghost" mysql user [skipped] ? Do you wish to set up Systemd? Yes ✔ Creating systemd service file at /var/www/ghost/system/files/ghost_pedalcrate-co-uk.service Running sudo command: ln -sf /var/www/ghost/system/files/ghost_pedalcrate-co-uk.service /lib/systemd/system/ghost_pedalcrate-co-uk.service Running sudo command: systemctl daemon-reload ✔ Setting up Systemd ✔ Running database migrations ? Do you want to start Ghost? Yes ✔ Validating config Running sudo command: systemctl start ghost_pedalcrate-co-uk ✔ Starting Ghost You can access your blog at https://www.pedalcrate.co.uk Ghost uses direct mail by default To set up an alternative email method read our docs at https://docs.ghost.org/docs/mail-config

Once this is complete, you should be able to browse to your blog!

502 Bad Gateway

If you instead are greeted by an nginx 502 Bad Gateway error, you may have a port mismatch. For some reason, sometimes when I’ve installed Ghost, it listens on a different port to the one nginx is configured to foward to. We can check this with netstat -plotn :

ghost@ghost1-nyc3:~ $ sudo netstat -plotn Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN 12047/mysqld off (0.00/0/0) tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 25721/nginx -g daem off (0.00/0/0) tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 12277/sshd off (0.00/0/0) tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 25721/nginx -g daem off (0.00/0/0) tcp 0 0 127.0.0.1:2368 0.0.0.0:* LISTEN 24521/nodejs off (0.00/0/0) tcp6 0 0 :::80 :::* LISTEN 25721/nginx -g daem off (0.00/0/0) tcp6 0 0 :::22 :::* LISTEN 12277/sshd off (0.00/0/0) tcp6 0 0 :::443 :::* LISTEN 25721/nginx -g daem off (0.00/0/0)

We can see that nginx is listening on tcp/80 tcp/443 on both IPv4 and IPv6, MySQL is listening locally on its native port of tcp/3306 , and ghost is listening on tcp/2368 . Let’s check that against what we have configured in nginx:

ghost@ghost1-nyc3:~ $ cat /etc/nginx/sites-enabled/www.pedalcrate.co.uk-ssl.conf | grep 127 proxy_pass http://127.0.0.1:2369;

Whoops, looks like it’s forwarding on to tcp/2369 , which no one is listening to. If this has happened, you can choose to edit the Ghost config file, or nginx. I chose nginx:

ghost@ghost1-nyc3:~ $ sudo sed 's/2369/2368/' -i /etc/nginx/sites-available/www.pedalcrate.co.uk*conf ghost@ghost1-nyc3:~ $ sudo nginx -t nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful ghost@ghost1-nyc3:~ $ sudo service nginx restart

You should now be greeted (if not already) by Ghost:

Wrap-up

Hopefully everything worked, if not, leave a comment below or catch me in the HangOps and DevOps Chat Slack communities.

Past this point, I’d recommend downloading the Ghost desktop app and tying it to your new blog, it’s pretty seamless and is a breath of fresh air if you’re coming from WordPress.

Multi-Tenancy

We setup a single tenancy server. It’s easily possible to host multiple Ghost sites on a server. You’ll need to create new MySQL details and /var/www/<name> directory, you should be able to accomplish most of the rest with certbot and ghost-cli , specifying a new domain/directory where required, using the commands in this article.

You will need to ensure your local Ghost installs are listening on different ports (usually tcp/2369 onwards), and the nginx config reflects this.

If You Enjoyed Reading

Using this Digital Ocean link will give you $10 in credits, and I’ll get some too which gives me free resources for making these kind of blog posts. Let me know if you’d like to see any similar posts or spotted any issues in this one.

Thanks for reading.