Running Ghost fast and secure with Debian Wheezy, Nginx and MariaDB

Introduction

I know, there are many tutorials in the wild covering different aspects of setting up Ghost, Node.js, Nginx or MariaDB. Maybe also all together, but i found out that they all fail at some point missing one aspect or another you might run into.
This tutorial tries to address a complete installation which is intended to be fast and secure!

I'll not cover securing Wheezy itself in this guide.

I assume that you run all commands as root, which is a bit faster for first-time installation. You can also add sudo in front of almost all commands to do the same!

1 Preparing Wheezy

At first we need to install some basic stuff like Node.js, Nginx and MariaDB before we can configure them.
For easier updatability, speed, latest features and bugfixes we will install Node.js from scratch and get nginx directly from the maintainers.

1.1 Install MariaDB

We will use the latest stable of MariaDB which you can download here. For your convenience, here are the commands you need to execute to add the repository:

apt-get install python-software-properties
apt-key adv --recv-keys --keyserver keyserver.ubuntu.com 0xcbcb082a1bb943db
add-apt-repository 'deb http://mirror.netcologne.de/mariadb/repo/10.0/debian wheezy main'

Now update apt and install it.

apt-get update
apt-get install mariadb-server

If you still have MySQL installed, MariaDB will replace it. No worries. MariaDB is binary-compatible so you can continue using all your applications and configuration. The replacement does not require any user interaction dispite creating a new MySQL/MariaDB root password.

1.2 Install Nginx

Now let us install Nginx from the maintainers repository. This way you'll always have the latest and greatest without the hassle of compiling it yourself.

cd /tmp && wget http://nginx.org/keys/nginx_signing.key
apt-key add nginx_signing.key
echo "deb http://nginx.org/packages/mainline/debian/ wheezy nginx" >> /etc/apt/sources.list.d/nginx.list
echo "deb-src http://nginx.org/packages/mainline/debian/ wheezy nginx" >> /etc/apt/sources.list.d/nginx.list
apt-get update && apt-get install nginx

1.3 Install Node.js

We will install Node.js from scratch, so we can easily update it later. For your convenience:

apt-get install python g++ make checkinstall fakeroot
src=$(mktemp -d) && cd $src
wget -N http://nodejs.org/dist/node-latest.tar.gz
tar xzvf node-latest.tar.gz && cd node-v*
./configure
fakeroot checkinstall -y --install=no --pkgversion $(echo $(pwd) | sed -n -re's/.+node-v(.+)$/\1/p') make -j$(($(nproc)+1)) install
dpkg -i node_*

Check your installation by issuing node -v and npm -v and see if it prints out the versions. As of this writing it should be 0.10.29 for node and 1.4.14 for npm.

1.4 Install Ghost and create a user

At first go to our www folder and download the latest Ghost package.

cd /var/www && wget https://ghost.org/zip/ghost-latest.zip
unzip -uo ghost.zip -d ghost

Now we need to create a new user under which Node.js can run Ghost.

adduser ghost # Just answer all those questions and use an arbitrary passwd
chown -R ghost:ghost /var/www/ghost

Finish installation and shortly test if Ghost is running with

cd /var/www/ghost
npm install --production
npm start

Ghost will listen on 127.0.0.1:2368. If so, let us continue.

I strongly recommend creating a symlink to your ghost installation using the desired domain name. This way you can update your ghost installation much easier and with almost zero downtime!

Let us create one... ln -s /var/www/ghost /var/www/mydomain.com

Nginx will point to that symlink so you just need to move the symlink to the new installation later without restarting nginx or changing its config!

2 Configuration

configure all the things

2.1 Configuring MariaDB

The only thing we need to do is creating a database for Ghost as well as a login for it.
Connect to your shiny new MariaDB like you always did: mysql -u root -p
Now let us create a user and the database and grant him all rights:

CREATE USER ghostuser IDENTIFIED BY 'password';
CREATE DATABASE ghostblog;
GRANT ALL PRIVILEGES ON ghostblog.* to ghostuser@localhost;

The @localhost ensures that the ghostuser is only allowed to connect from localhost which secures the database connection a bit more.

2.2 Configuring Nginx

We will create a new unique Nginx configuration beneath its conf.d folder. This way the configuration stays clean if you use more than one webservice.

Most of the configuration is documented with inline comments. Please read the Nginx documentation if some config options are unknown or leave a comment.

Create the file vim /etc/nginx/conf.d/mydomain.com

