Installing Ghost on FreeBSD 11.1

Last Updated 2/25/2018

This is a guide to install Ghost 1.x on FreeBSD 11.1 inside of a jail. I will be using Percona's MySQL, but you can easily substitute for MySQL or MariaDB. Email will be handled via SSMTP, a mail forwarding agent that uses a SMTP account (gmail) to send mail, since the required functions do not necessitate a full mail server. The aim is to install a production environment for Ghost on FreeBSD.

This assumes you start with a fresh install of FreeBSD 11.1 with a ZFS zpool of some sorts, and that Nginx is being run on the host machine which proxies requests to the jail(s) where node and MySQL (Percona MySQL in this case) are running. This will seem to be really long, but it includes a lot command output and covers everything from start to finish.

The virtual machine I am using in this guide is a Xen Server VM with 4GB of RAM and 80GB SSD disk. Previously I ran Ghost with sqlite3 on a 2GB VM and it was fine, but your results may vary.

Since we're dealing with a jail inside of FreeBSD, it is crucial to pay attention to the command prompts in the examples. root@dev:~ # is the host, root@ghost-prod:~ # or ghost@ghost-prod:~ $ is the jail. It may be simpler to think that Nginx is the only daemon we need to be concerned with on the host, everything else is inside of the jail.

First thing we're going to do is make sure our operating system is up to date with freebsd-update fetch, then install:

root@dev:~ # freebsd-update fetch
src component not installed, skipped
Looking up mirrors... 3 mirrors found.
Fetching public key from done.
Fetching metadata signature for 11.1-RELEASE from done.
Fetching metadata index... done.
Fetching 2 metadata files... done.
Inspecting system... done.
Preparing to download files... done.
Fetching 44 patches.....10....20....30....40.. done.
Applying patches... done.

The following files will be updated as part of updating to 11.1-RELEASE-p6:
----- snip -----

root@dev:~ #
root@dev:~ # freebsd-update install
src component not installed, skipped
Installing updates... done.

Next we need to install some packages on the host. Feel free to build from ports if you want, this guide will not be covering that.

root@dev:~ # pkg install ezjail nginx-devel rsync screen sudo wget
The package management tool is not yet installed on your system.
Do you want to fetch and install it now? [y/N]: y
Bootstrapping pkg from pkg+, please wait...

---- snip ----

The following 14 package(s) will be affected (of 0 checked):

New packages to be INSTALLED:
        ezjail: 3.4.2
        nginx-devel: 1.13.7_3
        rsync: 3.1.3
        screen: 4.6.2
        sudo: 1.8.21p2_1
        wget: 1.19.2
        pcre: 8.40_1
        libiconv: 1.14_11
        indexinfo: 0.3.1
        libidn2: 2.0.4
        libunistring: 0.9.8
        zfsnap: 1.11.1
        zxfer: 1.1.6

Number of packages to be installed: 14

The process will require 21 MiB more space.
5 MiB to be downloaded.

Proceed with this action? [y/N]: y
[1/14] Fetching ezjail-3.4.2.txz: 100%   43 KiB  43.8kB/s    00:01
----- snip -----

ZFS Setup

Before we set anything up we need to create a the space where we want our zpools and where additional datasets will be created as new jails are created. This makes them super simple to snapshot and backup.

First let's look at our disk setup to determine our zpool name so we can create our dataset. Run:

root@dev:~ # zfs list
zroot               1.13G  74.0G    88K  /zroot
zroot/ROOT           489M  74.0G    88K  none
zroot/ROOT/default   489M  74.0G   489M  /
zroot/tmp             88K  74.0G    88K  /tmp
zroot/usr            663M  74.0G    88K  /usr
zroot/usr/home       128K  74.0G   128K  /usr/home
zroot/usr/ports      663M  74.0G   663M  /usr/ports
zroot/usr/src         88K  74.0G    88K  /usr/src
zroot/var            592K  74.0G    88K  /var
zroot/var/audit       88K  74.0G    88K  /var/audit
zroot/var/crash       88K  74.0G    88K  /var/crash
zroot/var/log        144K  74.0G   144K  /var/log
zroot/var/mail        88K  74.0G    88K  /var/mail
zroot/var/tmp         96K  74.0G    96K  /var/tmp

