When Kira and I first decided to create FoxyPossibilities.com, I wanted to find a solution to launch the site as simply as possible and require little maintenance on our part. There are a lot of decisions that go into launching a site, and it’s easy to get stuck trying to make the perfect site before it even launches. I want to share with you the decisions we made and the steps I took to get our site up and running.

Deciding to use Ghost

My first instinct was to try to use a static site generator and I started to implement the website with Hugo. However, Hugo’s flexibility made it a challenge for us to design the site. Hugo tries to be flexible to support all sorts of different workflows, but we didn’t have an existing workflow, so there were a lot of open decisions on how to organize our content before we even had content. Designing the menu was another problem, and we had originally designed it to be nested two levels with categories and subcategories. Finally, I couldn’t find any themes that supported more than one author.

Ghost is focused for publishing sites like blogs, and provides a lot more structure and conventions for content. For example, Ghost’s site navigation forced us to scrap our plans to have a two tiered menu, but we both agree that the simpler menu works better for the site. Ghost also supports having multiple authors, and many themes were built with multiple authors in mind.

Since Ghost is a dynamic site rather than a static site, there is more complexity involved when it comes to running the site. However, Ghost makes our design, theme, and content organization decisions simpler, so we decided it was worth it to setup Ghost.

Choosing a hosting provider

Running a public site from home isn’t as viable as it used to be. The cost to have a static IP alone is more expensive than it is to run a small virtual private server (VPS) through a provider. Unless you have fiber running to your home, hosting providers also give your server access to better network speeds and quality than your ISP. While I’d like to see a more decentralized web, using a hosting provider makes a lot of sense in this case.

For this website I decided to use DigitalOcean primarily to simplify backups. For a little extra per month they will perform automated snapshots of your server, which is something you’d have to setup yourself on either Google Cloud Platform or Amazon Web Services. Doing snapshots of the whole server might not be the most elegant backup solution, but it’s simple, and doing more targeted backups correctly is difficult. In fact, GitLab recently had a meltdown because their backups weren’t useful when they needed to use them for recovery.

Running Ghost on CoreOS

CoreOS minimizes the need for maintenance by providing a minimal, self-updating, operating system designed to run containers. Many of the tutorials you’ll find for CoreOS are about how to run a cluster of servers, but I run one server to keep things easy to configure.

I use the Caddy web server to serve the site over HTTPS and handle automatic certificate renewal with Let’s Encrypt.

The Docker images and the tags that I use (at the time of writing) are:

Configuring Caddy

To configure Caddy on CoreOS, I first created a folder to hold the configuration and persist the certificate information. On the server I ran:

1
$ sudo mkdir /opt/caddy

Then I created the file /opt/caddy/Caddyfile with the following contents:

1
2
3
4
5
6
7
8
9
https://foxypossibilities.com, http://foxypossibilities.com, http://www.foxypossibilities.com {
  redir https://www.foxypossibilities.com{uri}
}

https://www.foxypossibilities.com {
  proxy / ghost:2368 {
    transparent
  }
}

This configuration file redirects all traffic from HTTP to HTTPS and anyone pointing their browser at foxypossibilities.com to www.foxypossibilities.com, and proxies all requests to the service with hostname ghost running on port 2368. Of course, if you’re wanting to follow this example to configure your own site, you’ll want to change these settings to match the hostname of your own site.

You can take a look at the Caddy User Guide if you’d like to learn more about configuration options for the Caddyfile.

The default Docker network doesn’t allow containers to discover each other unless you explicitly run them with the --link option, which is a feature that is being deprecated. Using links can also cause issues when one container tries to run with a link to a container that hasn’t started yet.

The more reliable way to have Caddy be able to reach the Ghost container is to create a custom network. I’ve created mine like so:

1
$ docker network create ghost_net

Docker makes containers discoverable by name within the same network when you create a custom network with the default settings. It works by giving DNS names to containers that match the names you’ve assigned to the containers. Later, we’ll assign our Ghost Docker container with the name ghost so the Caddy server will discover it via DNS and proxy requests to it.

I use systemd to manage my Docker containers. For Caddy, I created the systemd unit file named /etc/systemd/system/caddy.service, and configured it like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[Unit]
Description=Caddy HTTP/2 web server
Documentation=https://caddyserver.com/docs
After=docker.service
Requires=docker.service

[Service]
Environment=CADDY_VER=0.9.5
TimeoutStartSec=0
Restart=on-failure
ExecStartPre=-/usr/bin/docker kill caddy
ExecStartPre=-/usr/bin/docker rm caddy
ExecStartPre=/usr/bin/docker pull abiosoft/caddy:${CADDY_VER}
ExecStart=/usr/bin/docker run --name caddy \
    --network=ghost_net \
    -p 80:80/tcp \
    -p 443:443/tcp \
    -v /opt/caddy/Caddyfile:/etc/Caddyfile:ro \
    -v /opt/caddy:/root/.caddy \
    abiosoft/caddy:${CADDY_VER}
