Setup Ghost, Ubuntu, Nginx, Custom Domain, and SSL Let’s Encrypt

Hello guys,

In this article we will learn how to setup Ghost blogging platform on Ubuntu 16.04 server, with Nginx, as well as set up custom domain and HTTPS; from scratch.

Btw, I choose Ubuntu because in my opinion this distro is the easiest one to operate.

The real world result of this very long tutorial is this blog itself

Here are list of all stuff required to make this tutorial works. You don't have to prepare all of it at first, it's just to let you know, no need to worry 😊


  • Linux Server, Ubuntu 16.04; Digital Ocean and Vultr have good offer, you can get a VPS with only $5
  • Node JS & NPM
  • Ghost archive
  • Nginx
  • A Domain, this one is required to make the SSL works
  • Let's Encrypt certificate tools: certbot

Table of Contents

  1. Prepare Server
  2. Install Node JS
  3. Configure SWAP
  4. Install & Configure Ghost
  5. Install & Configure Nginx
  6. Configure Custom Domain
  7. Configure HTTPS
  8. Auto Renewal Certificate

1. Prepare Server

In this example I use service from Digital Ocean, the one which cost is USD $5 per month. Btw you can use any other VPS service if you want 😀

If you are student, try to join Github Student Developer Pack. There are lot of benefits you can get by joining the promo, one of them is free DO credit USD $50

OK, let's start! First, create new virtual server, in Digital Ocean it's called dropplet.

Instalasi Ghost, Ubuntu Digital Ocean, Nginx, Custom Domain, dan SSL Let’s Encrypt

It takes few minutes to complete. Later you'll notified by email contains default password of root account, after the setup is done. Use it to access your server via SSH.

# ==== command to make ssh connection
$ ssh root@

If you connect to the server for the first time, prompt request to change your password will appear.

Instalasi Ghost, Ubuntu Digital Ocean, Nginx, Custom Domain, dan SSL Let’s Encrypt

2. Install Node JS & NPM

Node.js is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. NPM is package manager for Node applications

Ghost is a blog engine, crafted using Node JS technology. To be able using Ghost, you can either get one by paying them monthly, or by downloading the installer then implements it on your own server (like what we did in this article).

First of all, because Ghost is Node JS-based, we must install Node first. The recommended Node version for ghost is node v6.9+.

There are may ways available to install Node, in my opinion the easiest one is by using NVM (Node Version Manager).

# ==== update ubuntu package list
$ sudo apt-get update

# ==== delete installed node, to avoid conflict with node which are going to be installed
$ sudo apt-get remove --purge node

# ==== install build-essential and libssl-dev
$ sudo apt-get install build-essential libssl-dev

# ==== install curl, zip, and wget
$ sudo apt-get install curl zip wget

# ==== install nvm
$ curl -o- | bash

# ==== after nvm installed, we need to restart the console (by doing logout from ssh, then login again). Or, you can just use these command
$ source $HOME/.nvm/

# ==== make sure nvm is installed
$ command -v nvm

# ==== install nvm version 6
$ nvm install 6

# ==== set installed node as default node
$ nvm alias default node

# ==== try to run some command using `node` keyword, like checking the version, or anything
$ node -v

3. Configure SWAP

This step is mandatory if your VPS or server only have 512MB of memory

Why we need SWAP? It's because 512MB is not enough, even for installing ghost dependencies from internet.

The npm install process will be immediately stopped in the middle of process, because of out-of-memory error.

This issue will be easily solved by just upgrading the RAM, but it'll cost more money. Other cheaper solution would be by allocate some storage for SWAP.

Swap space in Linux is used when the amount of physical memory (RAM) is full. If the system needs more memory resources and the RAM is full, inactive pages in memory are moved to the swap space. While swap space can help machines with a small amount of RAM, it should not be considered a replacement for more RAM

After SWAP is created, it'll still take time to complete package installation of Ghost. But at least the process will not be stopped.