Here my zpool is named zroot (the default for FreeBSD installer). I want to keep the jails in /usr/jails/ so I'm going to create my dataset on zroot and verify it was created and mounted properly

root@dev:~ # zfs create -o mountpoint=/usr/jails zroot/usr/jails
root@dev:~ # zfs list zroot/usr/jails
zroot/usr/jails    88K  74.0G    88K  /usr/jails

Setup the firewall with PF

We are going to use PF as a firewall on the host FreeBSD machine and use PF to provide our jail with network access since it only has access to a local-only lo1 network adapter. We are going to be enabling connections to port 22 for SSH, 80 for HTTP and 443 for SSL/HTTPS.

First we need to enable PF in /etc/rc.d by adding the following lines:

# PF

Then we need to create a table for bad hosts, this is a list of blocked I.P. addresses. It is a table that is only read when PF starts, if you need to add other I.P. addresses add them to the file, then to the current pf tables with pfctl -t blocklist -T add To create the table:

root@dev::~ # touch /etc/pf.blocklist

Now we copy over out pf config to /etc/pf.conf:

## /etc/pf.conf for FreeBSD

ext_if="xn0"             ## CHANGE ME
public_ip="" ## CHANGE ME
loop_if="lo1" # jail loopback device

set skip on lo
set skip on $loop_if
set debug urgent

set timeout { tcp.closing 60, tcp.established 7200 }

scrub in on $ext_if all

table <jailnet> { }
table <blocklist> persist file "/etc/pf.blocklist"
#table <whitelist> persist file "/etc/pf.whitelist"

# NAT rules
nat pass on $ext_if from <jailnet> to any -> $public_ip
nat on $ext_if from <jailnet> to any -> ($ext_if)  ## permits outoging traffic from the jails

#set block-policy return
block in all
pass out all
block on $ext_if from <blocklist> to any

pass inet proto tcp from any to port 22 flags S/SA keep state
#pass inet proto tcp from <whitelist> to port 22 flags S/SA keep state
pass inet proto tcp from any to port 80 flags S/SA keep state
pass inet proto tcp from any to port 443 flags S/SA keep state

## icmp settings
icmp_types = "{ echoreq, unreach }"   ## needed for traceroute and ping
pass inet proto icmp all icmp-type $icmp_types keep state

Let's make sure we don't have any syntax errors by checking the configuration file with pfctl like this:

root@dev:~ # pfctl -vnf /etc/pf.conf
ext_if = "xn0"
public_ip = ""
loop_if = "lo1"
set skip on { lo }
set skip on { lo1 }
set debug urgent
set timeout tcp.closing 60
set timeout tcp.established 7200
table <jailnet> { }
table <blocklist> persist file "/etc/pf.blocklist"
icmp_types = "{ echoreq, unreach }"
scrub in on xn0 all fragment reassemble
nat pass on xn0 inet from <jailnet> to any ->
nat on xn0 from <jailnet> to any -> (xn0) round-robin
block drop in all
pass out all flags S/SA keep state
block drop on xn0 from <blocklist> to any
pass inet proto tcp from any to any port = ssh flags S/SA keep state
pass inet proto tcp from any to any port = http flags S/SA keep state
pass inet proto tcp from any to any port = https flags S/SA keep state
pass inet proto icmp all icmp-type echoreq keep state
pass inet proto icmp all icmp-type unreach keep state

Everything is good. Now to enable PF. If you are connected via SSH you will be disconnected, don't freak out. If you are working on a remote machine set a cron job as root to disable PF in 15 minutes if you are worried about locking yourself out. You can do that by editing root's crontab crontab -e and adding in
*/4 * * * * /sbin/pfctl -d. If you do this make sure you disable it after verifying connectivity or your jail will not have network access.

Now enable pf

service pf start

Reconnect and we're good. If you set up that crontab be sure to disable it!

SSL Self Signed Certificate

