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
- EzJail on FreeBSD -- bookmark this, you will need to update your jails
- PF on FreeBSD
- ZFS on FreeBSD
- Ghost install documentation
- My ghost rc script on bitbucket