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 update.FreeBSD.org mirrors... 3 mirrors found. Fetching public key from update6.freebsd.org... done. Fetching metadata signature for 11.1-RELEASE from update6.freebsd.org... 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: /bin/freebsd-version /boot/kernel/kernel ----- 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+http://pkg.FreeBSD.org/FreeBSD:11:amd64/quarterly, 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 gettext-runtime: 0.19.8.1_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 NAME USED AVAIL REFER MOUNTPOINT 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 NAME USED AVAIL REFER MOUNTPOINT 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 pf_enable="YES" pf_flags="" pf_rules="/etc/pf.conf" pflog_enable="YES" pflog_logfile="/var/log/pflog" pflog_flags=""

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 11.22.33.44 . 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="192.168.1.28" ## 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> { 10.0.0.0/24 } 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 = "192.168.1.28" 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> { 10.0.0.0/24 } 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 -> 192.168.1.28 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/dev.idontwatch.tv.crt \ -keyout /usr/local/etc/nginx/dev.idontwatch.tv.key Generating a 2048 bit RSA private key .......................+++ ......................................+++ writing new private key to '/usr/local/etc/nginx/ssl/dev.idontwatch.tv.key' ----- 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) []:dev.idontwatch.tv Email Address []:devnull@idontwatch.tv 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 dev.idontwatch.tv.crt -rw-r--r-- 1 root wheel 1704 Feb 22 11:41 dev.idontwatch.tv.key

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:

nginx.conf

worker_processes 1; #pid logs/nginx.pid; 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; server_name dev.idontwatch.tv; return 301 https://dev.idontwatch.tv$request_uri; } server { listen 443 ssl; listen [::]:443 ssl; server_name dev.idontwatch.tv; #SSL certs we just created ssl_certificate /usr/local/etc/nginx/ssl/dev.idontwatch.tv.crt; ssl_certificate_key /usr/local/etc/nginx/ssl/dev.idontwatch.tv.key; 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 http://10.0.0.80:2368; } 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/nginx.pid 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 10.0.0.0/24 range on that loopback adapter. To do that, add the following to /etc/rc.conf

# loopback for jails cloned_interfaces="lo1" ipv4_addrs_lo1="10.0.0.0 netmask 255.255.255.0"

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 options=600003<RXCSUM,TXCSUM,RXCSUM_IPV6,TXCSUM_IPV6> inet 10.0.0.0 netmask 0xff000000 inet 255.255.255.0 netmask 0xffffff00 nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL> 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 10.0.0.80 and verify that it is running correctly run:

root@dev:~ # ezjail-admin create ghost-prod 'lo1|10.0.0.80' Warning: Some services already seem to be listening on all IP, (including 10.0.0.80) 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 10.0.0.80 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: https://www.FreeBSD.org/releases/ Security Advisories: https://www.FreeBSD.org/security/ FreeBSD Handbook: https://www.FreeBSD.org/handbook/ FreeBSD FAQ: https://www.FreeBSD.org/faq/ Questions List: https://lists.FreeBSD.org/mailman/listinfo/freebsd-questions/ FreeBSD Forums: https://forums.FreeBSD.org/ 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 8.8.8.8" >> /etc/resolv.conf root@ghost-prod:~ # echo "nameserver 8.8.4.4" >> /etc/resolv.conf root@ghost-prod:~ # host google.com google.com has address 172.217.14.78 google.com has IPv6 address 2607:f8b0:4007:808::200e google.com mail is handled by 50 alt4.aspmx.l.google.com. google.com mail is handled by 10 aspmx.l.google.com. google.com mail is handled by 40 alt3.aspmx.l.google.com. google.com mail is handled by 30 alt2.aspmx.l.google.com. google.com mail is handled by 20 alt1.aspmx.l.google.com.

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 percona57-server: 5.7.20.18 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 gettext-runtime: 0.19.8.1_1 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 percona57-client: 5.7.20.18_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-5.7.20.18: ************************************************************************ 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.

SSMTP

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.

root=My.Gmail.Account@gmail.com mailhub=smtp.gmail.com:587 rewriteDomain=gmail.com hostname=dev.idontwatch.tv FromLineOverride=YES UseTLS=YES UseSTARTTLS=YES AuthUser=My.Gmail.Account@gmail.com AuthPass=CHANGEME #Debug=YES

And configure /usr/local/etc/revaliases

echo "root:My.Gmail.Account@gmail.com:smtp.gmail.com:587">> /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" devnull@idontwatch.tv

NOTE: If you start receiving errors such as

send-mail: Authorization failed (534 5.7.14 https://support.google.com/mail/bin/answer.py?answer=78754 n3908366pbc.83 - gsmtp) Can't send mail: sendmail process failed with error code 1

You may need to log into gmail then go to https://www.google.com/settings/security/lesssecureapps 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 )Jk_Nh8WG<dN 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 owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql>

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.

mysql> SET PASSWORD=PASSWORD('CHANGEME'); 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) mysql> CREATE USER 'ghost'@'%' IDENTIFIED BY 'CHANGEME'; 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) mysql> FLUSH PRIVILEGES; 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 owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> SHOW DATABASES; +--------------------+ | Database | +--------------------+ | information_schema | | ghost_prod | +--------------------+ 2 rows in set (0.00 sec) mysql> exit Bye 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 Goodbye!

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 https://yarnpkg.com/en/docs/install 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: https://dev.idontwatch.tv ? 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 https://dev.idontwatch.tv Ghost uses direct mail by default To set up an alternative email method read our docs at https://docs.ghost.org/docs/mail-config

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 https://idontwatch.tv/static/ghost.sh /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 https://dev.idontwatch.tv/ghost/ (don't click)

TODO

Short tutorial on ZFS backups

Resources