Ghost CMS on Hetzner Cloud

styxlab

Joost Jansky

Posted on June 22, 2020

Ghost CMS on Hetzner Cloud

Learn howto install and run a headless Ghost content management system in the cloud. Deploy Ghost in a Docker container and make it accessible to your teams. This guide focuses on the specifics of a headless configuration and fully prepares you for the awesome Jamstack.

Ghost CMS on Hetzner Cloud

Ghost started as a blogging platform but now officially announces itself a powerful content management system (CMS) with support for headless mode. Headless means that you can run it as a pure content source, replacing it's build-in front-end with a Jamstack variant.

As Jamify sources in all content from a Ghost CMS, this guide focuses on setting it up correctly for the Jamstack and particularly for use with the Jamify tools. Jamify currently requires you to run your CMS on a public endpoint, because inline images are served directly from the CMS. Hosting your CMS on a public cloud also enables you to invite team members, so it's generally good for collaboration.

Choosing a cloud host

Public accessibility means that you need to pick a hosting service. The installations steps that follow can be easily adapted to other cloud providers, such as Digital Ocean, Scaleway or Vultr, just to name three alternatives.

For this guide, I choose Hetzner Cloud, because it as an extremely reliable and cost-efficient service. Although not that widely known, it's a very professional company with a fast network, featuring DDOS protection and GDPR compliancy.

For under $3/month you can host your Ghost CMS with them which is extremely competitive for a hight quality service. However, you do have to install, maintain and secure your server yourself.

Hetzner Cloud

If you want to closely follow the installation steps shown in this guide, start by registering a new account with Hetzner. Just Sign up and you are good to go. Unfortunately, you cannot register with your existing Github account, they are still a bit old school in that respect.

Set up a new project

After setting up an account with Hetzner Cloud, go to the Cloud Console, add a new project and give it the name ghost. To simplify access to the cloud, make sure to configure your ssh key and generate an API token:

Ghost CMS on Hetzner Cloud

Ghost CMS on Hetzner Cloud

Make sure to copy the API token in the clipboard, so you can paste it later. For security reasons, you cannot see it a second time.

Install and connect hcloud

Initially you may find it easier to configure your cloud servers on their web interface, but you will soon discover the benefits of the CLI tool hcloud. It will make automating processes a breeze, so installing it will save you a lot of time later. Just follow their install instructions on Github. After installation, check the version:

[local]$ hcloud version
hcloud v1.16.2
Enter fullscreen mode Exit fullscreen mode

You can now create a context in hcloud that lets you access your ghost project.

[local]$ hcloud context create ghost
Token:
Enter fullscreen mode Exit fullscreen mode

Paste you previously generated API token at the token prompt, and you'll see the confirmation message Context ghost created and activated. hcloud is now connected to your ghost project.

Create a cloud server

With the command hcloud server-type list you get a list of currently supported servers. The smallest option is totally sufficient for a Ghost CMS, so you can create a new server with the following command:

Don't forget to replace the ssh-key name with your own!

[local]$ hcloud server create --image fedora-32 --location fsn1 --type cx11 --name ghost --ssh-key your@key
Enter fullscreen mode Exit fullscreen mode

When you follow the commands in this guide you should also use the Fedora OS, because installation commands can vary slightly between different Linux distributions. Check the status of your server:

[local]$ hcloud server list
ID NAME STATUS IPV4 IPV6 DATACENTER
5838079 ghost running 49.12.109.72 2a01:4f8:c17:a6dc::/64 fsn1-dc14
Enter fullscreen mode Exit fullscreen mode

Connect a domain name

Now that you have a cloud server with a dedicated IP, you can connect your domain name with it. You can purchase a new one with Hetzner or you can use an existing one that you registered with another provider. In any case, you need to configure your DNS zone file:

Ghost CMS on Hetzner Cloud

Configure at least three A records for @, www and cms to point at your server IP. As you can see from the figure above, I am using the domain blazing-blogs.com.

It can take up to 24 hours until these settings have propagated to all DNS servers.

Protect your server

As a very first step, you must further harden the server. As the only authentication method to your server is a key based SSH method, it should not be possible for others to log into your server. Still, you should close down all ports that you don't need, rate-limit your SSH port and update all software packages to the latest version.

Firewall

Log into your server with ssh root@$(hcloud server ip ghost) and install the uncomplicated firewall ufw:

[remote]$ dnf -y install ufw
Enter fullscreen mode Exit fullscreen mode

Create a file containing the firewall commands:

