Ghost is an open source blogging platform that has been gaining popularity among developers and ordinary users since its 2013 release. It puts focus on content and blogging. The most attractive thing about Ghost is its simple, clean, and responsive design. You can write your blog posts from a mobile phone. Content for Ghost is written using the Markdown language. Ghost is perfect fit for individuals or small groups of writers.
In this guide we are going to set up and deploy a secure Ghost v0.11.x LTS blog on an CentOS 7.3 VPS using Let's Encrypt, Certbot, Node.js, NPM, NGINX and MySQL.
Register (purchase) a domain name.
CentOS 7.3 server instance with minimum of 1GB RAM.
Sudo user.
Check CentOS version:
cat /etc/centos-release
# CentOS Linux release 7.3.1611 (Core)
Create a new non-root user:
useradd -c "John Doe" johndoe && passwd johndoe
Make it superuser by adding it to wheel
group:
usermod -aG wheel johndoe
Switch to new user:
su - johndoe
Update your operating system's software:
sudo yum check-update || sudo yum update -y
Set up the timezone:
timedatectl list-timezones
sudo timedatectl set-timezone 'Region/City'
Install development tools:
sudo yum groupinstall -y 'Development Tools'
Install Vim text editor:
sudo yum install -y vim
Reboot system if required:
sudo shutdown -r now
NOTE: Before starting this step, ensure that you have set DNS records for your domain.
We are going to use Let's Encrypt CA and EFF's Certbot client to obtain SSL/TLS certificate for our Ghost blog. Don't forget to replace all instances of blog.domain.tld
with your domain name.
Enable the Extra Packages for Enterprise Linux (EPEL) repository:
# Certbot is packaged in Extra Packages for Enterprise Linux (EPEL) repository. To use Certbot, you must first enable the EPEL repository.
sudo yum install -y epel-release
Install Certbot (formerly Let's Encrypt client) certificate management software made with Python:
sudo yum install -y certbot
Check Certbot version:
certbot --version
# certbot 0.14.1
Obtain RSA certificate by using standalone authentication method (plugin):
sudo certbot certonly --standalone --domains blog.domain.tld --rsa-key-size 2048 --must-staple --email admin@domain.tld --no-eff-email --agree-tos
# IMPORTANT NOTES:
# - Congratulations! Your certificate and chain have been saved at
/etc/letsencrypt/live/blog.domain.tld/fullchain.pem.
# Your cert will expire on YYYY-MM-DD. . . .
# . . .
After going through previous steps, your certificate and private key will be in the /etc/letsencrypt/live/blog.domain.tld
directory.
NOTE: Ghost currently supports Node.js versions 4.5+ and 6.9+ only.
Ghost is built on Node.js. We are going to install the recommended version for Ghost which is v6 Boron LTS
at the time of this writing.
Download and install Node.js v6 LTS:
sudo curl --silent --location https://rpm.nodesource.com/setup_6.x | sudo bash -
sudo yum install -y nodejs
Check Node.js and NPM version:
node -v && npm -v
# v6.11.2
# 3.10.10
By default, Ghost comes configured to use an SQLite database, which requires no configuration.
Alternatively Ghost can also be used with a MySQL database by changing the database configuration. You must create a database and user first, you can then change the existing sqlite3 config.
Download and install the latest version of MySQL (currently 5.7) from the official MySQL Yum repository:
cd /tmp
# Adding the MySQL Yum Repository
wget https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm
sudo yum localinstall -y mysql57-community-release-el7-11.noarch.rpm
# Installing MySQL
sudo yum install -y mysql-community-server
Check MySQL version:
mysql --version
# mysql Ver 14.14 Distrib 5.7.19, for Linux (x86_64) using EditLine wrapper
Start MySQL Server and check its status:
sudo systemctl start mysqld.service
sudo systemctl status mysqld.service
MySQL version 5.7 or higher generates a temporary random password for MySQL root
user after installation and password is stored in the MySQL error log file, located at /var/log/mysqld.log
. To reveal it, use the following command:
sudo grep 'temporary password' /var/log/mysqld.log
Run the mysql_secure_installation
script to secure your database a bit:
NOTE: Password Validation Plugin is installed and enabled, so your new password for
root
user needs to be strong (one upper case letter, one lower case letter, one digit, and one special character, and that the total password length is at least 8 characters). If you want to relax that or disable plugin completely (not recommended) consult the official MySQL documentation for how to do that.
sudo mysql_secure_installation
Log into MySQL as the root user:
mysql -u root -p
# Enter password:
Create a new MySQL database and user:
create database dbname;
grant all on dbname.* to 'user' identified by 'password';
Exit MySQL:
exit
Download and install the latest mainline version of NGINX from the official NGINX repository:
# Add the NGINX Yum Repository
sudo vim /etc/yum.repos.d/nginx_mainline.repo
# Copy/paste the following into /etc/yum.repos.d/nginx_mainline.repo
[nginx]
name=nginx repo
baseurl=https://nginx.org/packages/mainline/centos/7/$basearch/
gpgcheck=1
enabled=1
wget https://nginx.org/keys/nginx_signing.key
sudo rpm --import nginx_signing.key
rm nginx_signing.key
sudo yum install -y nginx nginx-module-geoip nginx-module-image-filter nginx-module-njs nginx-module-perl nginx-module-xslt nginx-nr-agent
Verify that it is installed by checking the NGINX version:
sudo nginx -v
# nginx version: nginx/1.13.3
Check status, enable and start NGINX service (daemon):
sudo systemctl status nginx.service # inactive (dead)
sudo systemctl enable nginx.service
sudo systemctl start nginx.service
Create /etc/nginx/ssl
directory and generate a new Diffie-Hellman (DH) parameters:
sudo mkdir -p /etc/nginx/ssl
sudo openssl dhparam -out /etc/nginx/ssl/dhparams-2048.pem 2048
Create log directory for blog.domain.tld
virtual host:
sudo mkdir -p /var/log/nginx/blog.domain.tld
Configure NGINX as a HTTP(S) reverse proxy server:
sudo vim /etc/nginx/conf.d/ghost.conf
Paste the following in /etc/nginx/conf.d/ghost.conf
:
# domain: blog.domain.tld
# public: /var/www/ghost
upstream ghost_app {
server 127.0.0.1:2368;
keepalive 32;
}
server {
listen [::]:80 default_server;
listen 80 default_server;
listen [::]:443 ssl http2 default_server;
listen 443 ssl http2 default_server;
server_name blog.domain.tld;
root /var/www/ghost;
error_log /var/log/nginx/blog.domain.tld/error.log;
access_log /var/log/nginx/blog.domain.tld/access.log;
client_max_body_size 100M;
ssl_certificate /etc/letsencrypt/live/blog.domain.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.domain.tld/privkey.pem;
ssl_dhparam ssl/dhparams-2048.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
ssl_prefer_server_ciphers on;
ssl_buffer_size 4K;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50M;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/blog.domain.tld/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
location / {
proxy_pass http://ghost_app;
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_hide_header X-Powered-By;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
Save and test NGINX configuration for syntax errors:
sudo nginx -t
Reload NGINX configuration:
sudo systemctl reload nginx.service
NOTE: If you want to host multiple Ghost blogs on same VPS, each Ghost instance must be running on a separate port.
Create document root directory:
sudo mkdir -p /var/www/
Create a new ghost user:
sudo useradd -c 'Ghost application' ghost
Download Ghost:
curl -L https://github.com/TryGhost/Ghost/releases/download/0.11.11/Ghost-0.11.11.zip -o ghost.zip
Unzip Ghost into the /var/www/ghost
directory (recommended install location):
sudo unzip -uo ghost.zip -d /var/www/ghost
rm ghost.zip
Move to the new ghost directory:
cd /var/www/ghost
Change the ownership of the /var/www/ghost
directory:
sudo chown -R ghost:ghost .
Switch to new ghost
user:
sudo su - ghost
Navigate to document root /var/www/ghost
:
cd /var/www/ghost
Install Ghost with production dependencies only. When this completes, Ghost is installed:
npm install --production
Configure Ghost by changing url
, mail
and database
property of production object inside of config.js
file:
cp config.example.js config.js
vim /var/www/ghost/config.js
var path = require('path'),
config;
config = {
// ### Production
// When running Ghost in the wild, use the production environment.
// Configure your URL and mail settings here
production: {
url: 'https://blog.domain.tld',
mail: {
transport: 'SMTP',
options: {
service: 'Mailgun',
auth: {
user: '',
pass: ''
}
}
},
database: {
client: 'mysql',
connection: {
host: '127.0.0.1',
user: 'your_database_user',
password: 'your_database_password',
database: 'your_database_name',
charset: 'utf8'
},
debug: false
},
// . . .
// . . .
NOTE: You should configure
Start Ghost in production environment:
npm start --production
Ghost will now be running. Both blog front-end and admin interface are secured with HTTPS and HTTP/2 is working also. You can open your browser and visit site at https://blog.domain.tld
. Don't forget to replace blog.domain.tld
with your domain name.
Shut down Ghost process by pressing CTRL
+ C
and exit from ghost user back to non-root user that you have created at the beginning:
exit
If you close your terminal session with your VPS, your blog will also go down. That's not good. To avoid this, we are going to use systemd. It will keep our blog up 24/7.
Create ghost.service
systemd unit file. Run sudo sudo vim /etc/systemd/system/ghost.service
and copy/paste the below content:
[Unit]
Description=Ghost - the professional publishing platform
Documentation=https://docs.ghost.org/v0.11.11/docs
After=network.target
[Service]
Type=simple
# Edit WorkingDirectory, User and Group as needed
WorkingDirectory=/var/www/ghost
User=ghost
Group=ghost
ExecStart=/bin/npm start --production
ExecStop=/bin/npm stop --production
Restart=always
SyslogIdentifier=Ghost
[Install]
WantedBy=multi-user.target
Enable and start ghost.service
:
sudo systemctl enable ghost.service && sudo systemctl start ghost.service
Check ghost.service
status:
sudo systemctl status ghost.service && sudo systemctl is-enabled ghost.service
Navigate to https://blog.domain.tld/ghost/
and create a Ghost admin user. Do this as soon as possible!
That's it. We now have a fully functional Ghost blog. Your server is delivering content via HTTP/2 when supported by the client. If you want to change the default Ghost theme called Casper to a custom one, you can just download and unzip the theme into the /var/www/ghost/content/themes
folder and select it via Ghost admin interface, located at https://blog.domain.tld/ghost
.