server {
        listen       443 ssl spdy; # Enable SSL and SPDY on 443
		server_name  www.mydomain.com;

		charset utf-8;
		access_log  /var/log/nginx/mydomain.com.access.log  main;
		error_log  /var/log/nginx/mydomain.com.error.log;

		# The certificate bundle contains the server and all intermediate certs
		ssl_certificate      /etc/ssl/certs/mydomain.com-bundle.crt;
		ssl_certificate_key  /etc/ssl/private/mydomain.com.key;

		ssl_ecdh_curve secp384r1; # Select a more secure elliptic curve
		# Please read the nginx config before enabling the next option!!
		# ssl_dhparam /etc/ssl/dhparam.pem;
		ssl_session_cache shared:SSL:10m;
		ssl_session_timeout  10m;

		ssl_protocols  TLSv1 TLSv1.1 TLSv1.2;
		# For best ciphers see bettercrypto.org
		ssl_ciphers 'EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH:+CAMELLIA256:+AES256:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!ECDSA:CAMELLIA256-SHA:AES256-SHA:CAMELLIA128-SHA:AES128-SHA';
		ssl_prefer_server_ciphers   on;

		# Enable OCSP Stapling.
        # We need a fast resolver and the trusted Root CA of our SSL Cert!
		resolver 8.8.8.8 8.8.4.4;
		ssl_stapling on;
		ssl_trusted_certificate /etc/ssl/certs/mydomain.com-ca.crt;

		# Enable HSTS for a year
		add_header Strict-Transport-Security max-age=31536000;

		# Disallow Ghost Signup Page for security
        # Uncomment after your signed up for the first time!
		# location ~ ^/(ghost/signup/) {
		#	rewrite ^/(.*)$ https://www.mydomain.com/ permanent;
		# }

		# Next Locations will serve assets and scripts via nginx directly
		location ~ ^/(img/|css/|lib/|vendor/|fonts/|robots.txt|humans.txt) {
			root /var/www/mydomain.com/client/assets;
			access_log off;
			expires max;
		}

		location ~ ^/(shared/|built/) {
			root /var/www/mydomain.com/core;
			access_log off;
			expires max;
		}

		location ~ ^/(content/images/) {
			root /var/www/mydomain.com;
			access_log off;
			expires max;
		}

		# Node.js Proxy Part
        # Check Node.JS documentation for in-depth information
		location / {
    		proxy_set_header X-Real-IP $remote_addr;
    		proxy_set_header HOST $http_host;
    		proxy_set_header X-NginX-Proxy true;
			proxy_set_header X-Forwarded-Proto $scheme; 
			proxy_http_version 1.1; # Spares Overhead for better latency
            # Removes the X-Powered-By: Express Header
			proxy_hide_header X-Powered-By;

    		proxy_pass http://127.0.0.1:2368; # Node.JS will listen here
    		proxy_redirect off;
		}
	}

# Next two server parts will ensure that all connections will go to
# https://www.mydomain.com so the user always uses an encrypted connection only.
server {
		listen 443 ssl spdy;
		server_name mydomain.com;

		ssl_certificate      /etc/ssl/certs/mydomain.com-bundle.crt;
		ssl_certificate_key  /etc/ssl/private/mydomain.com.key;

		ssl_ecdh_curve secp384r1;
		# ssl_dhparam /etc/ssl/dhparam.pem;
		ssl_session_cache shared:SSL:10m;
		ssl_session_timeout  10m;

		ssl_protocols  TLSv1 TLSv1.1 TLSv1.2;
		ssl_ciphers 'EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH:+CAMELLIA256:+AES256:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!ECDSA:CAMELLIA256-SHA:AES256-SHA:CAMELLIA128-SHA:AES128-SHA';
		ssl_prefer_server_ciphers   on;

		resolver 8.8.8.8 8.8.4.4;
		ssl_stapling on;
		ssl_trusted_certificate /etc/ssl/certs/mydomain.com-ca.crt;

		return 301 https://www.mydomain.com$request_uri;
}

# Redirect all HTTP traffic to HTTPS (later HSTS will take over)
server {
		listen 80;
		server_name .mydomain.com;

		return 301 https://www.mydomain.com$request_uri;
}

In a later blog post i'll explain more about SSL Certificates and all that "cryptic" things.

2.3 Configuring Ghost

Now that we have configured everything there's just a bit of Ghost configuration left before you can start blogging.

I assume, you will run Ghost in a production environment. Therefore there is only the production part for Ghost's config.js below. Most of the explanation are inline comments again:

// # Ghost Configuration
// Setup your Ghost install for various environment
// Documentation can be found at http://docs.ghost.org/usage/configuration/

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://www.mydomain.com',
        // For mail configuration using Gmail or other "services"
        // see: http://docs.ghost.org/mail/
        // We will use our own standard SMTP Server here
        mail: {
		transport: 'SMTP',
		fromaddress: '[email protected]',
		options: {
			host: 'smtp.mydomain.com',
			port: 465,
            // If you want to use STARTTLS falsify the next option!
			secureConnection: true,
			auth: {
				user: '[email protected]',
				pass: 'ThisMailAccountsPassword',
			}
		}
	},
	database: {
		client: 'mysql',
		connection: {
			host:		'127.0.0.1',
			user:		'ghostuser',
			password:	'password',
			database:	'ghostblog',
			charset:	'utf8'
		}
	},
    server: {
        // Host to be passed to node's `net.Server#listen()`
        host: '127.0.0.1',
        // Port to be passed to node's `net.Server#listen()`, for iisnode set this to `process.env.PORT`
        port: '2368'
    }
},