[remote]$ (cat << EOF
#!/bin/sh
ufw default deny incoming
ufw limit in 22/tcp comment "rate-limit SSH"
ufw default allow outgoing
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
EOF
) > ./firewall.sh
Enter fullscreen mode Exit fullscreen mode

Execute and enable it with:

[remote]$ sh ./firewall.sh
[remote]$ systemctl enable --now ufw.service
Enter fullscreen mode Exit fullscreen mode

You can check the firewall rules at any time with:

[remote]$ ufw status
Enter fullscreen mode Exit fullscreen mode

Important: To verify that you have not locked out yourself, open a new terminal window and check that you can login in with ssh root@$(hcloud server ip ghost).

Update all packages

Second most important step is to bring the kernel and other software packages up-to-date:

[remote]$ dnf -y update
Enter fullscreen mode Exit fullscreen mode

These updates are only fully applied after a reboot.

Kernel tweak for docker

In perfect foresight, we are going to make a kernel tweak here, that is needed later for running docker. You need to enable the backward compatibility for cgroups.

[remote]$ grubby --update-kernel=ALL \
    --args="systemd.unified_cgroup_hierarchy=0"
Enter fullscreen mode Exit fullscreen mode

Reboot

For all these changes to take affect, a reboot is required:

[remote]$ reboot
Enter fullscreen mode Exit fullscreen mode

Logon again

You have to wait a minute until the OS has booted up again. Then login as usual:

[local]$ ssh root@$(hcloud server ip ghost)

Enter fullscreen mode Exit fullscreen mode

and check that the firewall is up and running

 [remote]$ ufw status
Enter fullscreen mode Exit fullscreen mode

and the kernel updates have been applied:

 [remote]$ uname -a
 Linux ghost 5.6.11-300.fc32.x86_64 #1 SMP... 
Enter fullscreen mode Exit fullscreen mode

Install Docker

Use Fedora's package manager to install Docker:

[remote]$ dnf -y install docker docker-compose
[remote]$ systemctl enable --now docker
Enter fullscreen mode Exit fullscreen mode

Fedora now uses the Moby Project to assemble the Docker components. Check that docker is running with an up-to-date version:

[remote]$ docker version
Client:
 Version: 19.03.8
Server:
 Engine:
  Version: 19.03.8

Enter fullscreen mode Exit fullscreen mode

Obtain certificates from LetsEncrypt

The connection to your CMS should be SSL encrypted, so network traffic is secured. This is an important step as you do not want to send your passwords in plain-text over the internet.

[remote]$ dnf -y install certbot 
Enter fullscreen mode Exit fullscreen mode

Substitute blazing-blogs.com with your domain, your@mail.com with your email and put it in a variable:

[remote]$ DOMAIN=blazing-blogs.com
[remote]$ EMAIL=your@mail.com

Enter fullscreen mode Exit fullscreen mode

Use certbot to get a certificate:

[remote]$ certbot certonly --standalone --no-eff-email \
--agree-tos --rsa-key-size 4096 --email ${EMAIL} \
--domains ${DOMAIN},www.${DOMAIN},cms.${DOMAIN}
Enter fullscreen mode Exit fullscreen mode

If this command exits on error, check that DNS servers have already picked up the changed IP. You should see your cloud server IP with [local]$ ping <your-domain.tld> , otherwise you have to wait up to 24 hours until you can complete this step.

Install Nginx

Nginx is a reverse-proxy and load-balancer that routes incoming traffic to your internal endpoints. It needs to be installed and configured with the previously obtained certificates:

[remote]$ dnf -y install nginx
Enter fullscreen mode Exit fullscreen mode

The nginx configuration specifies how nginx relays traffic from your public endpoint to your internal Ghost installation. It also handles SSL encryption for you.

Statically serving images

With a default install of Ghost, Nginx will proxy all requests to Node.js. That means Nginx is proxying a request that it could handle itself much faster. You may wonder why you should be concerned about proxy speeds on your CMS. There are two valid reasons why you want to improve image load performance:

  • Reduce initial build times: Although all resources are cached on Jamify, initial build times can be reduced when images are served faster.
  • Accelerate live sites: Feature images are added to the static assets in Jamify, but inline images are currently still served from your Ghost CMS.
[remote]$ (cat << EOF
server {

  listen 80;
  listen [::]:80;
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  server_name cms.${DOMAIN};

  ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

  location / {    
    proxy_set_header Host \$http_host;
    proxy_set_header X-Real-IP \$remote_addr;
    proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto \$scheme;
    proxy_pass http://127.0.0.1:2368;
  }

  location ^~ /content/images/(!size) {
    root /root;
  }

  location ^~ /.well-known/acme-challenge/ {
    default_type "text/plain";
    root /var/www/letsencrypt;
  }

  location = /.well-known/acme-challenge/ {
    return 404;
  }

}
EOF
) > /etc/nginx/conf.d/cms-ghost.conf
Enter fullscreen mode Exit fullscreen mode