Okay, now prepare the SWAP.

# ==== first, check wether there is SWAP or not. But normally at first there will be no SWAP
$ sudo swapon -s

# ==== create new SWAP, allocate 1GB
$ sudo dd if=/dev/zero of=/swapfile bs=1024 count=1024k
$ sudo mkswap /swapfile

# ==== activate the SWAP
$ sudo swapon /swapfile

# ==== check wether SWAP is active or not
$ sudo swapon -s

FYI, SWAP allocation will be reseted (removed) after reboot. It's pain in the ***, if we have to configure SWAP every time we restart the server.

SWAP allocation can be configured to be permanent, by modifying the fstab and stuff.

# ==== edit fstab file
$ sudo nano /etc/fstab

# >>>> append this text, then save
/swapfile       none    swap    sw      0       0 

Next, change swappiness value to 10, it'll increase the performance, also useful to prevent crash because of out-of-memory error.

$ echo 10 | sudo tee /proc/sys/vm/swappiness
$ echo vm.swappiness = 10 | sudo tee -a /etc/sysctl.conf

Change the permission of /swapfile, so other non-root user prohibited to modify the SWAP folder.

$ sudo chown root:root /swapfile 
$ sudo chmod 0600 /swapfile

Normally at first only one user will be available, root. Step above is only useful if there are another user other than root.

4. Install & Configure Ghost

Download latest stable archive of Ghost, then extract to /var/www/ghost.

# ==== create the folder first
$ sudo mkdir -p /var/www/ghost

# ==== download ghost
$ sudo wget

# ==== extract Ghost to `/var/www/ghost`
$ sudo unzip -d /var/www/ghost

# ==== Go to extracted folder, then perform package installation
$ cd /var/www/ghost
$ sudo npm install --production

The required dependencies that need to be installed is production stage dependencies, that's why --production added to npm install argument.

This process will take some time.

Next we need to make Ghost configuration file. Ghost provide default configuration that we can copy, so when we modify and something went wrong, it can easily reverted.

# ==== make a duplicate from default config file, then edit it
$ sudo cp config.example.js config.js
$ sudo nano config.js

Change value of config.production.url with public IP of VPS.
Also replace value of to