ExecStop=/usr/bin/docker stop caddy

[Install]
WantedBy=multi-user.target

Note that the caddy container gets removed and rebuilt each time. I’m of the belief that Docker containers should be disposable and anything that needs to be persisted should be mounted in a volume. On the server, everything that needs to be persisted for Caddy is in /opt/caddy.

Caddy can now be started, stopped, and restarted with:

1
2
3
$ sudo systemctl start caddy
$ sudo systemctl stop caddy
$ sudo systemctl restart caddy

If everything goes well, Caddy will respond when a browser is pointed to it. Caddy will likely respond with a 502 Bad Gateway error, but that is expected because we haven’t setup Ghost yet.

If all does not go well, you can check the status of Caddy with:

1
$ sudo systemctl status caddy

This will display the status of the process and display some log information that will help debug the problem.

If the systemd unit file needs to be updated, run the following before trying to restart the service:

1
$ sudo systemctl daemon-reload

If you forget, systemd will remind you that you need to do a daemon-reload before restarting the service.

Configuring Ghost

To configure Ghost to run on CoreOS, I first created a folder to hold the configuration, custom theme, and content for the site. On the server I ran:

1
$ sudo mkdir /opt/ghost

Ghost is a Node.js application and its configuration is a JavaScript file. I wanted to get the default configuration from the Docker container and modify it rather than starting from scratch. So, on the server I ran the following:

1
2
$ docker pull ghost:0.11
$ docker run -ti --name ghost -v /opt/ghost:/var/lib/ghost ghost:0.11

After the Ghost server initialized, I pressed Ctrl+C to stop the server. Then I edited the configuration file found at /opt/ghost/config.js and filled in the production settings with the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
config = {
    // ### Production
    // When running Ghost in the wild, use the production environment.
    // Configure your URL and mail settings here
    production: {
        url: 'https://www.foxypossibilities.com',
        mail: {
            transport: 'SMTP',
            options: {
                service: 'YOUR_EMAIL_SERVICE',
                auth: {
                   user: 'YOUR_EMAIL_USER',
                   pass: 'YOUR_EMAIL_PASS'
                }
            }
        },
        database: {
            client: 'sqlite3',
            connection: {
                filename: path.join(process.env.GHOST_CONTENT, '/data/ghost.db')
            },
            debug: false
        },

        server: {
            host: '0.0.0.0',
            port: '2368'
        },

        paths: {
            contentPath: path.join(process.env.GHOST_CONTENT, '/')
        },

        privacy: {
            useUpdateCheck: true,
            useGoogleFonts: true,
            useGravatar: false,
            useRpcPing: true,
            useStructuredData: true,
        }
    },
    // ...

I took out our personal email settings, so you’ll want to replace YOUR_EMAIL_SERVICE, YOUR_EMAIL_USER, and YOUR_EMAIL_PASS with your own settings. Setting up an email service is optional, so you can skip this option; but I found it to be necessary to create a new user account for Kira, since temporary passwords are sent by email. There is detailed documentation on how to setup an email service with Ghost.

Another thing to note is that the configuration is setup to use SQLite as the database. It’s perfect for getting started since we don’t have to run an extra database service, and one less thing to maintain. If you end up running a bigger site or have a few authors using the site at the same time, you’ll probably want to look into setting up Ghost with a dedicated database.

To learn more about Ghost’s configuration options, check out the Ghost configuration documentation.

The next step is to setup Ghost to be managed by systemd. I created the systemd unit file named /etc/systemd/system/ghost.service, and configured it like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[Unit]
Description=Ghost Blogging Platform Service
After=docker.service
Requires=docker.service

[Service]
Environment=GHOST_VER=0.11
TimeoutStartSec=0
Restart=on-failure
ExecStartPre=-/usr/bin/docker kill ghost
ExecStartPre=-/usr/bin/docker rm ghost
ExecStartPre=/usr/bin/docker pull ghost:${GHOST_VER}
ExecStart=/usr/bin/docker run --name ghost \
    --network=ghost_net \
    -e NODE_ENV=production \
    -v /opt/ghost:/var/lib/ghost \
    ghost:${GHOST_VER}
ExecStop=/usr/bin/docker stop ghost

[Install]
WantedBy=multi-user.target

Note that an environment variable NODE_ENV=production is set when we run the Ghost container. This lets Ghost know we are running in the production environment and to use the production configuration.

We can now start the Ghost service:

1
$ sudo systemctl start ghost

It takes a while for Ghost to startup and you can check on the status with:

1
$ sudo systemctl status ghost

If everything goes well, you’ll have Ghost up and running on your server. Pointing your browser to your server’s URL will take you to a page where you can create your first user.