The first location block above describes the proxy to Ghost and is followed by the discussed bypass to serve images statically.

The last two location blocks are for LetsEncrypt, so certbot can renew your certificates while your web server is up and running. Finally, start nginx with:

[remote]$ systemctl enable --now nginx
Enter fullscreen mode Exit fullscreen mode

and check it is running correctly:

[remote]$ systemctl status nginx
ā— nginx.service - The nginx HTTP and reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; vendor preset: disabled)
     Active: active (running) since Fri 2020-05-15 16:47:30 CEST; 6s ago

Enter fullscreen mode Exit fullscreen mode

Ghost CMS in a Container

You are now fully prepared to install headless Ghost CMS. With all the prerequisites out of the way this is the easy part. A couple of environment variables must be defined that allow Ghost to send emails such as user invites or lost password requests:

[remote]$ EMAIL_FROM=noreply@your-blog.com
[remote]$ SMTP_HOST=mail.server.com
[remote]$ SMTP_PORT=587
[remote]$ SMTP_USER=user@server.com
[remote]$ SMTP_PASS=strong password
Enter fullscreen mode Exit fullscreen mode

Replace the values with your own mail provider and use the following command to make a docker-compose.yml definition file:

[remote]$ (cat << EOF
version: '3.3'

services:
  ghost:
    image: ghost:alpine
    restart: always
    ports:
      - 2368:2368
    volumes:
      - ./content:/var/lib/ghost/content
    environment:
      # see https://docs.ghost.org/docs/config#section-running-ghost-with-config-env-variables
      url: https://cms.${DOMAIN}
      mail__transport: SMTP
      mail__from: ${EMAIL_FROM}
      mail __options__ host: ${SMTP_HOST}
      mail __options__ port: ${SMTP_PORT}
      mail __options__ auth__user: ${SMTP_USER}
      mail __options__ auth__pass: ${SMTP_PASS}
EOF
) > ./docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

This file configures the url for ghost and exposes the service on the standard port 2368. The service is configured to be persistent, so all configuration and database files are saved in your local directory under contents. This directory is created on-the-fly when you start the container.

Register your admin account

Fire up Ghost with docker. In this foreground mode, you see a lot of info messages and also warnings and errors, if they occur.

[remote]$ docker-compose up
Enter fullscreen mode Exit fullscreen mode

Open your CMS in a web browser, which is https://cms.blazing-blogs.com in this example. You should see the Ghost sign up page. Complete the registration process until you see the Ghost Admin panel.

Ghost CMS on Hetzner Cloud

Ghost CMS on Hetzner Cloud

Ghost CMS on Hetzner Cloud

If everything goes fine, stop your running container with Ctrl+C and re-start it in detached mode, so it runs in the background.

[remote]$ docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

Now, your docker container will be running also after a reboot.

Configure Headless Mode

For headless mode, it's crucial to switch on the private flag. It makes sure that the Ghost CMS does not interfere with your Jamify front-end. I did not manage to change this setting on the command line, but it's not a big deal to do it in the admin panel:

Ghost CMS on Hetzner Cloud

This enables password protection in front of the Ghost install and sets <meta name="robots" content="noindex" /> so your Jamify front-end becomes the authoritative source for search engines.

Enable Members

If you plan to integrate a newsletter subscription form into your site or would like to use Ghost Members features in the future, it's now a good time to enable that feature in Ghost Admin.

Ghost CMS on Hetzner Cloud

Just activate the Enable members switch and leave all other settings with their defaults. You can come back to those later.

When your users subscribe to your site, Ghost CMS will send a magic subscription link to the provided email address. For that to work, email must be correctly configured. If you correctly set up your EMAIL and SMTP variables as discussed above, this should be already working. Test it by pressing the Send button under Labs -> Test email configuration:

Ghost CMS on Hetzner Cloud

In order for you users to be able to see sign up messages on the Jamify site, make the following tweak in your nginx configuration:

// /etc/nginx/conf.d/cms-ghost.conf

server {

  ...

  server_name cms.${DOMAIN};

  ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

  if ($args ~* "^action=subscribe&success=") {
    return 301 $scheme://www.${DOMAIN}$request_uri;
  }

  ...

}
Enter fullscreen mode Exit fullscreen mode

