Host Multiple Ghost Instances with Docker

Sep 29, 2020

I use Ghost in my day job to host our corporate blog & internal knowledge base, I also use it for my personal website and a few publication projects.

Ghost is very powerful and versatile, it can be especially useful for writers, podcasters and video creators as a second source of income with built in Paid Membership and Newsletter features.

Ghost is Open Source and promotes independent publishing.

Unfortunately self hosting Ghost can be a pain & managed plans are expensive, especially when hosting multiple sites, using Docker & Compose can streamline and automate this process for seamless and maintenance-free updates. Hopefully, this detailed guide should help you get started!

Before you Begin

You will need a Server or VPS running Ubuntu or other Debian based distribution with the following installed:

  1. nginx
  2. docker
  3. docker-compose

I use Digital Ocean for my VPS, they have plans starting at $5 a month which would be more than capable for even a couple of Ghost instaces.

Create Docker Compose file

Create and Change Directory to the new folder that will contain your Docker image data.

mkdir ghostSite && cd ghostSite

Create a file named docker-compose.yml and open it in your text editor. Paste in the contents from the following snippet. Replace example.com with your domain, and use your folder name and location in place of /root/ghostSite.

# docker-compose.yml

version: '3.2'

services:

  ghost:
    # We are specifying the Ghost Image to run
    image: ghost:latest
    
    # We want the container to restart in case the host gets restarted
    restart: always
    
    # Map host port 3001 to container port 2368
    ports:
      - 3001:2368
      
    # Mount host directory to container directory
    volumes:
      - /root/ghostSite/data:/var/lib/ghost/content
      
    # Specify your Ghost blog URL and mail server settings
    environment:
      url: https://example.com
docker-compose.yml

Setup NGINX

I use the following NGINX configuration for my Ghost instances, remember to replace example.com with your domain, including in SSL and log section. If you're not using SSL while testing comment out that section or remove it.

# example.com redirect block
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

# example.com
server {
    listen 443;
    server_name example.com www.example.com;
    root /opt/ghost/system/nginx-root;

    access_log /var/log/nginx/example-com-access.log;
    error_log /var/log/nginx/example-com-error.log;

    ssl on;
    ssl_certificate         /etc/nginx/ssl/example-com.pem;
    ssl_certificate_key     /etc/nginx/ssl/example-com.key;
    ssl_session_timeout 5m;
    ssl_ciphers               'AES128+EECDH:AES128+EDH:!aNULL';
    ssl_protocols              TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;

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

    }

    location ~ /.well-known {
        allow all;
    }

    client_max_body_size 50m;
    }

Test Your Site

From your ghost directory in this example ghostSite run the following command, this will download the latest ghost image and run with your settings defined in docker-compose.yml.

docker-compose up

Access it from the domain you've configured, running docker-compose without the -d option will slow debug information when creating and running your new container.

If your ghost intance is created succesfully and loads in the browser you can start it with the -d option which will run Ghost in the background.

docker-compose up -d

Complete Ghost Setup

To complete the setup process, navigate to the Ghost configuration page by appending /ghost to the end of your blog’s URL or IP. This example uses https://example.com/ghost.

On the welcome screen, click Create your account:

Ghost Welcome Screen

Enter your email, create a user and password, and enter a blog title:

Create Your Account Screen

Invite additional members to your team. If you’d prefer to skip this step, click I’ll do this later, take me to my blog! at the bottom of the page:

Invite Your Team Screen

Navigate to the Ghost admin area to create your first post, change your site’s theme, or configure additional settings:

Ghost Admin Area

Automation and Updates

Automation

The bellow script is what I use to start, stop, restart & update my ghost instances. It's quick and dirty but can be used with Cron to auto-update your sites as well as quick access to manual operations.

  1. Create script touch /root/ghost-manage.sh You're free to place this script where you like.
  2. To configure the script change location to where your ghost instances are stored from /, in the example they are in /root/joelduncan-io & /root/renegademedia-uk.
  3. Change sites to reflect the name of your ghost instance folders.
  4. If not using a Debian & systemd based system comment or remove lines 43 & 44.
  5. Set script permissions
chmod 775 manage-ghost.sh
Set permissions
#!/bin/bash

