Skip to content
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
777 lines (574 sloc) 25.2 KB


How to install the Ghost blogging platform on Fedora Linux

Ghost is a blogging platform. One of the most popular and widely deployed. It's open source (MIT License) and written in JavaScript. It's designed to be beautiful, modern, and relatively simple to use by individual bloggers as well as online publications.

I looked at a whole pile of blogging options when I finally decided to go with Ghost. It checks all the boxes for what I required in a blogging platform: It's well supported, and, by the looks of it, well designed. A platform that embraces Markdown (a critical, must-have feature IMHO) and is easy to use and maintain. All on Linux, of course. And 100% had to be open source. Bonus! Ghost is wildly popular, so it should have some longevity.

But there was a stumbling block. The installation instructions for Fedora Linux were incomplete and very dated.

I fixed that with this article. Enjoy.

If you are reading this on another platform, the catalyst for this endeavor was

This howto will walk you through:

  • Installation of minimal Fedora Linux OS on a VPS system
  • Configure it to be secure (firewalld, SSH keys, fail2ban, etc.)
  • Generation and installation of your Let's Encrypt SSL cert and keys that you are using TLS/SSL as you should be
  • Setting that cert to auto renew
    ...via a systemd service, not a hacky crontab or script
  • Installation and configuration of Nginx
  • Installation and configuration of Ghost
    ...using sqlite3 and not MySQL or MariaDB with is unneeded complexity IMHO
  • Setting up Ghost to be managed by systemd
    ...and not pm2 or screen or some other less reliable mechanism
  • Some troubleshooting guidance
  • Properly setting up email support on the system
  • Setting up subscription management via MailChimp
  • Backing everything up

The technical specs of what was used to develop my blog and write this howto were...

  • VPS -- 1G RAM, 25G SSD storage, 1vCore CPU, and a lot of available bandwidth
  • Fedora Linux 30 (and the latest FirewallD, etc.)
  • OS Supplied:
    • Web server: Nginx - nginx-1.16.0-3.fc30.x86_64
    • Database: SQLite - sqlite-3.26.0-5.fc30.x86_64
    • Development: NodeJS - nodejs-10.16.0-3.fc30.x86_64
    • Node.js compiler: node-gyp - node-gyp-3.6.0-7.fc30.noarch
    • Email relayer: sSMTP - ssmtp-2.64-22.fc30.x86_64
    • Let's Encrypt (TLS) commandline frontend: Certbot - certbot-0.34.2-3.fc30.noarch
  • Ghost - zip file downloaded was 2.23.4

Install the server

[0] Install the latest Fedora Linux server

Follow the instructions for "HowTo Deploy and Configure a Minimalistic Fedora Linux Server" found here:

[1] Install additional packages

# Stuff I like
sudo dnf install vim-enhanced screen -y
# Development-ish and app-related stuff
sudo dnf install nginx nodejs node-gyp make certbot git -y

[2] Purchase a domain name and configure DNS at your registrar is one of my current favorite registrars, but there are many. Purchase a domain, for example, And for the purposes of this article, we'll also assume your blog is and your system's IP address is

Once purchased, edit the DNS tables and point your domain at your new server. The DNS record would look something like: blog A 1800

If you want the raw domain to route there, then @ A 1800

[3] Obtain an SSL (TLS) certificate

Note: certbot is the Let's Encrypt client that is used to generate the appropriate TLS certificate for your domain.

Fetch your SSL keys and install them on the system

# In this example,,, and
# all will be routable, securely, to this IP
sudo certbot certonly --standalone --domains,, --email --agree-tos --rsa-key-size 2048

Note: If you already have nginx or another webserver running on this host, you may have to pause it before running certbot and then start it up again. I.e., sudo systemctl stop nginx ...then execute the above command, and then... sudo systemctl start nginx

Certbot will populate this directory: /etc/letsencrypt/live/

Setup certbot to auto-renew

We'll use the built in systemd services to do this...

Edit /etc/sysconfig/certbot

Set the service POST_HOOK as such:

POST_HOOK="--post-hook '/usr/bin/systemctl reload nginx.service'"

Enable the renewal service

sudo systemctl enable --now certbot-renew.timer

Configure Nginx

[4] Configure SELinux to allow Nginx traffic to flow to Ghost

Nginx relays traffic through port 2368 (in our configuration) which is the port that the Ghost application manages things through. SELinux does not like this.

I was unsuccessful in getting SELinux to work in enforcing mode. Until I, or someone else, figures it out, set SELinux to permissive:

# This will take immediate effect, but not survive reboot
sudo setenforce permissive

To ensure it sticks after reboot...
Edit /etc/selinux/config and set SELINUX=permissive