Jut put the shown if statement below the certificate block. This statement instructs nginx to redirect subscription messages to your Jamify front-end. I made the assumption, that your Jamify site runs under www.${DOMAIN}. Please change it to the public endpoint of your production site.

Install Automation

In order to understand the installation steps, a manual install is preferable. Ultimately, you want to fully automate the process. I have great news for you: you can use a fully working automation script that I published on Github:

$ git clone https://github.com/styxlab/ghost-on-hetzner-cloud.git
$ cd ghost-on-hetzner-cloud
Enter fullscreen mode Exit fullscreen mode

The scripts also includes additional features, some of which are discussed further below.

  • Floating IPs
  • SSH on non-standard port
  • Scheduled backup
  • Certificate renewal
  • System updates

Hetzner Cloud doesn't let you attach environment variable in their Cloud Console, therefore you have to provide them trough your own scripts. Create a .env file that contains the following environment variables:

# .env file

# Server
CLOUD_SERVER_NAME=ghost
CLOUD_SERVER_IMAGE=fedora-32
CLOUD_SERVER_TYPE=cx11
CLOUD_SERVER_LOCATION=fsn1
CLOUD_SSH_KEY=you@home

# LetsEncrypt + Nginx
DOMAIN=your-blog.com
EMAIL=you@your-blog.com

# Ghost email settings
EMAIL_FROM=noreply@your-blog.com
SMTP_HOST=mail.server.com
SMTP_PORT=587
SMTP_USER=user@server.com
SMTP_PASS=SMTP_password
Enter fullscreen mode Exit fullscreen mode

Please take some time to review this file, because your install script will fail with wrong variables. You have to substitute at least all values starting from CLOUD_SSH_KEY until SMTP_PASS with your own ones.

Start the automated install with:

[ghost-on-hetzner-cloud]$ sh create.sh
Enter fullscreen mode Exit fullscreen mode

The script introduces a break when it asks you to update/verify your DNS zone file. You can comment out these lines, once you know that DNS is working.

The full install takes approximately 10 minutes to complete. After that you have a fresh Ghost installation and you can continue with the above steps for registering your admin account and configuring headless mode.

Snapshots

If you want to set up more installations or just want to make a backup of a fresh install, you can make a snapshot:

hcloud server create-image --type snapshot ghost
Enter fullscreen mode Exit fullscreen mode

You can always create a new server from your snapshot image:

hcloud server create --image 16614774 --location fsn1 --type cx11 --name ghost --ssh-key your@key
Enter fullscreen mode Exit fullscreen mode

Note that snapshots occupy cloud space and therefore incur costs.

Danger zone - tear down

You can easily remove your server again with hcloud. This is especially great during the testing phase, because a removed server doesn't incur any costs.

Caution! Make sure you have backed up all your data before doing this. You cannot recover this step - everything on your server will be lost.

[local]$ hcloud server delete ghost
Server 5851202 deleted
Enter fullscreen mode Exit fullscreen mode

Operation

If you are running your own cloud server, you need to be comfortable with maintaining it. You can automate a lot, but checking it from time to time is necessary.

Scheduled Backups

Taking snapshots as shown before, is a quick way to make backups. In the long run, you want a backup strategy and implement scheduled backups.

All you need to backup from Ghost is the content directory that we persisted earlier during the docker install. This directory contains the database file and the image assets. You can use a systemd timer to set up the schedule, compress the directory and copy it over to another location.

If you are using the automated install, this is already set up for you. Otherwise follow the manual steps below.

Start by creating a new directory for your backup storage:

$ mkdir -p /root/backup/weekly
Enter fullscreen mode Exit fullscreen mode

and subsequently create the following systemd unit files backup-weekly.service and backup-weekly.timer:

[remote]$ (cat << EOF
[Unit]
Description=Backup Ghost

[Service]
Type=oneshot
ExecStart=/usr/bin/sh -c 'rsync -avr --delete-after /root/backup /backup/weekly'
EOF
) > /usr/lib/systemd/system/backup-weekly.service

[remote]$ (cat << EOF
[Unit]
Description=Run backup-weekly.service

[Timer]
OnCalendar=Mon *-*-* 02:02:02

[Install]
WantedBy=multi-user.target
EOF
) > /usr/lib/systemd/system/backup-weekly.timer
Enter fullscreen mode Exit fullscreen mode

In this example your backup is scheduled to run every Monday at 2 in the morning. Enable your timer with:

[remote]$ systemctl enable --now backup-weekly.timer
Enter fullscreen mode Exit fullscreen mode

You can view all active timers with:

[remote]$ systemctl list-timers
Enter fullscreen mode Exit fullscreen mode

