Cheap, secure and fast - Ghost blog set-up step by step

In the following guide, I'll show you step-by-step how to set up Ghost blog on cheap VPS and fine tune it for performance. It requires basic experience with Linux, domain name and about 3h of time.

Why self-hosting?

You can get Ghost Pro for 19$ a month and has a lot of advantages, quoting their page:

(...) Ghost(Pro) will save you both time and money.

If you self host: You'll start with a base cost of around $20/month for a VPS, then you'll need to pay extra for CDN, security and backups services. After that you'll spend a couple of hours installing and setting up everything, and also set aside a couple of hours of your time each month for maintenance + software updates. Which gets expensive pretty quick.
If you use Ghost(Pro): We do all of that for you, automatically, for a single monthly price.

Another important consideration is that when it comes to downtime and security threats, if you self-host, you're on your own. When it comes to Ghost(Pro), we lose sleep so you don't have to.
Lastly, by using Ghost(Pro) you directly fund the future development of Ghost itself. If you use another host, then Ghost doesn't benefit from that money in any way.

Those are all excellent points, but I like to self-host mainly because of three reasons:

  • It's good learning experience
  • I can customise it on very low level (for example optimising images. It’s Ghosts top requested feature.)
  • It's much cheaper, starting from $2.5/month

So continue reading if you consider this as your free time learning experience and love copy pasting console commands, but if you need a blog for Serious Bussiness® go ahead with Ghost Pro.

Step 1 - Choosing a VPS

Cheapest one is Vultr starting at $2.5/month, but it's not available in most of their locations. At the time of writing, only Miami and New York had the cheapest plan available. Another one I can recommend is Digital Ocean at $5/month. Here is my referral code that will give you 10$, enough for two months of hosting: https://m.do.co/c/e8d419b091ce

Both of them are similar, but I'll use Digital Ocean in my examples because I've used it for a longer time and can vouch for it.

After signing up click on Create in the top right and select Droplet from the drop down menu.

Choose an image:

Latest Ubuntu LTS (Long Term Support) is selected by default. We’ll use it because thanks to its popularity you can easily find tutorials and help for common problems.

Choose a size:

5$/month should be enough, and we can always change that later once your blog gains traction.

Choose a datacenter region:

Depends on where do you expect most of your readers from.

Select additional options:

Choose IPv6 and Monitoring

(Those did nothing for me and I had to enable monitoring manually later by running:
curl -sSL https://agent.digitalocean.com/install.sh | sh
and ipv6 following this guide: https://www.digitalocean.com/community/tutorials/how-to-enable-ipv6-for-digitalocean-droplets#enabling-ipv6-on-an-existing-droplet)

Click on New SSH key and add yours. If you don’t have one yet, there is a link to a guide that will help you with creating it.

Last and most difficult step – choose a hostname.

The server should be ready in about a minute after clicking Create.

Step 2 - DNS setup

Before we continue, let’s configure DNS. Add A and AAAA records with your new servers IPs in your domain control panel. I recommend using a subdomain. Here is example using NameCheap

Screenshot-2017-08-04-14.53.08

Step 3 - Initial server setup

Use your favourite SSH client. On Windows, I recommend Kitty: http://www.9bis.net/kitty/?page=Download

Paste your servers IP like this:
Screenshot-2017-08-04-14.57.22

Then click on Connection and set “Seconds between keepalives” to 60. This will prevent disconnect when you leave it for a while.
Go to Connection → SSH → Auth and click on “Browse” to load your SSH key.
Then Connection → Data and fill “Auto-login username” with root

Go back to Sessions, type a name in “Saved Sessions/New Folder” field and click Save and then double clicked saved name in the list to connect.

You’ll probably see a screen with a new key warning, click Yes on it, and you should see welcome screen of your brand new server. Here is a link to initial server setup that will significantly improve your security:
https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-16-04
From now one I’m going to use ‘ghost’ user
Let’s switch to this user:
su - ghost

and install some dependencies first:

sudo apt-get update 
sudo apt-get install build-essential libssl-dev dpkg-dev zlib1g-dev libpcre3 libpcre3-dev unzip libxslt-dev libxslt1-dev libxml2-dev libgd2-xpm-dev libgeoip-dev libgoogle-perftools-dev libperl-dev mysql-server

Then install node version manager as a new user you just created. Warning! When you see install methods like this always download script first and inspect it before running. It’s not safe to run scripts downloaded directly from the internet, but I write them like this for convenience.
Check for latest version here: https://github.com/creationix/nvm At the time of writing this post it’s:

curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
After successful installation you should see some output ending with helpful information which you should follow:

=> Close and reopen your terminal to start using nvm or run the following to use it now:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion

Then install latest long term support node:
nvm install --lts
and enable it:
nvm use --lts

Step 4 - Nginx

Before we install nginx, we’ll have to create swap file because 512 MB of RAM is not enough for compilation. If you've chosen server with 1GB or more RAM you can skip this step (based on https://www.digitalocean.com/community/tutorials/how-to-add-swap-space-on-ubuntu-16-04):

sudo fallocate -l 1G /swapfile 
sudo chmod 600 /swapfile 
sudo mkswap /swapfile 
sudo swapon /swapfile 
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab 
sudo sysctl vm.swappiness=10 
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf

Then let’s install nginx with page speed module. This command uses arguments from default nginx package for Ubuntu, so it looks a bit complicated:

wget https://ngxpagespeed.com/install -O ngx_pagespeed_install.sh
sudo bash ngx_pagespeed_install.sh --nginx-version latest -y -a "--with-cc-opt='-g -O2 -fPIE -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2' --with-ld-opt='-Wl,-Bsymbolic-functions -fPIE -pie -Wl,-z,relro -Wl,-z,now' --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx  --conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-pcre-jit --with-ipv6 --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-http_auth_request_module --with-http_addition_module --with-http_dav_module --with-http_geoip_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_image_filter_module --with-http_v2_module --with-http_sub_module --with-http_xslt_module --with-stream --with-stream_ssl_module --with-mail --with-mail_ssl_module --with-threads"
sudo mkdir /etc/nginx/sites-available/
sudo mkdir /etc/nginx/sites-enabled/
sudo mkdir /var/lib/nginx
sudo mkdir /etc/nginx/snippets

It will take some time and you can use it to get up, stretch and give your eyes some rest.

Create service entry for nginx by running:
sudo vim /lib/systemd/system/nginx.service
and putting following data there:

[Unit]
Description=The NGINX HTTP and reverse proxy server
After=syslog.target network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/bin/kill -s HUP
ExecStop=/bin/kill -s QUIT
PrivateTmp=true

[Install]
WantedBy=multi-user.target

sudo vim /etc/nginx/nginx.conf to edit main ngxinx conf to look like below:

user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
        worker_connections 768;
        # multi_accept on;
}