This can also be done with Let's Encrypt but doing so on FreeBSD is a rather lengthy task and can introduce vulnerabilities if not done properly. Building it requires using LibreSSL which is incompatible with OpenSSL, which is needed for Nginx. Nginx can use LibreSSL but it has a lot of other compatibility issues that need to be worked around and is outside of the scope of this tutorial. If you need an SSL certificate to be signed by a Certificate Authority such as Comodo, Digicert, Thawte, etc., they will have a guide for you to follow so you can send the CA your CSR and they will sign and send you your cert. The rest of this section is based off of my self-signed SSL certificate. This will create a self-signed cert good for 10 years.

First we need to create our SSL directory and make it readable only by root, to do that run:

root@dev:~ # mkdir /usr/local/etc/nginx/ssl
root@dev:~ # chmod 600 /usr/local/etc/nginx/ssl

Then we create our self-signed certificate and private key. Since it is self signed fill in the requested info with whatever you like, the only important one is the Common Name (e.g. server FQDN or YOUR name), make sure you enter in your domain name you plan on using.

root@dev:~ # openssl req -new -x509 -nodes -out /usr/local/etc/nginx/ssl/ \
-keyout /usr/local/etc/nginx/
Generating a 2048 bit RSA private key
writing new private key to '/usr/local/etc/nginx/ssl/'
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:New California Republic
Locality Name (eg, city) []:Dayglow
Organization Name (eg, company) [Internet Widgits Pty Ltd]:idontwatchtv
Organizational Unit Name (eg, section) []:dev
Common Name (e.g. server FQDN or YOUR name) []
Email Address []
root@dev:~ # ls -al /usr/local/etc/nginx/ssl
total 18
drw-------  2 root  wheel     4 Feb 22 11:42 .
drwxr-xr-x  5 root  wheel    18 Feb 22 10:18 ..
-rw-r--r--  1 root  wheel  1497 Feb 22 11:41
-rw-r--r--  1 root  wheel  1704 Feb 22 11:41

Configure Nginx

First let's enable nginx:

root@dev:~ # echo 'nginx_enable="YES"' >> /etc/rc.conf

Create Nginx directories where we will store our per-site config files. This is the way Nginx works on Debian, the FreeBSD port stuffs it all into one big configuration file in /usr/local/etc/nginx/nginx.conf which becomes difficult to maintain. Using these subdirectories we can just symlink the vhost configs that we want to use in /usr/local/etc/nginx/sites-enabled rather than commenting out dozens of lines. So create those directories:

root@dev:~ # mkdir /usr/local/etc/nginx/{sites-available,sites-enabled}

/usr/local/etc/nginx/nginx.conf is already backed up as nginx.conf-dist so just replace the entire nginx.conf with the following:

worker_processes  1;

#pid        logs/;