In this example, the data is stored on the same disk. You can customize the systemd service and use rsync to copy all files in a remote location, for example in a private AWS S3 bucket. Another option is to use Hetzner's backup service that you can enable in the Cloud Console.

Certificate Renewal

The previously installed certbot already ships with a systemd service and timer similar to the ones that we previously discussed for backups. If you are using the automated install with the create.sh script, everything should already been set up for you. This is the manual install instruction:

[remote]$ systemctl enable --now certbot-renew.timer
Enter fullscreen mode Exit fullscreen mode

Always check that the scheduler is correctly installed:

[remote]$ systemctl list-timers
Enter fullscreen mode Exit fullscreen mode

Ghost updates

If you use the standard Ghost install based on the ghost-cli, updating to a new version is always risky: strangely enough the official update process does not provide a method to revert to a previous version.

Luckily, we are not affected by this limitation, as we are using the Docker approach. With Docker, updating Ghost is just a matter of downloading a new image and you can always revert back to the old image, if the update fails.

It is still important to make a backup of the content directory before updating, because the new version might make irreversible changes to your database.

In order to update, just update the docker-compose.yml file with the new image version. Here, I added 3.16.0- to the image signature. You can always check the latest version on the Docker hub.

version: '3.3'

services:
  ghost:
    image: ghost:3.16.0-alpine
    restart: always
    ports:
      - 2368:2368

Enter fullscreen mode Exit fullscreen mode

Once you have updated docker-compose.yml you need to restart with:

[remote]$ docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

My previous image was based on version 3.15.3, so I will see the new image pulled and the container restarted shortly after. You should then see a new entry in your image list:

[remote]$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ghost 3.16.0-alpine 2e6185cc8040 16 hours ago 351MB
ghost alpine fcc77b9d5f98 7 days ago 336MB

Enter fullscreen mode Exit fullscreen mode

This process could be automated and accompanied with a revert script. If you like to do that and contribute to this project, just go ahead an let us know.

System Updates

You should make every effort to keep your system up-to-date. With new security vulnerabilities discovered every day, frequent updates are a necessity. The following systemd units download updates every day:

[remote]$ (cat << EOF
[Unit]
Description=System update

[Service]
Type=oneshot
ExecStart=/usr/bin/sh -c 'dnf -y update'
EOF
) > /usr/lib/systemd/system/system-update.service

[remote]$ (cat << EOF
[Unit]
Description=Run daily system update

[Timer]
OnCalendar=*-*-* 03:03:03

[Install]
WantedBy=multi-user.target
EOF
) > /usr/lib/systemd/system/system-update.timer
Enter fullscreen mode Exit fullscreen mode

and performs a reboot to apply the kernel updates every week:

[remote]$ (cat << EOF
[Unit]
Description=System Reboot

[Service]
Type=oneshot
ExecStart=/usr/bin/sh -c 'reboot'
EOF
) > /usr/lib/systemd/system/system-reboot.service

[remote]$ (cat << EOF
[Unit]
Description=Weekly system reboot

[Timer]
OnCalendar=Tue *-*-* 04:04:04

[Install]
WantedBy=multi-user.target
EOF
) > /usr/lib/systemd/system/system-reboot.timer
Enter fullscreen mode Exit fullscreen mode

You can change these scripts to your own needs. They are already included and enabled if you use the automation scripts provided on ghost-on-hetzner-cloud.

Summary

This tutorial contains a lot of information. Congrats for following it through! Running your own Ghost instance is no rocket science: you can easily do it yourself. With this guide you get the knowledge and the tools to operate Ghost it in a predominantly unsupervised way.

While you learned a lot about the operational aspects, we also focused on a headless Ghost configuration: by setting the private flag correctly, by serving assets statically and by tweaking the members functionality.

You are now fully prepared for the next step: Sourcing your content from a headless Ghost into your Jamify front-end. Start publishing flaring fast websites today!

Do you want early access to Blogody, the brand new blogging platform that I am creating? Just sign-up on the new Blogody landing page and be among the first to get notified!


This post was originally published at jamify.org on April 7, 2020.

šŸ’– šŸ’Ŗ šŸ™… šŸš©
styxlab
Joost Jansky

Posted on June 22, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Newsletter marketing with Jamify
webdev Newsletter marketing with Jamify

October 1, 2020

Incremental Builds with Jamify
webdev Incremental Builds with Jamify

September 18, 2020

Routing with Jamify
webdev Routing with Jamify

July 12, 2020

Add a contact page to Jamify
webdev Add a contact page to Jamify

July 5, 2020

Getting started with Jamify
webdev Getting started with Jamify

June 28, 2020