My first post on this blog is kind of meta. My web server runs on FreeBSD, and although my existing website was running WordPress, I really prefer working with Markdown and decided to create my professional website with Ghost.

Nothing about what I'm going to show you is officially supported by the project, so don't go crying to them or me if this breaks during an upgrade.

This is from my original dry run on a test vm, so I'm assuming you have a fresh install of FreeBSD 12.1.

Let's go ahead and install Ghost's dependencies:

# pkg install node12 npm-node12 nginx py37-certbot py37-certbot-nginx

That will provide the basics, but we need a database backend. The SQLite database is really only for testing purposes. Ghost officially supports MySQL >=5.5 & <=5.8. You should probably install one of these from FreeBSD ports. For the sake of this tutorial, we're throwing caution to the wind and using MariaDB 10.4. Why? No better reason than it's what my server was already running.

# pkg install mariadb104-server mariadb104-client
# service mysql-server enable
# service mysql-server start

Now let's prepare a database and user for Ghost.

# mysql
CREATE USER 'ghost'@'localhost' IDENTIFIED BY 'IsThisEnoughBitsOfEntropy?_^';
CREATE DATABASE yourblog_ext_prod;
GRANT ALL privileges ON `yourblog_ext_prod`.* TO 'ghost'@'localhost';

Install ghost-cli. We'll handle the user account and web directories next.

# npm install ghost-cli -g

Run adduser and follow the prompts to make an unprivileged user for Ghost to run as. The official docs say to give it sudoer permissions, but I'm saying not to. From a default install, this just means don't add the user to the group wheel.

# mkdir /usr/local/www/yourblog.ext
# chown yourusername:yourusername /usr/local/www/yourblog.ext
# chmod 775 /usr/local/www/yourblog.ext
# su - yourusername
$ cd /usr/local/www/yourblog.ext

Now we're ready to install Ghost. It's going to complain a lot.

$ ghost install
✔ Checking system Node.js version
✔ Checking current folder permissions
System checks failed with message: 'Operating system is not Linux'
Some features of Ghost-CLI may not work without additional configuration.
For local installs we recommend using `ghost install local` instead.
? Continue anyway? (y/N) y
System stack check skipped
ℹ Checking operating system compatibility [skipped]
Local MySQL install not found. You can ignore this if you are using a remote MySQL host.
Alternatively you could:
a) install MySQL locally
b) run `ghost install --db=sqlite3` to use sqlite
c) run `ghost install local` to get a development install using sqlite3.
? Continue anyway? (y/N) y

So we told it yes regarding the OS and MySQL versions. We'll still be able to setup our MariaDB connection. Accept most of the defaults in the next round as we'll tweak these things later.

? Enter your blog URL: https://yourblog.ext/
? Enter your MySQL hostname: localhost
? Enter your MySQL username: ghost
? Enter your MySQL password: [hidden]
? Enter your Ghost database name: yourblog_ext_prod

Choose not to start Ghost. You can use ghost stop if you accidentally did. In the prior step I assumed you have the hostname for your site ready, either by a dns entry, or in your local hosts file for testing. We can talk about certificates later.

Fix the permissions of this file that has the database password in it:

chmod 600 /usr/local/www/yourblog.ext/config.production.json

Now edit config.production.json and change the following line to "local" instead of "systemd". Although it mentioned this in the wizard, actually using "local" during the install would have just given us a simple dev environment. We don't want this thing complaining about a lack of systemd every time we start it, though.

  "process": "local",

We can start Ghost now.

$ ghost start

The rest of the steps require you switch back to the root user.

If you already have a certificate for your site, skip this step.

Enable, but stop nginx. We're going to use the standalone mode to fetch the certificate because nginx isn't configured yet.

# service enable nginx
# service stop nginx

Run certbot, follow the prompts. This will install a certificate for you. The Nginx config examples assume you're using one. If your cert is from elsewhere, just sub in its path.

Edit the http block in /usr/local/etc/nginx/nginx.conf to follow standard practices:

http {
    include       mime.types;
    include       /usr/local/etc/nginx/sites-enabled/*.conf;
    default_type  application/octet-stream;
    sendfile      on;
    keepalive_timeout  65;
    gzip  on;
}

Create the sites-enabled and sites-available folders:

mkdir /usr/local/etc/nginx/sites-available
mkdir /usr/local/etc/nginx/sites-enabled

Edit /usr/local/etc/sites-available/yourblog_ext.conf to look like the following:

server {
    listen 80;
    server_name yourblog.ext www.yourblog.ext;
    location / {
        return 301 https://$host$request_uri;
    }
    location /.well_known/ {
        alias /usr/local/www/yourblog.ext/.well_known/;
    }
}

server {
    listen 443 ssl;
    server_name yourblog.ext www.yourblog.ext;
    location / {
        proxy_set_header HOST $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass       http://127.0.0.1:2368/;
    }
    location /ghost/ {
        proxy_set_header HOST $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass       http://127.0.0.1:2368/ghost/;
        allow 12.34.56.78; # YOUR IP ADDRESS GOES HERE otherwise your site will get hijacked
        deny  all;
    }
    ssl_certificate     /usr/local/etc/letsencrypt/live/yourblog.ext/fullchain.pem;
    ssl_certificate_key /usr/local/etc/letsencrypt/live/yourblog.ext/privkey.pem;
}

Here we have it redirecting to https because it's 2020. /ghost/ is the admin page, and we want it so that only you have access to it before you launch this. The setup wizard will let you create the admin account on the spot, and robots look for these kind of things. Afterward, you can either remove that, leave it, or add more allow lines to other addresses you'll connect from.

Now make sure that you or I did not make any mistakes here and resolve as needed.

ln -s /usr/local/etc/nginx/sites-available/yourblog_ext.conf /usr/local/etc/nginx/sites-enabled/yourblog_ext.conf
nginx -t
service nginx start

Login to https://[yourbloghere]/ghost/ and begin setup. You should be ready to go.

Let's throw a service together so this will persist past a reboot. Create /usr/local/etc/rc.d/ghost and modify the following to be relevant to your service:

#!/bin/sh

# PROVIDE: ghost
# REQUIRE: mysql-server
# KEYWORD: shutdown

. /etc/rc.subr

name="ghost"
rcvar="ghost_enable"
extra_commands="status"

load_rc_config ghost

start_cmd="ghost_start"
stop_cmd="ghost_stop"
restart_cmd="ghost_restart"
status_cmd="ghost_status"

PATH=/bin:/usr/bin:/usr/local/bin:/home/yourusername/.bin

ghost_start()
{
    su yourusername -c "/usr/local/bin/ghost start -d /usr/local/www/yourblog.ext"
}

ghost_stop()
{
    su yourusername -c "/usr/local/bin/ghost stop -d /usr/local/www/yourblog.ext"
}

ghost_restart()
{
    ghost_stop;
    ghost_start;
}

ghost_status()
{
    su yourusername -c "/usr/local/bin/ghost status -d /usr/local/www/yourblog.ext"
}

run_rc_command "$1"

Now try the following:

# chmod 755 /usr/local/etc/rc.d/ghost
# service ghost enable
# service ghost start

You should see details about the ghost process already running from earlier. If not, check the username and paths you used here.