events {
    worker_connections  1024;

http {
    server_tokens off;
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    include /usr/local/etc/nginx/sites-enabled/*;


And then create the vhost for ghost. This is going to force SSL, set up logging to /var/log/nginx/, and proxy all connections to our ghost jail. In /usr/local/etc/nginx/sites-available/ghost paste the following:

server {
    # Listen on 80, redirect to https
    listen 80;
    listen [::]:80;
    return 301$request_uri;

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    #SSL certs we just created
    ssl_certificate     /usr/local/etc/nginx/ssl/;
    ssl_certificate_key /usr/local/etc/nginx/ssl/;

    ssl on;
    access_log /var/log/nginx/ghost-access.log;
    error_log  /var/log/nginx/ghost-error.log;

    root /usr/local/www/nginx; #This doesn't matter, we're proxying connections
    index index.html index.txt;

    rewrite ^/index.php/(.*) /$1  permanent;

    location / {
        proxy_set_header        X-Forwarded-Proto https;
        proxy_set_header        X-Real-IP $remote_addr;
        proxy_set_header        Host $http_host;
        proxy_pass    ;

    location ~ /\.ht {
        deny all;

Then to activate the vhost, symlink from sites-available to sites-enabled:

root@dev:~ # ln -s /usr/local/etc/nginx/sites-available/ghost /usr/local/etc/nginx/sites-enabled/

Check our configuration to make sure there aren't any problems:

root@dev:~ # service nginx configtest
Performing sanity check on nginx configuration:
nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok
nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful

Now we can start nginx, it will try to proxy to a jail that will be created shortly

root@dev:~ # service nginx start
Performing sanity check on nginx configuration:
nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok
nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful
Starting nginx

To finish up, let's setup newsyslog to rotate our http logs every week on Sunday at midnight, and make them only readable by root. You may want to change this to a different user so that your logs can be fetched remotely from wherever you store your logs, consult man newsyslog.conf to understand the different options. This also requires that we send a SIG1 to Nginx's pid when the log is being rotated. To do this:

echo '/var/log/nginx/*.log root:root 600 * * $W0D0 GZ /var/run/ 1' >> /etc/newsyslog.conf.d/nginx

ezjail configuration

For jail management we're going to use ezjail and we need to enable the ZFS options so we can create backups.

In /usr/local/etc/ezjail.conf under "ZFS Options" uncomment ezjail_use_zfs="YES", ezjail_use_zfs_for_jails="YES", and uncomment and change ezjail_jailzfs="tank/ezjail" to ezjail_jailzfs="zroot/usr/jails" so that it reflects the correct zpool and dataset. Use your favorite text editor and change those three lines or run these sed lines:

root@dev:~ # sed -ie '/^#.* ezjail_use_zfs/s/^# //g' /usr/local/etc/ezjail.conf
root@dev:~ # sed -ie '/^#.* ezjail_jailzfs/s/^# //g' /usr/local/etc/ezjail.conf
root@dev:~ # sed -ie 's/ezjail_jailzfs="tank\/ezjail"/ezjail_jailzfs="zroot\/usr\/jails"/g' /usr/local/etc/ezjail.conf

We need to create a loopback for our jails and we're going give them IP addresses so we can route traffic to it and connect it to the internet. I am going to be using addresses for jails in the range on that loopback adapter. To do that, add the following to /etc/rc.conf

# loopback for jails
ipv4_addrs_lo1=" netmask"

Then to get that interface up run:

root@dev:~ # service netif cloneup
Created clone interfaces: lo1.

And to verify everything was done correctly we'll make sure we can see the adapter

root@dev:~ # ifconfig lo1
lo1: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384
        inet netmask 0xff000000
        inet netmask 0xffffff00
        groups: lo

No we can enable ezjail in /etc/rc.conf and start it

root@dev:~ # echo 'ezjail_enable="YES"' >> /etc/rc.conf
root@dev:~ # service ezjail start
 ezjailroot@dev:~ #

Now we create a basejail. A basejail is an isolated jail that holds shared operating system and ports files so we don't have to re-create and maintain it over several jails. To create the basejail, run:

root@dev:~ # ezjail-admin install -p

Now we're ready to create our jail for ghost. To create the jail with an IP address of and verify that it is running correctly run:

root@dev:~ # ezjail-admin create ghost-prod 'lo1|'
Warning: Some services already seem to be listening on all IP, (including
  This may cause some confusion, here they are:
root     ntpd       557   20 udp6   *:123                 *:*
root     ntpd       557   21 udp4   *:123                 *:*
root     ntpd       557   27 udp4   *:123                 *:*
root     ntpd       557   28 udp4   *:123                 *:*
root     syslogd    399   6  udp6   *:514                 *:*
root     syslogd    399   7  udp4   *:514                 *:*
root@dev:~ # ezjail-admin start ghost-prod
Starting jails: ghost-prod.
/etc/rc.d/jail: WARNING: Per-jail configuration via jail_* variables  is obsolete.  Please consider migrating to /etc/jail.conf.
root@dev:~ # ezjail-admin list
STA JID  IP              Hostname                       Root Directory
--- ---- --------------- ------------------------------ ------------------------
ZR  1       ghost-prod                     /usr/jails/ghost-prod

Now we log into the jail. NOTE: Notice the different prompts with the commands being run, anything in the jail will have the hostname ghost-prod

root@dev:~ # ezjail-admin console ghost-prod
FreeBSD 11.1-RELEASE (GENERIC) #0 r321309: Fri Jul 21 02:08:28 UTC 2017

Welcome to FreeBSD!

Release Notes, Errata:
Security Advisories:
FreeBSD Handbook:
Questions List:
FreeBSD Forums:

Documents installed with the system are in the /usr/local/share/doc/freebsd/
directory, or can be installed later with:  pkg install en-freebsd-doc
For other languages, replace "en" with a language code like de or fr.

Show the version of FreeBSD installed:  freebsd-version ; uname -a
Please include that output and any error messages when posting questions.
Introduction to manual pages:  man man
FreeBSD directory layout:      man hier

Edit /etc/motd to change this login announcement.
root@ghost-prod:~ #

Configure the jail

The first things we need to do in the jail are set a root password and add a dns resolver so we can resolve domain names. To set a root pass run passwd

root@ghost-prod:~ # passwd
Changing local password for root
New Password:
Retype New Password:

And set a DNS resolver. For this example we're just going to use google's resolvers, feel free to use others. Set resolvers by putting them in /etc/resolv.conf and then test by running a DNS query

root@ghost-prod:~ # echo "nameserver" >> /etc/resolv.conf
root@ghost-prod:~ # echo "nameserver" >> /etc/resolv.conf
root@ghost-prod:~ # host has address has IPv6 address 2607:f8b0:4007:808::200e mail is handled by 50 mail is handled by 10 mail is handled by 40 mail is handled by 30 mail is handled by 20

Install packages in the jail

Now we need to install our packages. This is where package versions become extremely important, otherwise we will be running into issues later. This is where this information is important. We will be using node8 and npm for node8.

root@ghost-prod:~ # pkg install node8 npm-node8 percona57-server bash ssmtp python3 sudo
The package management tool is not yet installed on your system.
Do you want to fetch and install it now? [y/N]: y
----- snip -----
The following 26 package(s) will be affected (of 0 checked):

New packages to be INSTALLED:
        node8: 8.9.3
        npm-node8: 5.6.0_2
        bash: 4.4.12_3
        ssmtp: 2.64_3
        ca_root_nss: 3.35
        c-ares: 1.12.0_2
        libuv: 1.18.0
        icu: 60.2_1,1
        python27: 2.7.14_1
        readline: 7.0.3_1
        indexinfo: 0.3.1
        libffi: 3.2.1_2
        gmake: 4.2.1_1
        perl5: 5.24.3
        curl: 7.58.0
        libnghttp2: 1.29.0
        libevent: 2.1.8_1
        libedit: 3.1.20170329_2,1
        cyrus-sasl: 2.1.26_12
        liblz4: 1.8.0,1
        python3: 3_3
        python36: 3.6.4
        sudo: 1.8.21p2_1

Number of packages to be installed: 26

The process will require 531 MiB more space.
79 MiB to be downloaded.

Proceed with this action? [y/N]: y
[ghost-prod] [1/26] Fetching node8-8.9.3.txz: 100%    4 MiB   2.3MB/s    00:02
----- snip -----

Message from percona57-server-


Remember to run mysql_upgrade the first time you start the MySQL server
after an upgrade from an earlier version.

Initial password for first time use of MySQL is saved in $HOME/.mysql_secret
ie. when you want to use "mysql -u root -p" first you should see password
in /root/.mysql_secret

Message from ssmtp-2.64_3:

sSMTP has been installed successfully.

To replace sendmail with ssmtp type "make replace" or change
your /etc/mail/mailer.conf to:

sendmail        /usr/local/sbin/ssmtp
send-mail       /usr/local/sbin/ssmtp
mailq           /usr/local/sbin/ssmtp
newaliases      /usr/local/sbin/ssmtp
hoststat        /usr/bin/true
purgestat       /usr/bin/true

However, before you can use the program, you should copy the files
"revaliases.sample" and "ssmtp.conf.sample" in /usr/local/etc/ssmtp
to "revaliases" and "ssmtp.conf" respectively and edit them to suit
your needs.


Next lets setup and configure ssmtp to use a Gmail account so our welcome emails and password reset tools work. There are many ways to configure this but I am going to set an email address to receive all emails that are GUID < 1000 (emails that go to root) which will be the same account that email is sent FROM. There are sample ssmtp.conf and revalises in the /usr/local/etc/ssmtp folder for reference, but rather than going through the config we're just going to create new files, the samples will remain as a reference. Create /usr/local/etc/ssmtp/ssmtp.conf and paste in this information, changing it to use your Gmail account, or other SMTP account.

And configure /usr/local/etc/revaliases

echo "">> /usr/local/etc/ssmtp/revaliases

And the last part we need to configure /etc/mail/mailer.conf with the settings shown above. Empty that file out or comment out the lines and then paste:

sendmail        /usr/local/sbin/ssmtp
send-mail       /usr/local/sbin/ssmtp
mailq           /usr/local/sbin/ssmtp
newaliases      /usr/local/sbin/ssmtp
hoststat        /usr/bin/true
purgestat       /usr/bin/true

And then send off a test email:

root@ghost-prod:~ # echo "testing from ghostdev" | mail -s "dev.idontwatch test"

NOTE: If you start receiving errors such as

send-mail: Authorization failed (534 5.7.14 n3908366pbc.83 - gsmtp)
Can't send mail: sendmail process failed with error code 1

You may need to log into gmail then go to and enable that feature (credit to ServerFault user: masegaloeh Source). If you still aren't seeing your emails enable Debugging in ssmtp.conf by uncommenting the Debug=YES line at the bottom. Check spam filters, and whitelist your sending email account if necessary. If you are running into problems with this you may also get throttled for trying to send too many emails that are bouncing which looks suspicious to spam filters.

Configure Percona MySQL

First we need to enable it and start it

root@ghost-prod:~ # echo 'mysql_enable="YES"' >> /etc/rc.conf
root@ghost-prod:~ # service mysql-server start
Starting mysql.

Now we need to login to MySQL, change root's password and create an account for Ghost. The default password for root's login to MySQL is in /root/.mysql_secret

root@ghost-prod:~ # cat .mysql_secret
# Password set for user 'root@localhost' at 2018-02-22 13:00:16
root@ghost-prod:~ # mysql -u root -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 3
Server version: 5.7.20-18

Copyright (c) 2009-2017 Percona LLC and/or its affiliates
Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.


Now at the mysql> prompt, the first thing we have to change root's password, it won't let us do anything else until we do.

Query OK, 0 rows affected, 1 warning (0.00 sec)

Now we need to create a database for ghost (ghost_prod), a mysql user for ghost, then grant that user full access to the database ghost_prod. To accomplish this run:

mysql> CREATE DATABASE ghost_prod;
Query OK, 1 row affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

mysql> GRANT ALL PRIVILEGES ON ghost_prod.* TO 'ghost'@'%';
Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

To verify we have everything setup properly, exit the mysql> prompt (type exit and press enter) and login as user ghost to make sure we can see our database.

root@ghost-prod:~ # mysql -u ghost -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 16
Server version: 5.7.20-18 Source distribution

Copyright (c) 2009-2017 Percona LLC and/or its affiliates
Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

| Database           |
| information_schema |
| ghost_prod         |
2 rows in set (0.00 sec)

mysql> exit
root@ghost-prod:~ #

Create a system user account for ghost

Next we need to create a ghost user so node isn't running as root. Note: It is important to note that a MySQL user and a system user are two different accounts. To create the user account run adduser and press enter through the responses until you have to enter a password and then confirm your account creation, then if everything is fine accept, then select no and leave. In this example I am creating user ghost with a bash shell:

root@ghost-prod:~ # adduser
Username: ghost
Full name:
Uid (Leave empty for default):
Login group [ghost]:
Login group is ghost. Invite ghost into other groups? []:
Login class [default]:
Shell (sh csh tcsh bash rbash nologin) [sh]: bash
Home directory [/home/ghost]:
Home directory permissions (Leave empty for default):
Use password-based authentication? [yes]:
Use an empty password? (yes/no) [no]:
Use a random password? (yes/no) [no]:
Enter password:
Enter password again:
Lock out the account after creation? [no]:
Username   : ghost
Password   : *****
Full Name  :
Uid        : 1001
Class      :
Groups     : ghost
Home       : /home/ghost
Home Mode  :
Shell      : /usr/local/bin/bash
Locked     : no
OK? (yes/no): yes
adduser: INFO: Successfully added (ghost) to the user database.
Add another user? (yes/no): no

Install the ghost-cli package

To install Ghost we're going to use npm to install ghost-cli as root. The account ghost does not have permissions to install system wide packages, so we will be installing it as root, but Ghost will be run with the privileges of the user ghost for security purposes, there is no reason it needs to run as root. To install ghost run:

root@ghost-prod:~ # npm i -g ghost-cli
npm WARN deprecated yarn@1.3.2: It is recommended to install Yarn using the native installation method for your environment. See
npm WARN deprecated fs-promise@0.5.0: Use mz or fs-extra^3.0 with Promise Support
/usr/local/bin/ghost -> /usr/local/lib/node_modules/ghost-cli/bin/ghost
+ ghost-cli@1.5.2
updated 1 package in 7.459s

Setting up our Ghost install

Now we need to change users to our system user ghost. From there we are going to create a directory called www and install ghost via ghost-cli in that directory. It is going to prompt several questions

[ghost@ghost-prod /usr/home/ghost]$ mkdir www
[ghost@ghost-prod /usr/home/ghost]$ cd www
[ghost@ghost-prod /usr/home/ghost/www]$ 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.

Yes we want to continue

? Continue anyway? Yes
ℹ 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? Yes

Yes, still want to continue

ℹ Checking for a MySQL installation [skipped]
✔ Checking for latest Ghost version
✔ Setting up install directory
✔ Downloading and installing Ghost v1.21.3
✔ Finishing install process

Here we give it our https url then database information. Remember to use the MySQL user information that was created from the MySQL CLI, and don't use root!

? Enter your blog URL:
? Enter your MySQL hostname: localhost
? Enter your MySQL username: ghost
? Enter your MySQL password: [hidden]
? Enter your Ghost database name: ghost_prod
✔ Configuring Ghost
✔ Setting up instance

This next part it would want to create a user if we entered in root, we didn't and don't want to, so No.

? Do you wish to set up "ghost" mysql user? No
ℹ Setting up "ghost" mysql user [skipped]

Nginx is running on the host so we can create multiple jails and vhosts, so no, don't need that

? Do you wish to set up Nginx? No
ℹ Setting up Nginx [skipped]
Task ssl depends on the 'nginx' stage, which was skipped.
ℹ Setting up SSL [skipped]

Since we're using FreeBSD and not Linux we don't have systemd as part of our operating system and we will have to setup an rc script to start, stop, and restart ghost, so select No.

? Do you wish to set up Systemd? No
ℹ Setting up Systemd [skipped]
✔ Running database migrations
? Do you want to start Ghost? Yes
Process manager 'systemd' will not run on this system, defaulting to 'local'
✔ Checking current folder permissions
✔ Validating config
✔ Checking folder permissions
✔ Checking file permissions
✔ Starting Ghost
You can access your blog at

Ghost uses direct mail by default
To set up an alternative email method read our docs at

Finishing Touches

Now everything should be working but we need to make a a change in the config file for ghost /home/ghost/www/config.production.json to change our mail transport to use ssmtp which is masquerading as sendmail.

In the jail, as user ghost, we need to change the mail transport from "Direct" to "sendmail", to do that run this sed command then restart ghost:

[ghost@ghost-prod ~]$ sed -ie 's/Direct/sendmail/g' /home/ghost/www/config.production.json
[ghost@ghost-prod ~]$ cd www
[ghost@ghost-prod ~/www]$ ghost restart
Process manager 'systemd' will not run on this system, defaulting to 'local'
✔ Restarting Ghost

rc script for Ghost

In order to have Ghost start on boot I wrote an rc script for it. It needs to be placed in /usr/local/etc/rc.d/ inside the jail. You will need to download it (local mirror) to the correct location and make it executable, then edit rc.conf so that it starts at boot. Example below:

root@ghost-prod:~ # fetch -o /usr/local/etc/rc.d/ghost
/usr/local/etc/rc.d/ghost                     100% of  675  B 9533 kBps 00m00s
root@ghost-prod:~ # chmod +x /usr/local/etc/rc.d/ghost

Then update /etc/rc.conf to enable ghost and set the path where ghost is installed

root@ghost-prod:~ # echo 'ghost_enable="YES"' >> /etc/rc.conf
root@ghost-prod:~ # echo 'ghost_path="/home/ghost/www" >> /etc/rc.conf

Do your Ghost thing

Now to setup your Ghost install go to the URL, the same one you made an SSL cert for, and add /ghost to it in order to create your account on Ghost so you can admin the blog. In my case, I would go to (don't click)


  • Short tutorial on ZFS backups