# Setup
# Ghost Folder Location from /
location="root"

# Specify Ghost folders
sites=('joelduncan-io' 'renegademedia-uk')

case $1 in
  start)
    for site in "${sites[@]}"; do
        echo "Starting: $site"
        docker-compose -f /$location/$site/docker-compose.yml up -d
    done
    ;;
  stop)
    for site in "${sites[@]}"; do
        echo "Stopping: $site"
        docker-compose -f /$location/$site/docker-compose.yml down
    done
    ;;
  restart)
    for site in "${sites[@]}"; do
        echo "Restarting: $site"
        docker-compose -f /$location/$site/docker-compose.yml down
        docker-compose -f /$location/$site/docker-compose.yml up -d
    done
    ;;
  update)
    echo Updating...
    echo Pulling New Build
    docker pull ghost:latest

    for site in "${sites[@]}"; do
        echo "Updating: $site"
        echo Stopping Old Build
        docker-compose -f /$location/$site/docker-compose.yml down
        docker-compose -f /$location/$site/docker-compose.yml up -d
    done
    ;;
  cleanup)
    apt-get clean
    journalctl --vacuum-time=3d
    docker system prune -a -f
    ;;
    *)
    echo don\'t know
    ;;
esac
ghost-manage.sh

Manual Functions

Below is a list of manual functions provided by ghost-manage.sh:

# Start All Containers
./ghost-manage.sh start

# Stop All Containers
./ghost-manage.sh stop

# Restart All Containers
./ghost-manage.sh restart

# Upgrade Ghost Image
./ghost-manage.sh update

# Clean-up Old Docker Images, APT Cache & systemd
./ghost-manage.sh cleanup

Automatic Updates

Run crontab -e and enter the following to update your Ghost version daily.

45 4 * * * /root/ghost-sites.sh update

Maintenance

Regular Docker image updates will build up old versions, to stop these from consuming space on your server also add the following by running crontab -e.

0 0 * * 0 /root/ghost-manage.sh cleanup

Multiple Sites

Docker Compose

Adding a second site is as simple as creating a new folder in the same location as your previous Ghost instance, you will need another docker-compose file modified to match the new location e.g.

/root/ghostSite2/data:/var/lib/ghost/content

You'll also need to change the local port from 3001 to 3002 as shown below.

# docker-compose.yml

version: '3.2'

services:

  ghost:
    # We are specifying the Ghost Image to run
    image: ghost:latest
    
    # We want the container to restart in case the host gets restarted
    restart: always
    
    # Map host port 3002 to container port 2368
    ports:
      - 3002:2368
      
    # Mount host directory to container directory
    volumes:
      - /root/ghostSite2/data:/var/lib/ghost/content
      
    # Specify your Ghost blog URL and mail server settings
    environment:
      url: https://example.com
docker-compose.yml

NGINX

This is much the same story, use the same configuration but update the URL & proxy_pass port to match your new site e.g.

# example.com redirect block
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

# example.com
server {
    listen 443;
    server_name example.com www.example.com;
    root /opt/ghost/system/nginx-root;

    access_log /var/log/nginx/example-com-access.log;
    error_log /var/log/nginx/example-com-error.log;

    ssl on;
    ssl_certificate         /etc/nginx/ssl/example-com.pem;
    ssl_certificate_key     /etc/nginx/ssl/example-com.key;
    ssl_session_timeout 5m;
    ssl_ciphers               'AES128+EECDH:AES128+EDH:!aNULL';
    ssl_protocols              TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;

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

    }

    location ~ /.well-known {
        allow all;
    }

    client_max_body_size 50m;
    }

Modify ghost-manage.sh

Remember to refer to Automation and Updates when adding other sites as they will need to be added to ghost-manager.sh.

Configuring Email

In your docker-compose file paste the following below url in the environment section, then simply adjust the details to suit.

      mail__transport: SMTP
      mail__options__service: Gmail
      mail__options__host: smtp.gmail.com
      mail__options__port: 465
      mail__options__secureConnection: 'true'
      mail__options__auth__user: [email protected]
      mail__options__auth__pass: yourpassword

Additional Information

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.