At the start of August, Ghost introduced the highly-anticipated version 6.0. It most notably features ActivityPub and native analytics. It's awesome!

But there is another change that wasn’t as marketed, but is maybe just as important. The core architecture of how Ghost is hosted on servers is changing with 7.0, and the team at Ghost are so kind to offer a detailed tutorial.

From Ghost 7.0, instances will be run with Docker and Caddy to better manage the complexity of the services now used to run Ghost. In my opinion, the installation process and hosting are straight-forward, even easier than the classic Ghost way.

💡
Docker is the requirement if you want to run native analytics, but you can still use ActivityPub features using the classic installation with Ghost CLI and nginx.

Follow their installation tutorial for more, it makes everything very clear.

How To Install Ghost With Docker (preview) - Ghost Developer Docs
Preview our new batteries-included tools for self-hosting Ghost using Docker Compose.

How do you host multiple Docker containers?

Before moving all of my Ghost instances to Docker, my first challenge was to figure out how to run them side-by-side. This tutorial from Luke Harris rescued my multiple failures.

The solution is to use IPv6 addresses and Cloudflare. Once you get the hang of it, it's very straight-forward.

Nevertheless, I found it difficult to start and found no resource that teaches exactly how to do that. So I’m doing this here to save you a couple of hours.

Use a reliable hosting provider and Cloudflare

I'm using Hetzner (CX32 cloud) as my server provider; the server has run flawlessly so far.

After adding the SSH key to log into the server securely, start by creating a new user and assigning it with sudo privileges.

# Add a new user
adduser newuser

# Assign sudo privileges
sudo usermod -aG sudo newuser

# Set up SSH access (replace "public key" user's SSH publi key)
sudo mkdir -p /home/newuser/.ssh
echo "public-key" | sudo tee /home/newuser/.ssh/authorized_keys

# Set permissions
sudo chown -R newuser:newuser /home/newuser/.ssh
sudo chmod 700 /home/newuser/.ssh
sudo chmod 600 /home/newuser/.ssh/authorized_keys

On the other hand, handling the DNS settings through Cloudflare allows pointing the domains to IPv6 addresses instead of IPv4, using the AAAA instead of A records.

Assign unique IPv6 addresses

The first step is to find the IPv6 address of your server, most likely somewhere inside your provider's settings.

Once you have it, create a new IPv6 address for every Ghost instance. You can do that by accessing the netplan folder and changing its settings and permissions.

sudo nano /etc/netplan/50-cloud-init.yaml

Find the addresses part and add one for each Ghost instance. Include your IPv6 address and add the number at the end, like shown below.

addresses:
  - aaaa:bbbb:cccc:dddd::1/64
  - aaaa:bbbb:cccc:dddd::2/64
  - aaaa:bbbb:cccc:dddd::3/64
  - aaaa:bbbb:cccc:dddd::4/64
  - aaaa:bbbb:cccc:dddd::5/64
  - aaaa:bbbb:cccc:dddd::6/64

Save by pressing Ctrl+O, Enter and Ctrl+X.

💡
In the case of Hetzner, you can do that by editing the etc/netplan/50-cloud-init.yaml file. Other hosting providers might use a different file name, but it should be located in the same folder.

Next, run the following commands to set permissions, apply the settings and verify the IP addresses.

# Set correct permissions
sudo chmod 600 /etc/netplan/50-cloud-init.yaml

# Generate and apply updated netplan config
sudo netplan generate && sudo netplan apply

# Verify new IP addresses 
ip addr show eth0

This is almost everything you have to do outside Ghost's own tutorial to host multiple Docker instances.

Set AAAA records in Cloudflare

Next, move over to Cloudflare's DNS management area. For every domain you intend to connect to Ghost, assign it with an AAAA record corresponding to the IPv6 address you just created. You can also set up the admin domain by assigning admin.example.com to that same AAAA record.

💡
Cloudlfare is likely not the only provider that allows AAAA records. Before starting, make sure your provider has this option.

Install Ghost instance with Docker

This section will be the same as in Ghost Docker tutorial with one minor change.

Create a directory for each Ghost instance in the /opt folder. The easiest way is to just run the following command and changing the /opt/ghost folder to your desired name.

git clone https://github.com/TryGhost/ghost-docker.git /opt/ghost && cd /opt/ghost

Next, copy and paste the .env and Caddyfileto your new instance.

cp .env.example .env
cp caddy/Caddyfile.example caddy/Caddyfile
💡
If some commands are not executable, try running them with sudo.

Proceed to edit the .env and Caddyfile as you would according to Ghost Docker tutorial. Do not run any other commands just yet.

The last step is to edit the compose.yml file, and it’s important. Edit the ports in the Caddy settings under services by adding the corresponding IPv6 address for a given Ghost instance.

services:
  caddy:
    image: caddy:2.10.0-alpine
    restart: always
    ports:
      - "[aaaa:bbbb:cccc:dddd::2]:80:80"
      - "[aaaa:bbbb:cccc:dddd::2]:443:443"
    environment:
      DOMAIN: ${DOMAIN:?DOMAIN environment variable is required}
      ADMIN_DOMAIN: ${ADMIN_DOMAIN:-}
      ACTIVITYPUB_TARGET: ${ACTIVITYPUB_TARGET:-https://ap.ghost.org}
    volumes:
      - ./caddy:/etc/caddy
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - ghost
    networks:
      - ghost_network

Save the file by pressing Ctrl+O, Enter and Ctrl+X.

Pull Ghost image and run it

The last step you have to do is to run exactly 2 commands, and then you’re done.

# Make sure you're still inside your installation folder

docker compose pull

docker compose up -d

The instance should run without any problems. Repeat the same exact process for every new instance you want to run.

Concluding remarks

The installation is heavily based on the Ghost Docker tutorial, with the small change of assigning specific IPv6 addresses to each instance.

To set up native analytics and ActivityPub, just follow their guide. It's very straight-forward and easy to do.

Happy coding and publishing.