http {

        ##
        # Basic Settings
        ##

        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 65;
        types_hash_max_size 2048;
        server_tokens off;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        ##
        # Logging Settings
        ##

        access_log off;
        error_log /var/log/nginx/error.log;

        ##
        # Gzip Settings
        ##

        gzip on;
        gzip_disable "msie6";

        gzip_vary on;
        gzip_proxied any;
        gzip_comp_level 6;
        gzip_buffers 16 8k;
        gzip_http_version 1.1;
        gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

        ##
        # Virtual Host Configs
        ##

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
}

Now you can finally start nginx by running:

sudo systemctl enable nginx
sudo systemctl start nginx
sudo ufw allow http
sudo ufw allow https

We also need a small workaround for bug in ghost cli:
sudo apt-get install nginx-doc

Step 5 - Final steps

We'll install Ghost CLI which helps us a lot with nginx configuration:

npm i -g ghost-cli
sudo mkdir -p /var/www/ghost
sudo chown ghost:ghost /var/www/ghost
cd /var/www/ghost
ghost install --url https://blog.tjl.rocks/ --dbhost localhost --dbuser root --dbname ghost_production

If all goes well, we should be able to see our blog under our domain: https://blog.tjl.rocks/

We still have to tune some things.

First, we’ll move all http traffic to https, because it’s more secure and will benefit from http2. It will already mostly work, because we have HSTS header, but it's good practice.
sudo vim /etc/nginx/sites-available/blog.tjl.rocks.conf
and change it to look like this:

server {
    listen 80;
    listen [::]:80 ipv6only=on;
    server_name blog.tjl.rocks;
    return 301 https://$host$request_uri;
}

then
sudo vim /etc/nginx/sites-available/blog.tjl.rocks-ssl.conf
and add following code under section with ssl certificates:

pagespeed on;

# Needs to exist and be writable by nginx.  Use tmpfs for best performance.
pagespeed FileCachePath /dev/shm/ngx_pagespeed_cache;
pagespeed EnableFilters collapse_whitespace,remove_comments,defer_javascript,move_css_to_head,move_css_above_scripts;

# Ensure requests for pagespeed optimized resources go to the pagespeed handler
# and no extraneous headers get set.
location ~ "\.pagespeed\.([a-z]\.)?[a-z]{2}\.[^.]{10}\.[^.]+" {
  add_header "" "";
}
location ~ "^/pagespeed_static/" { }
location ~ "^/ngx_pagespeed_beacon$" { }

and finally:
sudo nginx -t to check if you haven't made any typos and if it's ok then:
sudo systemctl restart nginx

You should finally be able to access final form of your blog at proper URL with most important parts optimized automatically.