Extra credit work...
If you can figure out how to get SELinux to work with ghost proxying things to port 2368, more power to you. I got stuck here:

  • I examined /var/log/audit/audit.log (after full installation)
  • I even used audit2why and sealert to analysize things
  • I ran this: sudo semanage port -a -t http_port_t -p tcp 2368 ; sudo semanage port -l|grep http
  • I then restarted nginx and tried again. But I couldn't get it to work.

[5] Crank up nginx

sudo systemctl start nginx.service && sudo systemctl enable nginx.service

[6] Configure /etc/nginx/conf.d/ghost.conf

Or better yet, /etc/nginx/conf.d/

server {
  # This manages blog.DOMAIN
  listen 80;
  listen [::]:80;
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  ssl_certificate /etc/letsencrypt/live/;
  ssl_certificate_key /etc/letsencrypt/live/;
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

  location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-Proto $scheme;

server {
  # This redirects www.DOMAIN and the bare DOMAIN to blog.DOMAIN
  listen 80;
  listen [::]:80;
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  ssl_certificate /etc/letsencrypt/live/;
  ssl_certificate_key /etc/letsencrypt/live/;
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

  return 301 $scheme://$request_uri;

[7] Check Nginx syntax:

sudo nginx -t

[8] Reload Nginx configuration:

sudo systemctl reload nginx.service

Install Ghost

[9] Create a ghost user

sudo useradd -c "Ghost Application" ghost 

[10] Create Ghost's document root and set permissions

Default webroot for Nginx is /usr/share/nginx/html. Note that /usr/share is for static read-only content. Therefore, we want to create and use our own webroot for Ghost. For this we create and use /var/www/ghost

# Create docroot/webroot and set permissions
sudo mkdir -p /var/www/ghost
sudo chown -R ghost:ghost /var/www/ghost
sudo chmod 775 /var/www/ghost

[11] Download Ghost

sudo dnf install curl jq -y
sudo -u ghost curl -L $(curl -sL | jq -r '.assets[].browser_download_url') -o /tmp/
# Note: Downloading to /tmp so that any user has permissions to it

# Alternative:
# curl -sL | jq -r '.assets[].browser_download_url' | sudo -u ghost xargs -I GHOST_URL curl -L GHOST_URL -o /tmp/

[12] Unzip/Refresh Ghost application

sudo -u ghost unzip -uo /tmp/ -d /var/www/ghost
sudo rm /tmp/

[13] Navigate to /var/www/ghost as ghost user

sudo su - ghost
cd /var/www/ghost

[14] Install Ghost

# You are still the ghost user
cd /var/www/ghost ; npm install --production

[15] Configure Ghost to use your domain and sqlite3

# You are still the ghost user
cd core/server/config/env/
cp -a config.production.json config.production.json--ORIGINAL

Swap out the existing config.production.json with something that looks like this...

  "url" : "",
  "server": {
    "port": 2368,
    "host": ""
  "database": {
    "client": "sqlite3",
    "connection": {
      "filename": "/var/www/ghost/content/data/ghost.db"
  "mail": {
    "transport": "Direct"
  "logging": {
    "level": "info",
    "rotation": {
      "enabled": true
    "transports": ["file", "stdout"]
  "paths": {
    "contentPath": "/var/www/ghost/content"
  "privacy": {
    "useUpdateCheck": true,
    "useGravatar": false,
    "useRpcPing": true,
    "useStructuredData": true

Note, once you have Ghost up and running, create a redundant backup of this file. Future updates tend to overwrite it: cp -a config.production.json config.production.json--backup

[16] Start Ghost (initial testing)

# You are still the ghost user
cd /var/www/ghost ; NODE_ENV=production node index.js
#cd /var/www/ghost ; npm start --production

[17] Test that it works

Browse to (or whatever your configured domain is).

Once satisfied, shut it down with ^C in the window that you are using to run ghost from the commandline.

[18] Configure a Ghost systemd service

Log in as your normal linux user (not root, not ghost).

Edit the systemd ghost.service...

sudo vim /etc/systemd/system/ghost.service

Add this, save, and exit...


ExecStart=/usr/bin/node index.js


[20] Crank up that Ghost service!

# You are still your normal working user
sudo systemctl daemon-reload
sudo systemctl start ghost.service
sudo systemctl status ghost.service
sudo systemctl enable ghost.service

Test it again.

Post install configuration

[21] Set up your admin credentials

Browse to and set your credentials promptly.

[22] Change your theme (if you like)

The default is nice, but there are others. Find a theme you like, download and unzip to /var/www/ghost/content/themes and configure in the link provided above.

Some themes to get you started:

More themes, but none of which are free:


sudo su - ghost
cd /var/www/ghost/content/themes
git clone
sudo systemctl restart ghost.service

Then browse to --> Design --> scroll down to "massively" --> activate

[23] Troubleshooting

Log in as your normal linux user (not root, not ghost)...

sudo systemctl status ghost.service
sudo journalctl -u ghost.service -f
sudo -u ghost tail -f /var/www/ghost/content/logs/https___blog_example_com.production.log
sudo -u ghost tail -f /var/www/ghost/content/logs/https___blog_example_com.production.error.log

sudo systemctl status nginx.service
sudo journalctl -u nginx.service -f
sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/nginx/access.log

Configured email support

Your blog needs to be able to email people. Forgot your password? Invites to admins? Etc.

[24] Install sSMTP

sudo dnf install ssmtp mailx -y

mailx is only used to test things more directly. It's not critical for the application. For more discussion on this topic, fee free to visit my related host: (Method 2 is the most apropos).

[25] Set up an email account with your domain provider

Something like and set a password. I use Lastpass to generate passwords.

Find our what the name of the smtp hostname is for your domain provider. For example, with, it's

They should also list the TLS requirements. Probably TLS and port 587.

[26] Configure sSMTP

Tell the OS which MTA you are going to be using...

sudo alternatives --config mta

It will look something like this, select sSMTP with the right number and exit...

There are 3 programs which provide 'mta'.

  Selection    Command
   1           /usr/bin/esmtp-wrapper
*+ 2           /usr/sbin/sendmail.postfix
   3           /usr/sbin/sendmail.ssmtp

Enter to keep the current selection[+], or type selection number: 3

Configure sSMTP

For this example, I am using (my domain provider -- each is different), therefore...

  • Example SMTP Server:
  • Example username:
  • Example (app) password: kjsadkjbfsfasdfqwfq

Note, you could use if you wanted to. Up to you and this assumes you have that set up for email. I do not.

Configuring sSMTP is pretty easy, and a bit more obvious than ther MTA, like for example, Postfix.

  • Backup the original configuration file:
    sudo cp -a /etc/ssmtp/ssmtp.conf /etc/ssmtp/ssmtp.conf-orig
  • Edit ssmtp configuration file:
    sudo nano /etc/ssmtp/ssmtp.conf

Change these settings, or add if they are missing...

Debug=YES                                    # For now, stick that at the very top of the config file                   # SMTP server hostname and port                    # The host the mail appears to be coming from
FromLineOverride=YES                         # Allow forcing the From: line at the commandline
UseSTARTTLS=YES                              # Secure connection (SSL/TLS) - don't use UseTLS
TLS_CA_File=/etc/pki/tls/certs/ca-bundle.crt # The TLS cert                 # The mail account at my domain provider
AuthPass=kjsadkjbfsfasdfqwfq                 # The password for the mail account

# Keeping in these notes for reference                    # Who gets all mail to userid < 1000             # SMTP server hostname and port                  # SMTP server hostname and port
#Hostname=localhost                          # The name of this host

Test that it works "in the raw"...

This is only needed for testing...
The way this works is that for every user on the system, you need to map
username --> email that will work for the --> smtp server

Edit the ssmtp alias mapping: sudo nano /etc/ssmtp/revaliases


Monitor it...

# Either one of these methods should work for you
sudo tail -f /var/log/maillog
#sudo journalctl -f | grep -i ssmtp

Send a test email...

Do this as root or ghost user...

sudo su - ghost
echo "This is the body of the email. Test. Test. Test." | mail -s "Direct email test 01" -r

[27] Edit config.production.json

sudo -u ghost vim /var/www/ghost/core/server/config/env/config.production.json

Change the "mail" stanza to look something like this:

  "mail": {
    "from": "'Example Blog' <>",
    "transport": "sendmail",
    "options": {
        "port": 587,
        "host": "",
        "secureConnection": false,
        "auth": {
          "user": "",
          "password": "kjsadkjbfsfasdfqwfq"

Note (again), once you have Ghost up and running, create a redundant backup of this file. Future updates tend to overwrite it: cp -a config.production.json config.production.json--backup

[28] Restart Ghost and Test

sudo systemctl restart ghost.service
sudo systemctl status ghost.service

Navigate to the admin screens, click Staff, and invite someone (I invited myself for testing purposes). It should send them an email.

Copy that production config to backup now.

sudo cp -a /var/www/ghost/core/server/config/env/config.production.json /var/www/ghost/core/server/config/env/config.production.json--BACKUP

[29] Email subscriptions

You can use Ghost's built in email subscriptions + Zapier + Mailchimp, but I like it simple, so I just use MailChimp directly, for that...

Direct Mailchimp integration

  • Create a MailChimp account.
  • Create two email campaigns: Click into "Campaigns" (on the MailChimp website) and
    1. Create a "Single welcome email" for new subscribers
    2. Create a "Weekly blog updates" that sends an RSS feed summary. See also these instructions.
  • Embed an subscribe-by-email widget to the bottom of every entry page by...
    • Click into the "Audience" tab on the MailChimp website.
    • Using the "Manage Audience" dropdown menu, choose "Signup Forms"
    • Click "Embedded Forms"
    • Change the form title to something like "Subscribe to the Blog!"
    • Click "Condensed"
    • Copy the embedded code that mailchimp provides you.
    • SSH into your blog server and edit the /var/www/ghost/content/themes/casper/post.hbs file (your chosen theme should have an equivalent .hbs file).
    • Uncomment and edit the section that starts with <section class="post-full-comments"> and add that embedded code that MailChimp provided you.
  • Restart Ghost: sudo systemctl restart ghost.service

You should now see an email subscription field at the bottom of each of your blog entries.

[30] Disqus commenting functionality and other integrations

I leave this to the reader to figure out. I don't currently enable commenting on my blog.

Back everything up

You did all this work! You gotta back everything up now. :)

Log in as your normal working user. Not root. Not ghost. Create a back script and copy to a safe place...

Edit vim add this and save it...


# This script will
# - shut down the ghost and nginx systemd services
# - create an RPM manifest for reference
# - back up ghost and the configurations for nginx, ssmtp, letsencrypt/certbot
#   (this does not back up your OS configuration)
# - restarts the ghost and nginx systemd services

echo "Shutting down ghost and nginx services..."
# Shut down ghost and nginx
sudo systemctl stop ghost.service
sudo systemctl stop nginx.service
echo "...done."

# Back everything up
DATE_YMD=$(date +%Y%m%d)
echo "Grabbing the RPM manifest of this server..."
rpm -qa | sort > $HOSTNAME-rpm-manifest-${DATE_YMD}.txt
echo "...done."

echo "Creating README file for backup..."
echo "\
Critical configuration files (and directories) are...

For Ghost itself:

For the Ghost systemd service:

For mail handling:

For the webserver:
...but especially...

For the TLS (SSL) certificates:
...but especially...
" > $HOSTNAME-ghost-on-fedora-${DATE_YMD}
echo "...done."

echo "Backing up Ghost, Nginx, and associated configuration..."
sudo tar -czf ./$HOSTNAME-ghost-on-fedora-${DATE_YMD}.tar.gz \
  /var/www/ghost /etc/nginx /etc/ssmtp/revaliases \
  /etc/systemd/system/ghost.service /etc/ssmtp/ssmtp.conf \
  /etc/letsencrypt /etc/sysconfig/certbot \
  $HOSTNAME-rpm-manifest-${DATE_YMD}.txt \
rm $HOSTNAME-rpm-manifest-${DATE_YMD}.txt $HOSTNAME-ghost-on-fedora-${DATE_YMD}
echo "...done."

echo "Starting up ghost and nginx services..."
# Start ghost and nginx
sudo systemctl start ghost.service
sudo systemctl start nginx.service
echo "...done."

echo "Here is your backup tarball, copy it somewhere safe:"
ls -lh ./$HOSTNAME-ghost-on-fedora-${DATE_YMD}.tar.gz

Save that script, then run it...

. ./

Congratulations! YOU'RE DONE!

Or you at least done with the initial setup. You now have an end-to-end functioning Ghost blogging platform installed.

Any questions or commentary, you can find me at

Addendum: Updates and Upgrades

Updating the operating system

Should just work: sudo dnf upgrade -y --refresh; sudo reboot

Upgrading Ghost

The process is relatively simple

  1. Backup -- See "Back Everything Up" above
  2. Download the new tarball -- See "Download Ghost" above
  3. Make a convenience backup of your Ghost configuration file: sudo cp -a /var/www/ghost/core/server/config/env/config.production.json /tmp/
  4. Shut down services:
sudo systemctl stop ghost.service
sudo systemctl stop nginx.service
  1. Install Ghost -- See "Unzip/Refresh Ghost application", "Navigate to /var/www/ghost", and "Install Ghost" above
  2. Replace config file with backup: sudo mv /tmp/config.production.json /var/www/ghost/core/server/config/env/
  3. Restart services:
sudo systemctl start ghost.service
sudo systemctl start nginx.service
  1. Browse to your domain and your domain/ghost and check that everything works correctly.


Getting help

Other resources and inspirations



Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Copyright © Todd Warner

You can’t perform that action at this time.