config = {  

    production: {
        url: '',

        server: {
            host: '',
            port: '2368'


Last but not least, run the application in the background.

# ==== run the application in the background using nohup
$ nohup sudo npm start --production &

We do connect the the server through ssh in command line. When we run something to the server, like sudo npm start --production &, the process will be killed if the ssh session ended (example: calling exit on the bash, or closing the command line prompt). Even the application is being run in the background, it'll still be killed.

By using nohup, the process will be prevented to get killed by system even though session is ended.

5. Install & Configure Nginx

Nginx (pronounced "engine-x") is a free, open-source, high-performance HTTP server and reverse proxy.

We will use Nginx to set the access of the application, point to a domain, and setup the SSL.

Installing Nginx on Ubuntu is pretty simple.

# ==== install nginx nginx
$ sudo apt-get install nginx

Now Nginx is installed, next create a Nginx configuration for Ghost application. By default Ghost will run on port :2368, we'll make a proxy so all incoming request to port :80 will be redirected to port :2368

# ==== do backup the default configuration
$ sudo mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak
$ sudo mv /etc/nginx/sites-enabled/default /etc/nginx/sites-enabled/default.bak

# ==== create new configuration file, called ghost
$ sudo nano /etc/nginx/sites-available/ghost

# >>>> fill it with this
server {  
    listen 80 default_server;
    listen [::]:80 default_server ipv6only=on;

    location / {
        proxy_pass http://localhost:2368;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_buffering off;

# ==== then create a symlink to /sites-enabled/ghost
$ sudo ln -s /etc/nginx/sites-available/ghost /etc/nginx/sites-enabled/ghost

# ==== last, restart the Nginx
$ sudo systemctl restart nginx

If you encounter this error during Nginx restart process, it's not a problem. It's Ubuntu related bug.

May 14 19:35:03 ngx systemd[1]: nginx.service: Failed to read PID from file /run/ Invalid argument  

Run this command to make error above gone.

mkdir /etc/systemd/system/nginx.service.d  
printf "[Service]\nExecStartPost=/bin/sleep 0.1\n" > /etc/systemd/system/nginx.service.d/override.conf  
sudo systemctl daemon-reload  
sudo systemctl restart nginx  

Now the application should be accessible on

The first thing we need to do after ghost is ready is complete the Ghost platform setup. Open, it'll take you to ghost installation process.

6. Configure Custom Domain

Now we'll make the application to be accessible through domain.

Prepare the domain, in this example we choose domain

Open the domain control panel, go to the DNS Management, add new A Record, put @ on the domain and dropplet IP as the destination.

FYI, the domain control panel looks could be different one another, depend on what domain provider you are currently using.

Instalasi Ghost, Ubuntu Digital Ocean, Nginx, Custom Domain, dan SSL Let’s Encrypt

No need to change the nameserver

Next, there is part from file config.js that need to be changed.

# ==== edit configuration file
$ sudo nano config.js

Change the value of url, previously it was pointed to dropplet IP, now change it to domain.


config = {  

    production: {
        url: '',


On the Nginx configuration file of our application, add new keyword: server_name with value is domain name.

# ==== edit configuration file
$ nano /etc/nginx/sites-available/ghost

# >>>> add server_name
server {  
    listen 80 default_server;
    listen [::]:80 default_server ipv6only=on;


    location / {
        proxy_pass http://localhost:2368;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_buffering off;

# ==== do restart the Nginx
$ sudo systemctl restart nginx

Application should be accessible through domain now

Instalasi Ghost, Ubuntu Digital Ocean, Nginx, Custom Domain, dan SSL Let’s Encrypt

If you can't see the application yet, it could be because the DNS is still being propagated. Usually it takes at max 1x24 hours to be completed.

7. Configure HTTPS

Actually you can skip this part if you don't want to implement SSL

Hyper Text Transfer Protocol Secure (HTTPS) is the secure version of HTTP, the protocol over which data is sent between your browser and the website that you are connected to. The 'S' at the end of HTTPS stands for 'Secure'. It means all communications between your browser and the website are encrypted.

Let's Encrypt is a new Certificate Authority (CA) that provides an easy way to obtain and install free TLS/SSL certificates, thereby enabling encrypted HTTPS on web servers.

We'll use Let's Encrypt to enable HTTPS on our application. There are few steps that need to be done to make it happen.

Let's Encrypt has tools called certbot. This small tools will help us to simplify the process to generate SSL certificate. So let's install the tools.

# ==== add certbot repository
$ sudo add-apt-repository ppa:certbot/certbot

# ==== update the repository
$ sudo apt-get update

# ==== install certbot
$ sudo apt-get install certbot

There are variety of ways to obtain SSL certificates, one of them is by using Webroot plugin. This plugin will generate special file on the directory /.well-known within document root of the application (it should be accessible as well from web server), later it'll be verified by Let's Encrypt.

OK, let's do it. First change the Nginx configuration of our app, we have to allow public access to /.well-known.

# ==== edit configuration file
$ sudo nano /etc/nginx/sites-available/ghost

# >>>> change the content like below
server {  
    listen 80 default_server;
    listen [::]:80 default_server ipv6only=on;


    location ~ /.well-known {
        root /var/www/ghost;

    location / {
        proxy_pass http://localhost:2368;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_buffering off;

# ==== do restart Nginx
$ sudo systemctl restart nginx

Next, run this command to obtain the certificate.

$ sudo certbot certonly --webroot --webroot-path=/var/www/ghost -d -d

A prompt asking your information will showing up. Fill it, and agree to all of the terms. At the very end, text underneath will showing up.

 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/ Your cert
   will expire on 2017-07-26. 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
 - If you lose your account credentials, you can recover through
   e-mails sent to
 - Your account credentials have been saved in your Certbot
   configuration directory at /etc/letsencrypt. You should make a
   secure backup of this folder now. This configuration directory will
   also contain certificates and private keys obtained by Certbot so
   making regular backups of this folder is ideal.
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:
   Donating to EFF:          

Firewall Note: If you receive an error like Failed to connect to host for DVSNI challenge, your server's firewall may need to be configured to allow TCP traffic on port 80 and 443.

Note: If your domain is routing through a DNS service like CloudFlare, you will need to temporarily disable it until you have obtained the certificate.

You have obtained the certificate, 4 new files should be created inside this folder :

$ sudo ls -l /etc/letsencrypt/live/

To increase security, we need to generate Diffie-Hellman Key Exchange.

# ==== command to generate Diffie-Hellman key
$ sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

Now create a configuration snippet file for Nginx.

# ==== create config snippet file
sudo nano /etc/nginx/snippets/

# fill with this content
ssl_certificate /etc/letsencrypt/live/;  
ssl_certificate_key /etc/letsencrypt/live/;  

Next we need to write some SSL configuration on file /etc/nginx/snippets/ssl-params.conf, as well as attaching the generated Diffie-Hellman file there.

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;  
ssl_prefer_server_ciphers on;  
ssl_ecdh_curve secp384r1;  
ssl_session_cache shared:SSL:10m;  
ssl_session_tickets off;  
ssl_stapling on;  
ssl_stapling_verify on;  
resolver valid=300s;  
resolver_timeout 5s;  
add_header X-Frame-Options DENY;  
add_header X-Content-Type-Options nosniff;

ssl_dhparam /etc/ssl/certs/dhparam.pem;  

Edit the nginx configuration file of our blog, to active the https. We'll also make some rule to redirect incoming access from http to https.

# ==== edit blog nginx conf
sudo nano /etc/nginx/sites-available/ghost

# >>>> fill with this content
server {  
    listen 80 default_server;
    listen [::]:80 default_server;

    return 301$request_uri;

server {  
    listen 443 ssl http2 default_server;
    listen [::]:443 ssl http2 default_server;
    include snippets/;
    include snippets/ssl-carirejekihalal.conf;

    server_name _;

    root /usr/share/nginx/html;
    index index.html index.htm;

    client_max_body_size 10G;

    location ~ /.well-known {
        root /var/www/ghost;

    location / {
        proxy_pass http://localhost:2368;

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_buffering off;

Last but not least, change the firewall settings, make sure only https and ssh request are allowed.

# ==== activate ufw firewall
sudo ufw enable

# ==== set some rules
sudo ufw allow ssh  
sudo ufw allow 'Nginx Full'  
sudo ufw delete allow 'Nginx HTTP'

# ==== reload uwf
sudo ufw reload  

Do check the firewall state.

# ==== check rules
sudo ufw status

# <<<< the output should be like this

Status: active

To                         Action      From  
--                         ------      ----
OpenSSH                    ALLOW       Anywhere  
Nginx Full                 ALLOW       Anywhere  
OpenSSH (v6)               ALLOW       Anywhere (v6)  
Nginx Full (v6)            ALLOW       Anywhere (v6)  

Lastly, restart nginx.

sudo nginx -t  
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok  
nginx: configuration file /etc/nginx/nginx.conf test is successful

# ==== restart nginx
sudo systemctl restart nginx  

Finally, domain is accessable 🤗

8. Auto Renewal Certificate

SSL certificates from Let's Encrypt are only valid for 90 days. We have to renew the certificate before it's expiration date. Statement below is used to renew certificates.

/usr/bin/certbot renew --quiet

To make our job easier, we better put that into cron.

# ==== open cron
$ sudo crontab -e

# >>>> at the very end, put this
15 1 * * * /usr/bin/certbot renew --quiet  

Certificate will be renewed at 1 AM in the morning everyday.