We can see, that the Ghost configuration is straightforward. The last step is to create a init.d script whichs start/stops our Node.js server for Ghost.

Create the file vim /etc/init.d/ghost with the following script.

#! /bin/sh
### BEGIN INIT INFO
# Provides:          ghost
# Required-Start:    $network $syslog
# Required-Stop:     $network $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Ghost Blogging Platform
# Description:       Ghost: Just a blogging platform
### END INIT INFO

# Do NOT "set -e"

# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="Ghost"
NAME=ghost
GHOST_ROOT=/var/www/mydomain.com
GHOST_GROUP=ghost
GHOST_USER=ghost
DAEMON=/usr/local/bin/node
DAEMON_ARGS="$GHOST_ROOT/index.js"
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME
export NODE_ENV=production

# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 0

# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME

# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh
# I like to know what is going on
VERBOSE=yes

# Define LSB log_* functions.
# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
# and status_of_proc is working.
. /lib/lsb/init-functions

#
# Function that starts the daemon/service
#
do_start()
{
	# Return
	#   0 if daemon has been started
	#   1 if daemon was already running
	#   2 if daemon could not be started
	start-stop-daemon --start --quiet \
    --chuid $GHOST_USER:$GHOST_GROUP --chdir $GHOST_ROOT --background \
    --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
    || return 1
	start-stop-daemon --start --quiet \
    --chuid $GHOST_USER:$GHOST_GROUP --chdir $GHOST_ROOT --background \
    --make-pidfile --pidfile $PIDFILE --exec $DAEMON -- $DAEMON_ARGS \
    || return 2
	# Add code here, if necessary, that waits for the process to be ready
	# to handle requests from services started subsequently which depend
	# on this one.  As a last resort, sleep for some time.
}

#
# Function that stops the daemon/service
#
do_stop()
{
	# Return
	#   0 if daemon has been stopped
	#   1 if daemon was already stopped
	#   2 if daemon could not be stopped
	#   other if a failure occurred
	start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 \
    --pidfile $PIDFILE --exec $DAEMON
	RETVAL="$?"
	[ "$RETVAL" = 2 ] && return 2
	# Wait for children to finish too if this is a daemon that forks
	# and if the daemon is only ever run from this initscript.
	# If the above conditions are not satisfied then add some other code
	# that waits for the process to drop all resources that could be
	# needed by services started subsequently.  A last resort is to
	# sleep for some time.
	start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 \
    --exec $DAEMON
	[ "$?" = 2 ] && return 2
	# Many daemons don't delete their pidfiles when they exit.
	rm -f $PIDFILE
	return "$RETVAL"
}

#
# Function that sends a SIGHUP to the daemon/service
#
do_reload() {
	#
	# If the daemon can reload its configuration without
	# restarting (for example, when it is sent a SIGHUP),
	# then implement that here.
	#
	start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE \
    --exec $DAEMON
	return 0
}

case "$1" in
start)
    [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
    do_start
    case "$?" in
            0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
            2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
    esac
    ;;
stop)
    [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
    do_stop
    case "$?" in
            0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
            2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
    esac
    ;;
status)
	status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
	;;
#reload|force-reload)
    #
    # If do_reload() is not implemented then leave this commented out
    # and leave 'force-reload' as an alias for 'restart'.
    #
    #log_daemon_msg "Reloading $DESC" "$NAME"
    #do_reload
    #log_end_msg $?
    #;;
restart|force-reload)
    #
    # If the "reload" option is implemented then remove the
    # 'force-reload' alias
    #
    log_daemon_msg "Restarting $DESC" "$NAME"
    do_stop
    case "$?" in
    0|1)
            do_start
            case "$?" in
                    0) log_end_msg 0 ;;
                    1) log_end_msg 1 ;; # Old process is still running
                    *) log_end_msg 1 ;; # Failed to start
            esac
            ;;
    *)
            # Failed to stop
            log_end_msg 1
            ;;
    esac
    ;;
*)
    #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
    echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
    exit 3
    ;;
esac

:

Now we need to make this script executable and add it to all linux runlevels.

chmod +x /etc/init.d/ghost
update-rc.d ghost defaults

3 Run it!

No we're done with everything needed to run your shiny new blog. Issue the following two commands to get ready.

service ghost start
service nginx restart